diff --git a/.github/workflows/auth-internal-release.yml b/.github/workflows/auth-internal-release.yml index 0a3d192856..4aec41202f 100644 --- a/.github/workflows/auth-internal-release.yml +++ b/.github/workflows/auth-internal-release.yml @@ -40,7 +40,7 @@ jobs: - name: Build PlayStore AAB run: | - flutter build appbundle --release --flavor playstore --dart-define=app.flavor=playstore + flutter build appbundle --dart-define=cronetHttpNoPlay=true --release --flavor playstore env: SIGNING_KEY_PATH: "/home/runner/work/_temp/keystore/ente_auth_key.jks" SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS }} @@ -54,3 +54,12 @@ jobs: packageName: io.ente.auth releaseFiles: auth/build/app/outputs/bundle/playstoreRelease/app-playstore-release.aab track: internal + + - name: Notify Discord + uses: sarisia/actions-status-discord@v1 + with: + webhook: ${{ secrets.DISCORD_INTERNAL_RELEASE_WEBHOOK }} + nodetail: true + title: "🏆 Internal release available for Auth" + description: "[Download](https://play.google.com/store/apps/details?id=io.ente.auth)" + color: 0x800080 diff --git a/.github/workflows/auth-release.yml b/.github/workflows/auth-release.yml index f1f8eff830..aa2bdc27d1 100644 --- a/.github/workflows/auth-release.yml +++ b/.github/workflows/auth-release.yml @@ -68,7 +68,7 @@ jobs: - name: Build independent APK run: | - flutter build apk --release --flavor independent --dart-define=app.flavor=independent + flutter build apk --dart-define=cronetHttpNoPlay=true --release --flavor independent mv build/app/outputs/flutter-apk/app-independent-release.apk artifacts/ente-${{ github.ref_name }}.apk env: SIGNING_KEY_PATH: "/home/runner/work/_temp/keystore/ente_auth_key.jks" diff --git a/.github/workflows/mobile-internal-release.yml b/.github/workflows/mobile-internal-release.yml index 6752fb1308..cbba50064f 100644 --- a/.github/workflows/mobile-internal-release.yml +++ b/.github/workflows/mobile-internal-release.yml @@ -40,7 +40,7 @@ jobs: - name: Build PlayStore AAB run: | - flutter build appbundle --release --flavor playstore + flutter build appbundle --dart-define=cronetHttpNoPlay=true --release --flavor playstore env: SIGNING_KEY_PATH: "/home/runner/work/_temp/keystore/ente_photos_key.jks" SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS_PHOTOS }} @@ -54,3 +54,12 @@ jobs: packageName: io.ente.photos releaseFiles: mobile/build/app/outputs/bundle/playstoreRelease/app-playstore-release.aab track: internal + + - name: Notify Discord + uses: sarisia/actions-status-discord@v1 + with: + webhook: ${{ secrets.DISCORD_INTERNAL_RELEASE_WEBHOOK }} + nodetail: true + title: "🏆 Internal release available for Photos" + description: "[Download](https://play.google.com/store/apps/details?id=io.ente.photos)" + color: 0x00ff00 diff --git a/.github/workflows/mobile-release.yml b/.github/workflows/mobile-release.yml index ecf2c6d769..8997f0afbc 100644 --- a/.github/workflows/mobile-release.yml +++ b/.github/workflows/mobile-release.yml @@ -45,7 +45,7 @@ jobs: - name: Build independent APK run: | - flutter build apk --release --flavor independent + flutter build apk --dart-define=cronetHttpNoPlay=true --release --flavor independent mv build/app/outputs/flutter-apk/app-independent-release.apk build/app/outputs/flutter-apk/ente-${{ github.ref_name }}.apk env: SIGNING_KEY_PATH: "/home/runner/work/_temp/keystore/ente_photos_key.jks" diff --git a/.github/workflows/server-publish.yml b/.github/workflows/server-publish.yml index a1d259480f..a677fd9eac 100644 --- a/.github/workflows/server-publish.yml +++ b/.github/workflows/server-publish.yml @@ -1,27 +1,24 @@ name: "Publish ghcr (server)" on: - # Run manually, providing it the commit. - # - # To obtain the commit from the currently deployed museum, do: - # curl -s https://api.ente.io/ping | jq -r '.id' - # - # See server/docs/publish.md for more details. + # Run automatically on 15th of every month, at 05:00 UTC. + schedule: + - cron: '0 5 15 * *' + # Run manually if needed to publish out of schedule. workflow_dispatch: - inputs: - commit: - description: "Commit to publish the image from" - type: string - required: true jobs: publish: runs-on: ubuntu-latest steps: + - name: Determine commit from prod museum + run: | + echo "museum_commit=$(curl -s https://api.ente.io/ping | jq -r .id)" >> $GITHUB_ENV + - name: Checkout code uses: actions/checkout@v4 with: - ref: ${{ inputs.commit }} + ref: ${{ env.museum_commit }} - name: Build and push uses: mr-smithers-excellent/docker-build-push@v6 @@ -34,8 +31,8 @@ jobs: enableBuildKit: true multiPlatform: true platform: linux/amd64,linux/arm64 - buildArgs: GIT_COMMIT=${{ inputs.commit }} - tags: ${{ inputs.commit }}, latest + buildArgs: GIT_COMMIT=${{ env.museum_commit }} + tags: ${{ env.museum_commit }}, latest username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} diff --git a/auth/assets/custom-icons/_data/custom-icons.json b/auth/assets/custom-icons/_data/custom-icons.json index 702c0dcd9e..73a0ac3142 100644 --- a/auth/assets/custom-icons/_data/custom-icons.json +++ b/auth/assets/custom-icons/_data/custom-icons.json @@ -35,9 +35,18 @@ { "title": "Amazon" }, + { + "title": "Ankama", + "slug": "ankama" + }, { "title": "Anycoin Direct", "slug": "anycoindirect" + }, + { + "title": "Aruba", + "slug": "aruba", + "hex": "ef8a33" }, { "title": "AscendEX" @@ -199,6 +208,10 @@ { "title": "Bugzilla" }, + { + "title": "ButterflyMX", + "slug": "butterflymx" + }, { "title": "Bybit" }, @@ -297,6 +310,9 @@ { "title": "Discourse" }, + { + "title": "Deloitte" + }, { "title": "DMarket" }, @@ -352,6 +368,14 @@ { "title": "Estateguru" }, + { + "title": "EVEOnline", + "slug": "eve_online", + "altNames": [ + "EVE Online" + ], + "hex": "858585" + }, { "title": "Fastmail" }, @@ -376,9 +400,17 @@ { "title": "ForUsAll" }, + { + "title": "FreeTaxUSA", + "slug": "freetaxusa" + }, { "title": "G2A" }, + { + "title": "Gate.io", + "slug": "gateio.svg" + }, { "title": "GitHub" }, @@ -717,7 +749,8 @@ { "title": "nintendo", "altNames": [ - "任天堂" + "任天堂", + "Nintendo Account" ] }, { @@ -734,6 +767,15 @@ { "title": "Notesnook" }, + { + "title": "NoIp", + "slug": "noip", + "altNames": [ + "No IP", + "No-IP", + "noip.com" + ] + }, { "title": "Notion" }, @@ -760,6 +802,11 @@ "altNames": [ "欧易" ] + }, + { + "title": "OnShape", + "slug": "onshape", + "hex": "7abb5e" }, { "title": "Parqet", @@ -814,6 +861,13 @@ "PostScanMail" ] }, + { + "title": "Prey Project", + "slug": "prey_project", + "altNames": [ + "PreyProject" + ] + }, { "title": "Privacy Guides", "slug": "privacyguides" @@ -857,6 +911,11 @@ { "title": "RealMe", "slug": "realme" + }, + { + "title": "RealVNC", + "slug": "realvnc", + "hex": "488aec" }, { "title": "Registro br", @@ -901,6 +960,10 @@ { "title": "Samsung" }, + { + "title": "Seafile", + "slug": "seafile" + }, { "title": "Sendgrid" }, @@ -1138,6 +1201,9 @@ { "title": "Wolvesville" }, + { + "title": "Workflowy" + }, { "title": "WorkOS", "altNames": [ @@ -1179,6 +1245,12 @@ }, { "title": "Zoom" + }, + { + "title": "BingX" + }, + { + "title": "CoinSpot" } ] } diff --git a/auth/assets/custom-icons/icons/ankama.svg b/auth/assets/custom-icons/icons/ankama.svg new file mode 100644 index 0000000000..5e21c7c4e8 --- /dev/null +++ b/auth/assets/custom-icons/icons/ankama.svg @@ -0,0 +1,5 @@ + + ankama + + + \ No newline at end of file diff --git a/auth/assets/custom-icons/icons/aruba.svg b/auth/assets/custom-icons/icons/aruba.svg new file mode 100644 index 0000000000..c078116929 --- /dev/null +++ b/auth/assets/custom-icons/icons/aruba.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/auth/assets/custom-icons/icons/bingx.svg b/auth/assets/custom-icons/icons/bingx.svg new file mode 100644 index 0000000000..9bcb83f42d --- /dev/null +++ b/auth/assets/custom-icons/icons/bingx.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/auth/assets/custom-icons/icons/butterflymx.svg b/auth/assets/custom-icons/icons/butterflymx.svg new file mode 100644 index 0000000000..b73c3b15b9 --- /dev/null +++ b/auth/assets/custom-icons/icons/butterflymx.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/auth/assets/custom-icons/icons/coinspot.svg b/auth/assets/custom-icons/icons/coinspot.svg new file mode 100644 index 0000000000..0d94e75a88 --- /dev/null +++ b/auth/assets/custom-icons/icons/coinspot.svg @@ -0,0 +1,130 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/auth/assets/custom-icons/icons/deloitte.svg b/auth/assets/custom-icons/icons/deloitte.svg new file mode 100644 index 0000000000..990f467189 --- /dev/null +++ b/auth/assets/custom-icons/icons/deloitte.svg @@ -0,0 +1,15 @@ + + Deloitte-svg + + + + + + + + + + \ No newline at end of file diff --git a/auth/assets/custom-icons/icons/eve_online.svg b/auth/assets/custom-icons/icons/eve_online.svg new file mode 100644 index 0000000000..816807454d --- /dev/null +++ b/auth/assets/custom-icons/icons/eve_online.svg @@ -0,0 +1,3 @@ + + + diff --git a/auth/assets/custom-icons/icons/freetaxusa.svg b/auth/assets/custom-icons/icons/freetaxusa.svg new file mode 100644 index 0000000000..a4fc2eb654 --- /dev/null +++ b/auth/assets/custom-icons/icons/freetaxusa.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/auth/assets/custom-icons/icons/gateio.svg b/auth/assets/custom-icons/icons/gateio.svg new file mode 100644 index 0000000000..83972c0d84 --- /dev/null +++ b/auth/assets/custom-icons/icons/gateio.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + diff --git a/auth/assets/custom-icons/icons/noip.svg b/auth/assets/custom-icons/icons/noip.svg new file mode 100644 index 0000000000..c15b4caa5f --- /dev/null +++ b/auth/assets/custom-icons/icons/noip.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/auth/assets/custom-icons/icons/prey_project.svg b/auth/assets/custom-icons/icons/prey_project.svg new file mode 100644 index 0000000000..cc3c19b8c2 --- /dev/null +++ b/auth/assets/custom-icons/icons/prey_project.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/auth/assets/custom-icons/icons/realvnc.svg b/auth/assets/custom-icons/icons/realvnc.svg new file mode 100644 index 0000000000..07bcac774a --- /dev/null +++ b/auth/assets/custom-icons/icons/realvnc.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/auth/assets/custom-icons/icons/seafile.svg b/auth/assets/custom-icons/icons/seafile.svg new file mode 100644 index 0000000000..0794ace85e --- /dev/null +++ b/auth/assets/custom-icons/icons/seafile.svg @@ -0,0 +1,13 @@ + + seafile + + + + + + + + + \ No newline at end of file diff --git a/auth/assets/custom-icons/icons/workflowy.svg b/auth/assets/custom-icons/icons/workflowy.svg new file mode 100644 index 0000000000..df6419cdba --- /dev/null +++ b/auth/assets/custom-icons/icons/workflowy.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/auth/flutter b/auth/flutter index 68415ad1d9..2663184aa7 160000 --- a/auth/flutter +++ b/auth/flutter @@ -1 +1 @@ -Subproject commit 68415ad1d920f6fe5ec284f5c2febf7c4dd5b0b3 +Subproject commit 2663184aa79047d0a33a14a3b607954f8fdd8730 diff --git a/auth/lib/core/network.dart b/auth/lib/core/network.dart index c14c9e758b..0f5ae3555d 100644 --- a/auth/lib/core/network.dart +++ b/auth/lib/core/network.dart @@ -8,6 +8,7 @@ import 'package:ente_auth/utils/package_info_util.dart'; import 'package:ente_auth/utils/platform_util.dart'; import 'package:fk_user_agent/fk_user_agent.dart'; import 'package:flutter/foundation.dart'; +import 'package:native_dio_adapter/native_dio_adapter.dart'; import 'package:uuid/uuid.dart'; int kConnectTimeout = 15000; @@ -50,6 +51,10 @@ class Network { }, ), ); + + _dio.httpClientAdapter = NativeAdapter(); + _enteDio.httpClientAdapter = NativeAdapter(); + _setupInterceptors(endpoint); Bus.instance.on().listen((event) { diff --git a/auth/lib/events/opened_settings_event.dart b/auth/lib/events/opened_settings_event.dart deleted file mode 100644 index 3aadeb1222..0000000000 --- a/auth/lib/events/opened_settings_event.dart +++ /dev/null @@ -1,3 +0,0 @@ -import 'package:ente_auth/events/event.dart'; - -class OpenedSettingsEvent extends Event {} diff --git a/auth/lib/json/converter.dart b/auth/lib/json/converter.dart deleted file mode 100644 index 9f45f8f8e5..0000000000 --- a/auth/lib/json/converter.dart +++ /dev/null @@ -1,17 +0,0 @@ -import 'dart:typed_data'; - -import "package:json_annotation/json_annotation.dart"; - -class Uint8ListConverter implements JsonConverter> { - const Uint8ListConverter(); - - @override - Uint8List fromJson(List? json) { - return json == null ? Uint8List(0) : Uint8List.fromList(json); - } - - @override - List toJson(Uint8List object) { - return object.toList(); - } -} diff --git a/auth/lib/l10n/arb/app_ar.arb b/auth/lib/l10n/arb/app_ar.arb index d2a3985d20..9b8e3e7462 100644 --- a/auth/lib/l10n/arb/app_ar.arb +++ b/auth/lib/l10n/arb/app_ar.arb @@ -482,7 +482,6 @@ "importFailureDescNew": "تعذر إعراب الملف المنتقى.", "duplicateCodes": "رموز مكررة", "noDuplicates": "✨ لا تكرارات", - "youveNoDuplicateCodesThatCanBeCleared": "ليس لديك رموز مكررة يمكن مسحها", "deselectAll": "ألغِ تحديد الكل", "selectAll": "حدد الكل", "deleteDuplicates": "احذف التكرار" diff --git a/auth/lib/l10n/arb/app_bg.arb b/auth/lib/l10n/arb/app_bg.arb index 156adf4bdc..d76ffbcc0d 100644 --- a/auth/lib/l10n/arb/app_bg.arb +++ b/auth/lib/l10n/arb/app_bg.arb @@ -499,10 +499,11 @@ "appLockOfflineModeWarning": "Избрахте да продължите без резервни копия. Ако забравите паролата на приложението си, ще бъдете заключени от достъп до вашите данни.", "duplicateCodes": "Повтарящи се кодове", "noDuplicates": "✨ Няма дубликати", - "youveNoDuplicateCodesThatCanBeCleared": "Нямате повтарящи се кодове, които могат да бъдат изчистени", "deduplicateCodes": "Премахване на повтарящи се кодове", "deselectAll": "Демаркиране на всички", "selectAll": "Избиране на всички", "deleteDuplicates": "Изтриване на дубликатите", - "plainHTML": "Обикновен HTML" + "plainHTML": "Обикновен HTML", + "tellUsWhatYouThink": "Кажете ни какво мислите", + "freeStorageOfferDescription": "Използвайте промокод „AUTH“, за да получите 10% отстъпка през първата година" } \ No newline at end of file diff --git a/auth/lib/l10n/arb/app_ca.arb b/auth/lib/l10n/arb/app_ca.arb index adef48b36e..012427ea50 100644 --- a/auth/lib/l10n/arb/app_ca.arb +++ b/auth/lib/l10n/arb/app_ca.arb @@ -88,6 +88,8 @@ "useRecoveryKey": "Usa la clau de recuperació", "incorrectPasswordTitle": "Contrasenya incorrecta", "welcomeBack": "Benvingut de nou!", + "emailAlreadyRegistered": "El correu electrònic ja està registrat.", + "emailNotRegistered": "El correu electrònic no està registrat.", "madeWithLoveAtPrefix": "fet amb ❤️ a ", "supportDevs": "Subscriu-te a ente per donar-nos suport", "supportDiscount": "Usa el codi de descompte \"AUTH\" per obtenir un 10% de descompte el primer any", @@ -497,10 +499,17 @@ "appLockOfflineModeWarning": "Has triat procedir sense còpies de seguretat. Si oblides el bloqueig de l'aplicació, no podràs accedir a les teves dades.", "duplicateCodes": "Codis duplicats", "noDuplicates": "✨ Sense duplicats", - "youveNoDuplicateCodesThatCanBeCleared": "No teniu codis duplicats que es puguin esborrar", "deduplicateCodes": "Desduplica codis", "deselectAll": "Desselecciona-ho tot", "selectAll": "Seleccionar-ho tot", "deleteDuplicates": "Elimina duplicats", - "plainHTML": "HTML pla" + "plainHTML": "HTML pla", + "tellUsWhatYouThink": "Digueu-nos què us sembla", + "dropReview": "Deixa una ressenya a l'App/Play Store", + "supportEnte": "Donar suport a ente", + "giveUsAStarOnGithub": "Dona'ns una estrella a Github", + "free5GB": "5 GB gratuïts a ente Photos", + "loginWithAuthAccount": "Inicieu sessió amb el vostre compte Auth", + "freeStorageOffer": "10% de descompte a ente photos", + "freeStorageOfferDescription": "Utilitzeu el codi \"AUTH\" per obtenir un 10% de descompte el primer any" } \ No newline at end of file diff --git a/auth/lib/l10n/arb/app_cs.arb b/auth/lib/l10n/arb/app_cs.arb index 04e8e411fb..5dfb19cc33 100644 --- a/auth/lib/l10n/arb/app_cs.arb +++ b/auth/lib/l10n/arb/app_cs.arb @@ -495,7 +495,6 @@ "appLockOfflineModeWarning": "Zvolili jste si pokračování bez zálohování. Pokud zapomenete heslo do aplikace, přístup k datům bude uzamčen.", "duplicateCodes": "Duplikovat kódy", "noDuplicates": "✨ Žádné duplikáty", - "youveNoDuplicateCodesThatCanBeCleared": "Nemáte žádné duplicitní kódy k odstranění", "deduplicateCodes": "Deduplikovat kódy", "deselectAll": "Zrušit výběr všech položek", "selectAll": "Vybrat vše", diff --git a/auth/lib/l10n/arb/app_de.arb b/auth/lib/l10n/arb/app_de.arb index 8bce3046c8..62c84330d4 100644 --- a/auth/lib/l10n/arb/app_de.arb +++ b/auth/lib/l10n/arb/app_de.arb @@ -88,6 +88,8 @@ "useRecoveryKey": "Wiederherstellungsschlüssel verwenden", "incorrectPasswordTitle": "Falsches Passwort", "welcomeBack": "Willkommen zurück!", + "emailAlreadyRegistered": "E-Mail ist bereits registriert.", + "emailNotRegistered": "E-Mail-Adresse nicht registriert.", "madeWithLoveAtPrefix": "gemacht mit ❤️ bei ", "supportDevs": "Bei ente registrieren, um das Projekt zu unterstützen", "supportDiscount": "Benutzen Sie den Rabattcode \"AUTH\" für 10 % Rabatt im ersten Jahr", @@ -145,6 +147,7 @@ "leaveFamily": "Familie verlassen", "leaveFamilyMessage": "Sind Sie sicher, dass Sie den Familien-Plan verlassen wollen?", "inFamilyPlanMessage": "Sie haben einen Familien-Plan!", + "hintForDesktop": "Klicken Sie mit der rechten Maustaste auf einen Code zum Bearbeiten oder Entfernen.", "scan": "Scannen", "scanACode": "Scan einen Code", "verify": "Überprüfen Sie", @@ -154,6 +157,7 @@ "twoFactorAuthTitle": "Zwei-Faktor-Authentifizierung", "passkeyAuthTitle": "Passkey Authentifizierung", "verifyPasskey": "Passkey verifizieren", + "loginWithTOTP": "Mit TOTP anmelden", "recoverAccount": "Konto wiederherstellen", "enterRecoveryKeyHint": "Geben Sie Ihren Wiederherstellungsschlüssel ein", "recover": "Wiederherstellen", @@ -255,6 +259,10 @@ "areYouSureYouWantToLogout": "Sind sie sicher, dass sie sich ausloggen möchten?", "yesLogout": "Ja ausloggen", "exit": "Schließen", + "theme": "Theme", + "lightTheme": "Hell", + "darkTheme": "Dunkel", + "systemTheme": "System", "verifyingRecoveryKey": "Verifiziere Wiederherstellungsschlüssel...", "recoveryKeyVerified": "Wiederherstellungsschlüssel verifiziert", "recoveryKeySuccessBody": "Großartig! Ihr Wiederherstellungsschlüssel ist gültig. Vielen Dank für die Verifizierung.\n\nBitte denken sie daran, dass sie ihren Wiederherstellungsschlüssel sicher aufbewahren.", @@ -325,6 +333,10 @@ } } }, + "manualSort": "Benutzerdefiniert", + "editOrder": "Reihenfolge bearbeiten", + "mostFrequentlyUsed": "Häufig verwendet", + "mostRecentlyUsed": "Zuletzt verwendet", "activeSessions": "Aktive Sitzungen", "somethingWentWrongPleaseTryAgain": "Ein Fehler ist aufgetreten, bitte versuche es erneut", "thisWillLogYouOutOfThisDevice": "Dadurch wirst du von diesem Gerät abgemeldet!", @@ -444,6 +456,7 @@ "customEndpoint": "Mit {endpoint} verbunden", "pinText": "Anpinnen", "unpinText": "Lösen", + "pinned": "Angeheftet", "tags": "Tags", "createNewTag": "Neuen Tag erstellen", "tag": "Tag", @@ -478,5 +491,16 @@ "setNewPin": "Neue PIN festlegen", "importFailureDescNew": "Die ausgewählte Datei konnte nicht verarbeitet werden.", "appLockNotEnabled": "App-Sperre nicht aktiviert", - "appLockNotEnabledDescription": "Bitte aktivieren Sie die App-Sperre über Security > App-Sperre" + "appLockNotEnabledDescription": "Bitte aktivieren Sie die App-Sperre über Security > App-Sperre", + "authToViewPasskey": "Bitte authentifizieren, um deinen Passkey zu sehen", + "duplicateCodes": "Doppelte Codes", + "noDuplicates": "✨ Keine Duplikate", + "deselectAll": "Alle abwählen", + "selectAll": "Alles auswählen", + "deleteDuplicates": "Duplikate löschen", + "plainHTML": "Reines HTML", + "tellUsWhatYouThink": "Sagen Sie uns, was Sie denken", + "dropReview": "Eine Bewertung im App/Play Store ablegen", + "giveUsAStarOnGithub": "Gib uns einen Stern auf Github", + "loginWithAuthAccount": "Mit Ihrem Auth Account anmelden" } \ 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 a51ec12366..9f450d8de3 100644 --- a/auth/lib/l10n/arb/app_en.arb +++ b/auth/lib/l10n/arb/app_en.arb @@ -499,7 +499,7 @@ "appLockOfflineModeWarning": "You have chosen to proceed without backups. If you forget your applock, you will be locked out from accessing your data.", "duplicateCodes": "Duplicate codes", "noDuplicates": "✨ No duplicates", - "youveNoDuplicateCodesThatCanBeCleared": "You've no duplicate codes that can be cleared", + "youveNoDuplicateCodesThatCanBeCleared": "You don't have any duplicate codes that can be cleared", "deduplicateCodes": "Deduplicate codes", "deselectAll": "Deselect all", "selectAll": "Select all", diff --git a/auth/lib/l10n/arb/app_es.arb b/auth/lib/l10n/arb/app_es.arb index 9ae3c65aae..c224d6677c 100644 --- a/auth/lib/l10n/arb/app_es.arb +++ b/auth/lib/l10n/arb/app_es.arb @@ -499,10 +499,16 @@ "appLockOfflineModeWarning": "Has elegido proceder sin copia de seguridad. Si olvidas el código de desbloqueo de la aplicación, se bloqueará el acceso a sus datos.", "duplicateCodes": "Duplicar códigos", "noDuplicates": "✨ No hay duplicados", - "youveNoDuplicateCodesThatCanBeCleared": "No tienes códigos duplicados que se puedan borrar", "deduplicateCodes": "Desduplicar códigos", "deselectAll": "Deseleccionar todo", "selectAll": "Seleccionar todo", "deleteDuplicates": "Eliminar duplicados", - "plainHTML": "HTML plano" + "plainHTML": "HTML plano", + "tellUsWhatYouThink": "Cuéntanos cuál es su opinión", + "dropReview": "Danos una reseña en la App/Play Store", + "supportEnte": "Apoya a ente", + "giveUsAStarOnGithub": "Danos una estrella en GitHub", + "free5GB": "5 GB gratis en ente Fotos", + "freeStorageOffer": "10% de descuento en ente fotos", + "freeStorageOfferDescription": "Usa el cupón \"AUTH\" para obtener un 10% de descuento en el primer año" } \ No newline at end of file diff --git a/auth/lib/l10n/arb/app_fr.arb b/auth/lib/l10n/arb/app_fr.arb index 49a4dd04a3..14dd5571d6 100644 --- a/auth/lib/l10n/arb/app_fr.arb +++ b/auth/lib/l10n/arb/app_fr.arb @@ -499,7 +499,6 @@ "appLockOfflineModeWarning": "Vous avez choisi de fonctionner sans sauvegardes. Si vous oubliez votre outil Applock, vous serez bloqué dans l'accès à vos données.", "duplicateCodes": "Codes dupliqués", "noDuplicates": "✨ Pas de doublons", - "youveNoDuplicateCodesThatCanBeCleared": "Vous n'avez aucun code en doublon pouvant être supprimé", "deduplicateCodes": "Codes dédupliqués", "deselectAll": "Tout désélectionner", "selectAll": "Tout sélectionner", diff --git a/auth/lib/l10n/arb/app_hi.arb b/auth/lib/l10n/arb/app_hi.arb index 6f311b95d5..728ef319cd 100644 --- a/auth/lib/l10n/arb/app_hi.arb +++ b/auth/lib/l10n/arb/app_hi.arb @@ -6,12 +6,15 @@ "@counterAppBarTitle": { "description": "Text shown in the AppBar of the Counter Page" }, + "onBoardingBody": "अपने 2FA कोड का सुरक्षित रूप से बैकअप लें", "onBoardingGetStarted": "प्रारंभ करें", "setupFirstAccount": "अपना पहला अकाउंट सेटअप करें", "importScanQrCode": "एक QR कोड स्कैन करें", "qrCode": "QR कोड", "importEnterSetupKey": "", "importAccountPageTitle": "अकाउंट विवरण डालें", + "secretCanNotBeEmpty": "सीक्रेट खाली नहीं हो सकता है", + "bothIssuerAndAccountCanNotBeEmpty": "दोनों इश्यूअर और अकाउंट ख़ाली नहीं हो सकते है", "incorrectDetails": "ग़लत विवरण", "pleaseVerifyDetails": "कृपया विवरण सत्यापित करें और पुनः प्रयास करें", "codeIssuerHint": "ज़ारीकर्ता", @@ -32,18 +35,24 @@ }, "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} पर लॉग ईमेल करें", @@ -57,6 +66,7 @@ "copyEmailAction": "ईमेल कॉपी करें", "exportLogsAction": "लॉग एक्सपोर्ट करें", "reportABug": "बग रिपोर्ट करें", + "crashAndErrorReporting": "क्रैश एवं त्रुटि रिपोर्टिंग", "reportBug": "बग रिपोर्ट करें", "emailUsMessage": "कृपया हमें {email} पर ईमेल करें", "@emailUsMessage": { @@ -69,14 +79,37 @@ "contactSupport": "सपोर्ट टीम से संपर्क करें", "rateUsOnStore": "हमें {storeName} पर रेट करें", "blog": "ब्लॉग", + "merchandise": "मर्चेंडाइज़", "verifyPassword": "पासवर्ड सत्यापित करें", "pleaseWait": "कृपया प्रतीक्षा करें...", + "generatingEncryptionKeysTitle": "एन्क्रिप्शन कुंजियाँ उत्पन्न हो रही हैं...", + "recreatePassword": "पासवर्ड दोबारा बनाएं", + "recreatePasswordMessage": "वर्तमान डिवाइस आपके पासवर्ड को सत्यापित करने के लिए पर्याप्त शक्तिशाली नहीं है, इसलिए हमें इसे सभी डिवाइसों के साथ काम करने वाले तरीके से एक बार पुन: उत्पन्न करने की आवश्यकता है। \n\nकृपया अपनी पुनर्प्राप्ति कुंजी का उपयोग करके लॉगिन करें और अपना पासवर्ड पुनः बनाएं (यदि आप चाहें तो उसी का दोबारा उपयोग कर सकते हैं)।", + "useRecoveryKey": "रिकवरी कुंजी का उपयोग करें", "incorrectPasswordTitle": "ग़लत पासवर्ड", "welcomeBack": "आपका पुनः स्वागत है!", + "emailAlreadyRegistered": "ईमेल पहले से ही पंजीकृत है।", + "emailNotRegistered": "ईमेल पंजीकृत नहीं है।", + "madeWithLoveAtPrefix": " ❤️ से बनाया गया ", + "supportDevs": "हमें समर्थन देने के लिए ente की सदस्यता लें", + "supportDiscount": "पहले साल 10% छूट पाने के लिए कूपन कोड \"AUTH\" का उपयोग करें", "changeEmail": "ईमेल बदलें", "changePassword": "पासवर्ड बदलें", "data": "डेटा", + "importCodes": "कोड आयात करें", + "importTypePlainText": "साधारण टेक्स्ट", + "importTypeEnteEncrypted": "Ente द्वारा एनक्रिप्टेड टेक्स्ट", + "passwordForDecryptingExport": "डीक्रिप्ट करने के लिए पासवर्ड", "passwordEmptyError": "पासवर्ड रिक्त नहीं हो सकता है", + "importFromApp": "{appName} से कोड इंपोर्ट करें", + "importGoogleAuthGuide": "ट्रांसफर अकाउंट्स\" विकल्प का उपयोग करके अपने खातों को Google प्रमाणक से एक क्यूआर कोड में निर्यात करें। फिर किसी अन्य डिवाइस का उपयोग करके QR कोड को स्कैन करें।\n\nटिप: क्यूआर कोड की तस्वीर लेने के लिए आप अपने लैपटॉप के वेबकैम का उपयोग कर सकते हैं।", + "importSelectJsonFile": "JSON फाइल चुनें", + "importSelectAppExport": "{appName} की निर्यात फ़ाइल का चयन करें", + "importEnteEncGuide": "Ente से निर्यात की गई एन्क्रिप्टेड JSON फ़ाइल का चयन करें", + "importRaivoGuide": "Raivo की सेटिंग्स में \"एक्सपोर्ट ओटीपी टू जिप आर्काइव\" विकल्प का उपयोग करें।\n\nज़िप फ़ाइल निकालें और JSON फ़ाइल आयात करें।", + "importBitwardenGuide": "बिटवर्डन टूल्स के भीतर \"एक्सपोर्ट वॉल्ट\" विकल्प का उपयोग करें और अनएन्क्रिप्टेड JSON फ़ाइल आयात करें।", + "importAegisGuide": "Aegis की सेटिंग्स में \"एक्सपोर्ट द वॉल्ट\" विकल्प का उपयोग करें।\n\nयदि आपकी वॉल्ट एन्क्रिप्टेड है, तो आपको वॉल्ट को डिक्रिप्ट करने के लिए वॉल्ट पासवर्ड दर्ज करना होगा।", + "import2FasGuide": "2FAS में \"सेटिंग्स->बैकअप-एक्सपोर्ट\" विकल्प का उपयोग करें।\n\nयदि आपका बैकअप एन्क्रिप्टेड है, तो आपको बैकअप को डिक्रिप्ट करने के लिए पासवर्ड दर्ज करना होगा", "importLabel": "इंपोर्ट", "selectFile": "फ़ाइल का चयन करें", "emailVerificationToggle": "ईमेल सत्यापन", diff --git a/auth/lib/l10n/arb/app_hu.arb b/auth/lib/l10n/arb/app_hu.arb index 10fcee33ec..b68a3d8d7a 100644 --- a/auth/lib/l10n/arb/app_hu.arb +++ b/auth/lib/l10n/arb/app_hu.arb @@ -499,10 +499,17 @@ "appLockOfflineModeWarning": "Úgy döntött, hogy biztonsági mentés nélkül folytatja. Ha elfelejti az alkalmazászárat, akkor nem férhet hozzá adataihoz.", "duplicateCodes": "Ismétlődő kódok", "noDuplicates": "✨Nincs ismétlődés", - "youveNoDuplicateCodesThatCanBeCleared": "Nincsenek ismétlődő kódjai, amelyeket törölni lehetne", "deduplicateCodes": "Ismétlődő kódok", "deselectAll": "Összes kijelölés megszüntetése", "selectAll": "Összes kijelölése", "deleteDuplicates": "Ismétlődések törlése", - "plainHTML": "Sima HTML kód" + "plainHTML": "Sima HTML kód", + "tellUsWhatYouThink": "Mondja el mit gondol", + "dropReview": "Írjon véleményt az App/Play Store-ban", + "supportEnte": "Támogassa ente ", + "giveUsAStarOnGithub": "Adj nekünk egy csillagot a Githubon", + "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" } \ 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 628c1d650f..c791a866a9 100644 --- a/auth/lib/l10n/arb/app_id.arb +++ b/auth/lib/l10n/arb/app_id.arb @@ -497,7 +497,6 @@ "appLockOfflineModeWarning": "Anda telah memilih untuk mengunci aplikasi tanpa cadangan apa pun. Jika Anda lupa kode Pengunci Apl Anda, Anda tidak akan dapat mengakses data-data Anda.", "duplicateCodes": "Kode duplikat", "noDuplicates": "✨ Tak ada duplikat", - "youveNoDuplicateCodesThatCanBeCleared": "Kamu tidak memiliki kode duplikat yang dapat dihapus", "deduplicateCodes": "Hapus kode duplikat", "deselectAll": "Batalkan semua pilihan", "selectAll": "Pilih semua", diff --git a/auth/lib/l10n/arb/app_it.arb b/auth/lib/l10n/arb/app_it.arb index 17281db08a..c68a87301b 100644 --- a/auth/lib/l10n/arb/app_it.arb +++ b/auth/lib/l10n/arb/app_it.arb @@ -499,7 +499,6 @@ "appLockOfflineModeWarning": "Hai scelto di procedere senza backup. Se dimentichi il tuo codice di blocco dell'app, non potrai più accedere ai tuoi dati.", "duplicateCodes": "Codici duplicati", "noDuplicates": "✨ Nessun doppione", - "youveNoDuplicateCodesThatCanBeCleared": "Non ci sono codici duplicati che possono essere cancellati", "deduplicateCodes": "Codici deduplicati", "deselectAll": "Deselezionare tutti", "selectAll": "Seleziona tutti", diff --git a/auth/lib/l10n/arb/app_ja.arb b/auth/lib/l10n/arb/app_ja.arb index 3ad6e048ef..49cd9a1e84 100644 --- a/auth/lib/l10n/arb/app_ja.arb +++ b/auth/lib/l10n/arb/app_ja.arb @@ -499,7 +499,17 @@ "appLockOfflineModeWarning": "バックアップなしで進むことを選択しました。アプリロックを忘れると、データにアクセスできなくなります。", "duplicateCodes": "重複コード", "noDuplicates": "✨ 重複なし", + "deduplicateCodes": "重複コード", + "deselectAll": "すべての選択を解除", + "selectAll": "すべて選択", + "deleteDuplicates": "重複を削除", "plainHTML": "Plain HTML", "tellUsWhatYouThink": "ご意見をお聞かせください", - "loginWithAuthAccount": "認証アカウントでログイン" + "dropReview": "App/Playストアにレビューを投稿する", + "supportEnte": "enteをサポートする", + "giveUsAStarOnGithub": "Githubで星をつける", + "free5GB": "enteフォトで5GB無料", + "loginWithAuthAccount": "認証アカウントでログイン", + "freeStorageOffer": "enteの写真が10%オフ", + "freeStorageOfferDescription": "クーポンコード \"AUTH\" の使用で初年度が10%オフになります" } \ No newline at end of file diff --git a/auth/lib/l10n/arb/app_ko.arb b/auth/lib/l10n/arb/app_ko.arb index 0af9fa10f0..a250219cee 100644 --- a/auth/lib/l10n/arb/app_ko.arb +++ b/auth/lib/l10n/arb/app_ko.arb @@ -499,7 +499,6 @@ "appLockOfflineModeWarning": "백업 없이 진행하는 것을 선택하셨습니다. App 잠금 방법을 잊어버리신 경우, 데이터에 접근하실 수 없게 됩니다.", "duplicateCodes": "중복된 코드", "noDuplicates": "✨ 중복 없음", - "youveNoDuplicateCodesThatCanBeCleared": "지울 수 있는 중복 코드가 없습니다", "deduplicateCodes": "중복된 코드 제거", "deselectAll": "모두 선택 해제", "selectAll": "모두 선택", diff --git a/auth/lib/l10n/arb/app_lt.arb b/auth/lib/l10n/arb/app_lt.arb index aa00c7b1ab..71b96d3027 100644 --- a/auth/lib/l10n/arb/app_lt.arb +++ b/auth/lib/l10n/arb/app_lt.arb @@ -499,11 +499,15 @@ "appLockOfflineModeWarning": "Pasirinkote tęsti be atsarginių kopijų. Jei pamiršite programos užraktą, jums bus užrakinta prieiga prie duomenų.", "duplicateCodes": "Dubliuoti kodus", "noDuplicates": "✨ Dublikatų nėra", - "youveNoDuplicateCodesThatCanBeCleared": "Neturite dubliuotų kodų, kuriuos būtų galima išvalyti.", "deduplicateCodes": "Atdubliuoti kodus", "deselectAll": "Naikinti visų pasirinkimą", "selectAll": "Pasirinkti viską", "deleteDuplicates": "Ištrinti dublikatus", "plainHTML": "Grynasis HTML", + "tellUsWhatYouThink": "Pasakykite mums, ką manote", + "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. " } \ No newline at end of file diff --git a/auth/lib/l10n/arb/app_ml.arb b/auth/lib/l10n/arb/app_ml.arb index 9e26dfeeb6..ed441ead94 100644 --- a/auth/lib/l10n/arb/app_ml.arb +++ b/auth/lib/l10n/arb/app_ml.arb @@ -1 +1,28 @@ -{} \ No newline at end of file +{ + "blog": "ബ്ലോഗ്", + "verifyPassword": "പാസ്‌വേഡ് സ്ഥിരീകരിക്കുക", + "recreatePassword": "പാസ്‌വേഡ് പുനഃസൃഷ്ടിക്കുക", + "incorrectPasswordTitle": "തെറ്റായ പാസ്‌വേഡ്", + "welcomeBack": "വീണ്ടും സ്വാഗതം!", + "emailAlreadyRegistered": "ഇമെയിൽ ഇതിനകം രജിസ്റ്റർ ചെയ്തിട്ടുണ്ട്.", + "emailNotRegistered": "ഇമെയിൽ രജിസ്റ്റർ ചെയ്തിട്ടില്ല.", + "changeEmail": "ഇമെയിൽ മാറ്റുക", + "changePassword": "പാസ്സ്‌വേർഡ് മാറ്റുക", + "ok": "ശരി", + "cancel": "റദ്ദാക്കുക", + "yes": "അതെ", + "no": "അല്ല", + "email": "ഇമെയിൽ", + "somethingWentWrongMessage": "എന്തോ കുഴപ്പമുണ്ടായി, ദയവായി വീണ്ടും ശ്രമിക്കുക", + "inFamilyPlanMessage": "നിങ്ങൾ ഒരു ഫാമിലി പ്ലാനിലാണ്!", + "scan": "സ്കാൻ ചെയ്യുക", + "scanACode": "കോഡ് സ്കാൻ ചെയ്യുക", + "verify": "പരിശോധിക്കുക", + "verifyEmail": "ഇമെയിൽ സ്ഥിരീകരിക്കുക", + "enterCodeHint": "നിങ്ങളുടെ ഓതന്റിക്കേറ്റർ ആപ്പിൽ നിന്നുള്ള 6 അക്ക കോഡ് നൽകുക", + "twoFactorAuthTitle": "ടു-ഫാക്ടർ ആധികാരികത", + "createNewAccount": "പുതിയ അക്കൗണ്ട് സൃഷ്ടിക്കുക", + "confirmPassword": "പാസ്വേഡ് സ്ഥിരീകരിക്കുക", + "language": "ഭാഷ", + "security": "സുരക്ഷ" +} \ 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 020646852f..c3d8fa7dd8 100644 --- a/auth/lib/l10n/arb/app_nl.arb +++ b/auth/lib/l10n/arb/app_nl.arb @@ -499,7 +499,6 @@ "appLockOfflineModeWarning": "Je hebt ervoor gekozen om verder te gaan zonder backups. Als je jouw applock vergeet, wordt je uitgesloten van toegang tot je gegevens.", "duplicateCodes": "Dubbele codes", "noDuplicates": "✨ Geen dubbele", - "youveNoDuplicateCodesThatCanBeCleared": "Je hebt geen dubbele codes die kunnen worden gewist", "deduplicateCodes": "Dubbele codes", "deselectAll": "Alles deselecteren", "selectAll": "Alles selecteren", diff --git a/auth/lib/l10n/arb/app_pl.arb b/auth/lib/l10n/arb/app_pl.arb index 95eddd5fd5..e2c6b26f6a 100644 --- a/auth/lib/l10n/arb/app_pl.arb +++ b/auth/lib/l10n/arb/app_pl.arb @@ -499,7 +499,7 @@ "appLockOfflineModeWarning": "Wybrano kontynuowanie bez kopii zapasowych. Jeśli zapomnisz blokady aplikacji, utracisz dostęp do swoich danych.", "duplicateCodes": "Zduplikowane kody", "noDuplicates": "✨ Brak duplikatów", - "youveNoDuplicateCodesThatCanBeCleared": "Nie masz duplikatów kodów, które mogą być wyczyszczone", + "youveNoDuplicateCodesThatCanBeCleared": "Nie masz żadnych duplikatów kodów do usunięcia", "deduplicateCodes": "Deduplikuj kody", "deselectAll": "Odznacz wszystko", "selectAll": "Zaznacz wszystko", diff --git a/auth/lib/l10n/arb/app_pt.arb b/auth/lib/l10n/arb/app_pt.arb index 5b65e71b1e..cc7d6f4975 100644 --- a/auth/lib/l10n/arb/app_pt.arb +++ b/auth/lib/l10n/arb/app_pt.arb @@ -499,7 +499,7 @@ "appLockOfflineModeWarning": "Você prosseguiu sem cópias de segurança. Caso, se esqueça de seu aplicativo de bloqueio, você não poderá mais acessar seus dados.", "duplicateCodes": "Duplicar códigos", "noDuplicates": "✨ Sem duplicados", - "youveNoDuplicateCodesThatCanBeCleared": "Você não possui códigos duplicados para limpar", + "youveNoDuplicateCodesThatCanBeCleared": "Você não possui códigos duplicados que possam ser excluídos", "deduplicateCodes": "Desduplicar códigos", "deselectAll": "Deselecionar tudo", "selectAll": "Selecionar tudo", diff --git a/auth/lib/l10n/arb/app_sv.arb b/auth/lib/l10n/arb/app_sv.arb index a096aa931e..facf5c1c1e 100644 --- a/auth/lib/l10n/arb/app_sv.arb +++ b/auth/lib/l10n/arb/app_sv.arb @@ -451,7 +451,6 @@ "appLockOfflineModeWarning": "Du har valt att fortsätta utan säkerhetskopior. Om du glömmer ditt applås, kommer du att bli utelåst från att komma åt dina data.", "duplicateCodes": "Dubblettkoder", "noDuplicates": "✨ Inga dubbletter", - "youveNoDuplicateCodesThatCanBeCleared": "Du har inga dubblettkoder som kan rensas bort", "deduplicateCodes": "Deduplicera koder", "deselectAll": "Avmarkera alla", "selectAll": "Markera alla", diff --git a/auth/lib/l10n/arb/app_tr.arb b/auth/lib/l10n/arb/app_tr.arb index 965689a53d..aee7870c06 100644 --- a/auth/lib/l10n/arb/app_tr.arb +++ b/auth/lib/l10n/arb/app_tr.arb @@ -499,7 +499,6 @@ "appLockOfflineModeWarning": "Yedekleme olmadan devam etmeyi seçtiniz. Eğer uygulama parolanızı unutursanız, verilerinize erişiminiz engellenir.", "duplicateCodes": "Yinelenen kodlar", "noDuplicates": "✨ Yinelenen yok", - "youveNoDuplicateCodesThatCanBeCleared": "Temizlenebilecek yinelenen kodunuz yok", "deduplicateCodes": "Kodları tekilleştir", "deselectAll": "Tümünün seçimini kaldır", "selectAll": "Tümünü seç", diff --git a/auth/lib/l10n/arb/app_uk.arb b/auth/lib/l10n/arb/app_uk.arb index a758171d89..09c485b7c2 100644 --- a/auth/lib/l10n/arb/app_uk.arb +++ b/auth/lib/l10n/arb/app_uk.arb @@ -497,7 +497,6 @@ "appLockOfflineModeWarning": "Ви обрали продовжити без резервних копій. Якщо ви забудете свій пароль, доступ до ваших даних буде заблоковано.", "duplicateCodes": "Дублікати кодів", "noDuplicates": "✨ Немає дублікатів", - "youveNoDuplicateCodesThatCanBeCleared": "У вас немає дублікатів кодів, які можна очистити", "deduplicateCodes": "Дедуплікувати коди", "deselectAll": "Зняти виділення", "selectAll": "Вибрати все", diff --git a/auth/lib/l10n/arb/app_vi.arb b/auth/lib/l10n/arb/app_vi.arb index e186b4bbfd..1c7dbecb1e 100644 --- a/auth/lib/l10n/arb/app_vi.arb +++ b/auth/lib/l10n/arb/app_vi.arb @@ -499,7 +499,6 @@ "appLockOfflineModeWarning": "Bạn đã chọn tiếp tục mà không có bản sao lưu. Nếu bạn quên khóa ứng dụng, bạn sẽ bị khóa khỏi việc truy cập dữ liệu của mình.", "duplicateCodes": "Mã trùng lặp", "noDuplicates": "✨ Không có trùng lặp", - "youveNoDuplicateCodesThatCanBeCleared": "Bạn không có mã trùng lặp nào có thể được xóa", "deduplicateCodes": "Loại bỏ mã trùng lặp", "deselectAll": "Bỏ chọn tất cả", "selectAll": "Chọn tất cả", diff --git a/auth/lib/l10n/arb/app_zh.arb b/auth/lib/l10n/arb/app_zh.arb index a15b3f3896..b9c7ebc4ab 100644 --- a/auth/lib/l10n/arb/app_zh.arb +++ b/auth/lib/l10n/arb/app_zh.arb @@ -499,7 +499,6 @@ "appLockOfflineModeWarning": "您已选择继续而不备份。如果您忘记了应用锁,您将无法访问数据。", "duplicateCodes": "重复代码", "noDuplicates": "✨ 没有重复", - "youveNoDuplicateCodesThatCanBeCleared": "您没有可清除的重复代码", "deduplicateCodes": "删除重复代码", "deselectAll": "取消全选", "selectAll": "全选", diff --git a/auth/lib/models/magic_metadata.dart b/auth/lib/models/magic_metadata.dart deleted file mode 100644 index 9edab547ee..0000000000 --- a/auth/lib/models/magic_metadata.dart +++ /dev/null @@ -1,73 +0,0 @@ -import 'dart:convert'; - -const visibilityVisible = 0; -const visibilityArchive = 1; - -const magicKeyVisibility = 'visibility'; - -const pubMagicKeyEditedTime = 'editedTime'; -const pubMagicKeyEditedName = 'editedName'; - -class MagicMetadata { - // 0 -> visible - // 1 -> archived - // 2 -> hidden etc? - int visibility; - - MagicMetadata({required this.visibility}); - - factory MagicMetadata.fromEncodedJson(String encodedJson) => - MagicMetadata.fromJson(jsonDecode(encodedJson)); - - factory MagicMetadata.fromJson(dynamic json) => MagicMetadata.fromMap(json); - - static fromMap(Map? map) { - if (map == null) return null; - return MagicMetadata( - visibility: map[magicKeyVisibility] ?? visibilityVisible, - ); - } -} - -class PubMagicMetadata { - int? editedTime; - String? editedName; - - PubMagicMetadata({this.editedTime, this.editedName}); - - factory PubMagicMetadata.fromEncodedJson(String encodedJson) => - PubMagicMetadata.fromJson(jsonDecode(encodedJson)); - - factory PubMagicMetadata.fromJson(dynamic json) => - PubMagicMetadata.fromMap(json); - - static fromMap(Map? map) { - if (map == null) return null; - return PubMagicMetadata( - editedTime: map[pubMagicKeyEditedTime], - editedName: map[pubMagicKeyEditedName], - ); - } -} - -class CollectionMagicMetadata { - // 0 -> visible - // 1 -> archived - // 2 -> hidden etc? - int visibility; - - CollectionMagicMetadata({required this.visibility}); - - factory CollectionMagicMetadata.fromEncodedJson(String encodedJson) => - CollectionMagicMetadata.fromJson(jsonDecode(encodedJson)); - - factory CollectionMagicMetadata.fromJson(dynamic json) => - CollectionMagicMetadata.fromMap(json); - - static fromMap(Map? map) { - if (map == null) return null; - return CollectionMagicMetadata( - visibility: map[magicKeyVisibility] ?? visibilityVisible, - ); - } -} diff --git a/auth/lib/models/public_key.dart b/auth/lib/models/public_key.dart deleted file mode 100644 index 0d14a4a557..0000000000 --- a/auth/lib/models/public_key.dart +++ /dev/null @@ -1,6 +0,0 @@ -class PublicKey { - final String email; - final String publicKey; - - PublicKey(this.email, this.publicKey); -} diff --git a/auth/lib/services/auth_feature_flag.dart b/auth/lib/services/auth_feature_flag.dart deleted file mode 100644 index 92a1c540d7..0000000000 --- a/auth/lib/services/auth_feature_flag.dart +++ /dev/null @@ -1,23 +0,0 @@ -import 'package:ente_auth/core/configuration.dart'; -import 'package:flutter/foundation.dart'; - -class FeatureFlagService { - FeatureFlagService._privateConstructor(); - static final FeatureFlagService instance = - FeatureFlagService._privateConstructor(); - - static final _internalUserIDs = const String.fromEnvironment( - "internal_user_ids", - defaultValue: "1,2,3,4,191,125,1580559962388044,1580559962392434,10000025", - ).split(",").map((element) { - return int.parse(element); - }).toSet(); - - bool isInternalUserOrDebugBuild() { - final String? email = Configuration.instance.getEmail(); - final userID = Configuration.instance.getUserID(); - return (email != null && email.endsWith("@ente.io")) || - _internalUserIDs.contains(userID) || - kDebugMode; - } -} diff --git a/auth/lib/services/window_listener_service.dart b/auth/lib/services/window_listener_service.dart index 73f431116f..bb5d1cbcfa 100644 --- a/auth/lib/services/window_listener_service.dart +++ b/auth/lib/services/window_listener_service.dart @@ -6,7 +6,7 @@ import 'package:shared_preferences/shared_preferences.dart'; import 'package:window_manager/window_manager.dart'; class WindowListenerService { - static const double minWindowHeight = 320.0; + static const double minWindowHeight = 600.0; static const double minWindowWidth = 800.0; static const double maxWindowHeight = 8192.0; static const double maxWindowWidth = 8192.0; diff --git a/auth/lib/store/user_store.dart b/auth/lib/store/user_store.dart deleted file mode 100644 index b191d169d8..0000000000 --- a/auth/lib/store/user_store.dart +++ /dev/null @@ -1,14 +0,0 @@ -import 'package:shared_preferences/shared_preferences.dart'; - -class UserStore { - UserStore._privateConstructor(); - - // ignore: unused_field - late SharedPreferences _preferences; - - static final UserStore instance = UserStore._privateConstructor(); - - Future init() async { - _preferences = await SharedPreferences.getInstance(); - } -} diff --git a/auth/lib/ui/account/login_pwd_verification_page.dart b/auth/lib/ui/account/login_pwd_verification_page.dart index d5ae7a3dbc..6d43988e2c 100644 --- a/auth/lib/ui/account/login_pwd_verification_page.dart +++ b/auth/lib/ui/account/login_pwd_verification_page.dart @@ -113,7 +113,7 @@ class _LoginPasswordVerificationPageState ); } else { _logger.severe('API failure during SRP login', e, s); - if (e.type == DioExceptionType.unknown) { + if (e.type == DioExceptionType.connectionError) { await _showContactSupportDialog( context, context.l10n.noInternetConnection, diff --git a/auth/lib/ui/common/bottom_shadow.dart b/auth/lib/ui/common/bottom_shadow.dart deleted file mode 100644 index 2de7e944e2..0000000000 --- a/auth/lib/ui/common/bottom_shadow.dart +++ /dev/null @@ -1,25 +0,0 @@ -import 'package:flutter/material.dart'; - -class BottomShadowWidget extends StatelessWidget { - final double offsetDy; - final Color? shadowColor; - const BottomShadowWidget({this.offsetDy = 28, this.shadowColor, super.key}); - - @override - Widget build(BuildContext context) { - return Container( - height: 8, - decoration: BoxDecoration( - color: Colors.transparent, - boxShadow: [ - BoxShadow( - color: shadowColor ?? Theme.of(context).colorScheme.surface, - spreadRadius: 42, - blurRadius: 42, - offset: Offset(0, offsetDy), // changes position of shadow - ), - ], - ), - ); - } -} diff --git a/auth/lib/ui/common/linear_progress_dialog.dart b/auth/lib/ui/common/linear_progress_dialog.dart deleted file mode 100644 index 08c46d6c97..0000000000 --- a/auth/lib/ui/common/linear_progress_dialog.dart +++ /dev/null @@ -1,49 +0,0 @@ -import 'package:ente_auth/ente_theme_data.dart'; -import 'package:flutter/material.dart'; - -class LinearProgressDialog extends StatefulWidget { - final String message; - - const LinearProgressDialog(this.message, {super.key}); - - @override - LinearProgressDialogState createState() => LinearProgressDialogState(); -} - -class LinearProgressDialogState extends State { - double? _progress; - - @override - void initState() { - _progress = 0; - super.initState(); - } - - void setProgress(double progress) { - setState(() { - _progress = progress; - }); - } - - @override - Widget build(BuildContext context) { - return PopScope( - canPop: false, - child: AlertDialog( - title: Text( - widget.message, - style: const TextStyle( - fontSize: 16, - ), - textAlign: TextAlign.center, - ), - content: LinearProgressIndicator( - value: _progress, - valueColor: AlwaysStoppedAnimation( - Theme.of(context).colorScheme.alternativeColor, - ), - ), - ), - ); - } -} diff --git a/auth/lib/ui/common/rename_dialog.dart b/auth/lib/ui/common/rename_dialog.dart deleted file mode 100644 index ad93d1abaa..0000000000 --- a/auth/lib/ui/common/rename_dialog.dart +++ /dev/null @@ -1,98 +0,0 @@ - - -import 'package:ente_auth/utils/dialog_util.dart'; -import 'package:flutter/material.dart'; - -class RenameDialog extends StatefulWidget { - final String name; - final String type; - final int maxLength; - - const RenameDialog(this.name, this.type, {super.key, this.maxLength = 100}); - - @override - State createState() => _RenameDialogState(); -} - -class _RenameDialogState extends State { - String? _newName; - - @override - void initState() { - super.initState(); - _newName = widget.name; - } - - @override - Widget build(BuildContext context) { - return AlertDialog( - title: const Text("Enter a new name"), - content: SingleChildScrollView( - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - TextFormField( - decoration: InputDecoration( - hintText: '${widget.type} name', - hintStyle: const TextStyle( - color: Colors.white30, - ), - contentPadding: const EdgeInsets.all(12), - ), - onChanged: (value) { - setState(() { - _newName = value; - }); - }, - autocorrect: false, - keyboardType: TextInputType.text, - initialValue: _newName, - autofocus: true, - ), - ], - ), - ), - actions: [ - TextButton( - child: const Text( - "Cancel", - style: TextStyle( - color: Colors.redAccent, - ), - ), - onPressed: () { - Navigator.of(context).pop(null); - }, - ), - TextButton( - child: Text( - "Rename", - style: TextStyle( - color: Theme.of(context).colorScheme.onSurface, - ), - ), - onPressed: () { - if (_newName!.trim().isEmpty) { - showErrorDialog( - context, - "Empty name", - "${widget.type} name cannot be empty", - ); - return; - } - if (_newName!.trim().length > widget.maxLength) { - showErrorDialog( - context, - "Name too large", - "${widget.type} name should be less than ${widget.maxLength} characters", - ); - return; - } - Navigator.of(context).pop(_newName!.trim()); - }, - ), - ], - ); - } -} diff --git a/auth/lib/ui/components/home_header_widget.dart b/auth/lib/ui/components/home_header_widget.dart deleted file mode 100644 index 0079cc7fa7..0000000000 --- a/auth/lib/ui/components/home_header_widget.dart +++ /dev/null @@ -1,41 +0,0 @@ -import 'package:ente_auth/core/event_bus.dart'; -import 'package:ente_auth/events/opened_settings_event.dart'; -import 'package:flutter/material.dart'; - -class HomeHeaderWidget extends StatefulWidget { - final Widget centerWidget; - const HomeHeaderWidget({required this.centerWidget, super.key}); - - @override - State createState() => _HomeHeaderWidgetState(); -} - -class _HomeHeaderWidgetState extends State { - @override - Widget build(BuildContext context) { - final hasNotch = View.of(context).viewPadding.top > 65; - return Padding( - padding: EdgeInsets.fromLTRB(4, hasNotch ? 4 : 8, 4, 4), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - IconButton( - visualDensity: const VisualDensity(horizontal: -2, vertical: -2), - onPressed: () { - Scaffold.of(context).openDrawer(); - Bus.instance.fire(OpenedSettingsEvent()); - }, - splashColor: Colors.transparent, - icon: const Icon( - Icons.menu_outlined, - ), - ), - AnimatedSwitcher( - duration: const Duration(milliseconds: 250), - child: widget.centerWidget, - ), - ], - ), - ); - } -} diff --git a/auth/lib/ui/settings/danger_section_widget.dart b/auth/lib/ui/settings/danger_section_widget.dart deleted file mode 100644 index 4f8160c38e..0000000000 --- a/auth/lib/ui/settings/danger_section_widget.dart +++ /dev/null @@ -1,69 +0,0 @@ -import 'package:ente_auth/l10n/l10n.dart'; -import 'package:ente_auth/services/user_service.dart'; -import 'package:ente_auth/theme/ente_theme.dart'; -import 'package:ente_auth/ui/account/delete_account_page.dart'; -import 'package:ente_auth/ui/components/captioned_text_widget.dart'; -import 'package:ente_auth/ui/components/expandable_menu_item_widget.dart'; -import 'package:ente_auth/ui/components/menu_item_widget.dart'; -import 'package:ente_auth/ui/settings/common_settings.dart'; -import 'package:ente_auth/utils/dialog_util.dart'; -import 'package:ente_auth/utils/navigation_util.dart'; -import 'package:flutter/material.dart'; - -class DangerSectionWidget extends StatelessWidget { - const DangerSectionWidget({super.key}); - - @override - Widget build(BuildContext context) { - return ExpandableMenuItemWidget( - title: context.l10n.exit, - selectionOptionsWidget: _getSectionOptions(context), - leadingIcon: Icons.logout_outlined, - ); - } - - Widget _getSectionOptions(BuildContext context) { - return Column( - children: [ - sectionOptionSpacing, - MenuItemWidget( - captionedTextWidget: CaptionedTextWidget( - title: context.l10n.logout, - ), - pressedColor: getEnteColorScheme(context).fillFaint, - trailingIcon: Icons.chevron_right_outlined, - trailingIconIsMuted: true, - onTap: () async { - _onLogoutTapped(context); - }, - ), - sectionOptionSpacing, - MenuItemWidget( - captionedTextWidget: CaptionedTextWidget( - title: context.l10n.deleteAccount, - ), - pressedColor: getEnteColorScheme(context).fillFaint, - trailingIcon: Icons.chevron_right_outlined, - trailingIconIsMuted: true, - onTap: () async { - // ignore: unawaited_futures - routeToPage(context, const DeleteAccountPage()); - }, - ), - sectionOptionSpacing, - ], - ); - } - - void _onLogoutTapped(BuildContext context) { - showChoiceActionSheet( - context, - title: context.l10n.areYouSureYouWantToLogout, - firstButtonLabel: context.l10n.yesLogout, - isCritical: true, - firstButtonOnTap: () async { - await UserService.instance.logout(context); - }, - ); - } -} diff --git a/auth/lib/ui/settings/debug_section_widget.dart b/auth/lib/ui/settings/debug_section_widget.dart deleted file mode 100644 index 03406f7911..0000000000 --- a/auth/lib/ui/settings/debug_section_widget.dart +++ /dev/null @@ -1,94 +0,0 @@ -import 'package:ente_auth/core/configuration.dart'; -import 'package:ente_auth/l10n/l10n.dart'; -import 'package:ente_auth/ui/settings/common_settings.dart'; -import 'package:ente_auth/ui/settings/settings_section_title.dart'; -import 'package:ente_auth/ui/settings/settings_text_item.dart'; -import 'package:ente_crypto_dart/ente_crypto_dart.dart'; -import 'package:expandable/expandable.dart'; -import 'package:flutter/material.dart'; - -class DebugSectionWidget extends StatelessWidget { - const DebugSectionWidget({super.key}); - - @override - Widget build(BuildContext context) { - // This is a debug only section not shown to end users, so these strings are - // not translated. - return ExpandablePanel( - header: const SettingsSectionTitle("Debug"), - collapsed: Container(), - expanded: _getSectionOptions(context), - theme: getExpandableTheme(), - ); - } - - Widget _getSectionOptions(BuildContext context) { - return Column( - children: [ - GestureDetector( - behavior: HitTestBehavior.translucent, - onTap: () async { - _showKeyAttributesDialog(context); - }, - child: const SettingsTextItem( - text: "Key attributes", - icon: Icons.navigate_next, - ), - ), - ], - ); - } - - void _showKeyAttributesDialog(BuildContext context) { - final l10n = context.l10n; - final keyAttributes = Configuration.instance.getKeyAttributes()!; - final AlertDialog alert = AlertDialog( - title: const Text("key attributes"), - content: SingleChildScrollView( - child: Column( - children: [ - const Text( - "Key", - style: TextStyle(fontWeight: FontWeight.bold), - ), - Text(CryptoUtil.bin2base64(Configuration.instance.getKey()!)), - const Padding(padding: EdgeInsets.all(12)), - const Text( - "Encrypted Key", - style: TextStyle(fontWeight: FontWeight.bold), - ), - Text(keyAttributes.encryptedKey), - const Padding(padding: EdgeInsets.all(12)), - const Text( - "Key Decryption Nonce", - style: TextStyle(fontWeight: FontWeight.bold), - ), - Text(keyAttributes.keyDecryptionNonce), - const Padding(padding: EdgeInsets.all(12)), - const Text( - "KEK Salt", - style: TextStyle(fontWeight: FontWeight.bold), - ), - Text(keyAttributes.kekSalt), - const Padding(padding: EdgeInsets.all(12)), - ], - ), - ), - actions: [ - TextButton( - child: Text(l10n.ok), - onPressed: () { - Navigator.of(context, rootNavigator: true).pop('dialog'); - }, - ), - ], - ); - - showDialog( - context: context, - builder: (BuildContext context) { - return alert; - }, - ); - } -} diff --git a/auth/lib/ui/settings/made_with_love_widget.dart b/auth/lib/ui/settings/made_with_love_widget.dart deleted file mode 100644 index a8e12c106b..0000000000 --- a/auth/lib/ui/settings/made_with_love_widget.dart +++ /dev/null @@ -1,34 +0,0 @@ -import 'package:ente_auth/l10n/l10n.dart'; -import 'package:flutter/material.dart'; -import 'package:url_launcher/url_launcher.dart'; - -class MadeWithLoveWidget extends StatelessWidget { - const MadeWithLoveWidget({ - super.key, - }); - - @override - Widget build(BuildContext context) { - final l10n = context.l10n; - return GestureDetector( - onTap: () { - launchUrl(Uri.parse("https://ente.io")); - }, - child: RichText( - text: TextSpan( - text: l10n.madeWithLoveAtPrefix, - style: DefaultTextStyle.of(context).style, - children: const [ - TextSpan( - text: 'ente.io', - style: TextStyle( - fontWeight: FontWeight.bold, - color: Colors.green, - ), - ), - ], - ), - ), - ); - } -} diff --git a/auth/lib/ui/settings/settings_text_item.dart b/auth/lib/ui/settings/settings_text_item.dart deleted file mode 100644 index afaf56432c..0000000000 --- a/auth/lib/ui/settings/settings_text_item.dart +++ /dev/null @@ -1,35 +0,0 @@ - - -import 'dart:io'; - -import 'package:flutter/material.dart'; - -class SettingsTextItem extends StatelessWidget { - final String text; - final IconData icon; - const SettingsTextItem({ - super.key, - required this.text, - required this.icon, - }); - - @override - Widget build(BuildContext context) { - return Column( - children: [ - Padding(padding: EdgeInsets.all(Platform.isIOS ? 4 : 6)), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Align( - alignment: Alignment.centerLeft, - child: Text(text, style: Theme.of(context).textTheme.titleMedium), - ), - Icon(icon), - ], - ), - Padding(padding: EdgeInsets.all(Platform.isIOS ? 4 : 6)), - ], - ); - } -} diff --git a/auth/lib/ui/settings/support_dev_widget.dart b/auth/lib/ui/settings/support_dev_widget.dart deleted file mode 100644 index 849b954153..0000000000 --- a/auth/lib/ui/settings/support_dev_widget.dart +++ /dev/null @@ -1,83 +0,0 @@ -import 'package:dotted_border/dotted_border.dart'; -import 'package:ente_auth/core/configuration.dart'; -import 'package:ente_auth/l10n/l10n.dart'; -import 'package:ente_auth/models/subscription.dart'; -import 'package:ente_auth/services/billing_service.dart'; -import 'package:ente_auth/theme/ente_theme.dart'; -import 'package:flutter/material.dart'; -import 'package:styled_text/styled_text.dart'; -import 'package:url_launcher/url_launcher.dart'; - -class SupportDevWidget extends StatelessWidget { - const SupportDevWidget({ - super.key, - }); - - @override - Widget build(BuildContext context) { - final l10n = context.l10n; - - // fetch - if (Configuration.instance.hasConfiguredAccount()) { - return FutureBuilder( - future: BillingService.instance.getSubscription(), - builder: (context, snapshot) { - if (snapshot.hasData) { - final subscription = snapshot.data; - if (subscription != null && subscription.productID == "free") { - return buildWidget(l10n, context); - } - } - return const SizedBox.shrink(); - }, - ); - } else { - return buildWidget(l10n, context); - } - } - - Widget buildWidget(AppLocalizations l10n, BuildContext context) { - return GestureDetector( - onTap: () { - launchUrl(Uri.parse("https://ente.io")); - }, - child: DottedBorder( - borderType: BorderType.RRect, - radius: const Radius.circular(12), - padding: const EdgeInsets.all(6), - dashPattern: const [3, 3], - color: getEnteColorScheme(context).primaryGreen, - child: ClipRRect( - borderRadius: const BorderRadius.all(Radius.circular(12)), - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 6), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - StyledText( - text: l10n.supportDevs, - style: getEnteTextTheme(context).large, - tags: { - 'bold-green': StyledTextTag( - style: TextStyle( - fontWeight: FontWeight.bold, - color: getEnteColorScheme(context).primaryGreen, - ), - ), - }, - ), - const Padding(padding: EdgeInsets.all(6)), - Text( - l10n.supportDiscount, - style: const TextStyle( - color: Colors.grey, - ), - ), - ], - ), - ), - ), - ), - ); - } -} diff --git a/auth/lib/ui/settings_section_title.dart b/auth/lib/ui/settings_section_title.dart deleted file mode 100644 index 613744d49d..0000000000 --- a/auth/lib/ui/settings_section_title.dart +++ /dev/null @@ -1 +0,0 @@ -// TODO Implement this library. diff --git a/auth/lib/utils/dialog_util.dart b/auth/lib/utils/dialog_util.dart index f747b41f4d..1f64414f8f 100644 --- a/auth/lib/utils/dialog_util.dart +++ b/auth/lib/utils/dialog_util.dart @@ -113,12 +113,12 @@ String parseErrorForUI( if (dioError.response?.data["code"] != null) { errorInfo = "Reason: ${dioError.response!.data["code"]}"; } else { - errorInfo = "Reason: ${dioError.response!.data}"; + errorInfo = "Reason: ${dioError.response!.data.toString()}"; } - } else if (dioError.type == DioExceptionType.unknown) { - errorInfo = "Reason: $dioError.error"; + } else if (dioError.type == DioExceptionType.badCertificate) { + errorInfo = "Reason: ${dioError.error.toString()}"; } else { - errorInfo = "Reason: $dioError.type"; + errorInfo = "Reason: ${dioError.type.toString()}"; } } else { if (kDebugMode) { diff --git a/auth/pubspec.lock b/auth/pubspec.lock index 3d5d6e9cab..9795a507a8 100644 --- a/auth/pubspec.lock +++ b/auth/pubspec.lock @@ -250,10 +250,10 @@ packages: dependency: "direct main" description: name: collection - sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.19.0" confetti: dependency: "direct main" description: @@ -286,6 +286,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.1" + cronet_http: + dependency: transitive + description: + name: cronet_http + sha256: "3af9c4d57bf07ef4b307e77b22be4ad61bea19ee6ff65e62184863f3a09f1415" + url: "https://pub.dev" + source: hosted + version: "1.3.2" cross_file: dependency: transitive description: @@ -310,6 +318,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + cupertino_http: + dependency: transitive + description: + name: cupertino_http + sha256: "6fcf79586ad872ddcd6004d55c8c2aab3cdf0337436e8f99837b1b6c30665d0c" + url: "https://pub.dev" + source: hosted + version: "2.0.2" dart_style: dependency: transitive description: @@ -346,10 +362,10 @@ packages: dependency: "direct main" description: name: dio - sha256: "5598aa796bbf4699afd5c67c0f5f6e2ed542afc956884b9cd58c306966efc260" + sha256: "253a18bbd4851fecba42f7343a1df3a9a4c1d31a2c1b37e221086b4fa8c8dbc9" url: "https://pub.dev" source: hosted - version: "5.7.0" + version: "5.8.0+1" dio_web_adapter: dependency: transitive description: @@ -861,6 +877,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.2" + http_profile: + dependency: transitive + description: + name: http_profile + sha256: "7e679e355b09aaee2ab5010915c932cce3f2d1c11c3b2dc177891687014ffa78" + url: "https://pub.dev" + source: hosted + version: "0.1.0" image: dependency: transitive description: @@ -885,6 +909,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.4" + jni: + dependency: transitive + description: + name: jni + sha256: f377c585ea9c08d48b427dc2e03780af2889d1bb094440da853c6883c1acba4b + url: "https://pub.dev" + source: hosted + version: "0.10.1" js: dependency: transitive description: @@ -1061,6 +1093,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.2" + native_dio_adapter: + dependency: "direct main" + description: + name: native_dio_adapter + sha256: "7420bc9517b2abe09810199a19924617b45690a44ecfb0616ac9babc11875c03" + url: "https://pub.dev" + source: hosted + version: "1.4.0" nested: dependency: transitive description: @@ -1077,6 +1117,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.5.0" + objective_c: + dependency: transitive + description: + name: objective_c + sha256: "62e79ab8c3ed6f6a340ea50dd48d65898f5d70425d404f0d99411f6e56e04584" + url: "https://pub.dev" + source: hosted + version: "4.1.0" otp: dependency: "direct main" description: diff --git a/auth/pubspec.yaml b/auth/pubspec.yaml index 179586a3b2..7816992bd5 100644 --- a/auth/pubspec.yaml +++ b/auth/pubspec.yaml @@ -21,7 +21,7 @@ dependencies: connectivity_plus: ^6.0.5 convert: ^3.1.1 device_info_plus: ^9.1.1 - dio: ^5.4.0 + dio: ^5.8.0+1 dotted_border: ^2.0.0+2 dropdown_button2: ^2.3.9 email_validator: ^3.0.0 @@ -72,6 +72,7 @@ dependencies: logging: ^1.0.1 modal_bottom_sheet: ^3.0.0 move_to_background: ^1.0.2 + native_dio_adapter: ^1.4.0 otp: ^3.1.1 package_info_plus: ^8.0.2 password_strength: ^0.2.0 diff --git a/cli/main.go b/cli/main.go index 990d751b01..15dc6ac5a3 100644 --- a/cli/main.go +++ b/cli/main.go @@ -15,7 +15,7 @@ import ( "strings" ) -var AppVersion = "0.2.2" +var AppVersion = "0.2.3" func main() { cliConfigDir, err := GetCLIConfigDir() @@ -50,18 +50,21 @@ func main() { } } - // Define a set of commands that do not require KeyHolder initialisation. - skipKeyHolderCommands := map[string]struct{}{"version": {}, "docs": {}, "help": {}} + // Define a set of commands that do not require KeyHolder or cli initialisation. + skipInitCommands := map[string]struct{}{"version": {}, "docs": {}, "help": {}} var keyHolder *secrets.KeyHolder - // Only initialise KeyHolder if the command isn't in the skip list. + shouldInit := len(os.Args) > 1 if len(os.Args) > 1 { - if _, skip := skipKeyHolderCommands[os.Args[1]]; !skip { - keyHolder = secrets.NewKeyHolder(secrets.GetOrCreateClISecret()) + if _, skip := skipInitCommands[os.Args[1]]; skip { + shouldInit = false } } + if shouldInit { + keyHolder = secrets.NewKeyHolder(secrets.GetOrCreateClISecret()) + } ctrl := pkg.ClICtrl{ Client: api.NewClient(api.Params{ Debug: viper.GetBool("log.http"), @@ -71,16 +74,10 @@ func main() { KeyHolder: keyHolder, } - err = ctrl.Init() - if err != nil { - panic(err) + if len(os.Args) == 1 { + // If no arguments are passed, show help + os.Args = append(os.Args, "help") } - defer func() { - if err := db.Close(); err != nil { - panic(err) - } - }() - if len(os.Args) == 2 && os.Args[1] == "docs" { log.Println("Generating docs") err = cmd.GenerateDocs() @@ -89,9 +86,16 @@ func main() { } return } - if len(os.Args) == 1 { - // If no arguments are passed, show help - os.Args = append(os.Args, "help") + if shouldInit { + err = ctrl.Init() + if err != nil { + panic(err) + } + defer func() { + if err := db.Close(); err != nil { + panic(err) + } + }() } if os.Args[1] == "version" && viper.GetString("endpoint.api") != constants.EnteApiUrl { log.Printf("Custom endpoint: %s\n", viper.GetString("endpoint.api")) @@ -120,10 +124,10 @@ func initConfig(cliConfigDir string) { func GetCLIConfigDir() (string, error) { var configDir = os.Getenv("ENTE_CLI_CONFIG_DIR") - if configDir == "" { - // for backward compatibility, check for ENTE_CLI_CONFIG_PATH - configDir = os.Getenv("ENTE_CLI_CONFIG_PATH") - } + if configDir == "" { + // for backward compatibility, check for ENTE_CLI_CONFIG_PATH + configDir = os.Getenv("ENTE_CLI_CONFIG_PATH") + } if configDir != "" { // remove trailing slash (for all OS) diff --git a/desktop/CHANGELOG.md b/desktop/CHANGELOG.md index aa647dd8e6..bbefd6c513 100644 --- a/desktop/CHANGELOG.md +++ b/desktop/CHANGELOG.md @@ -1,11 +1,20 @@ # CHANGELOG -## v1.7.9 (Unreleased) +## v1.7.11 (Unreleased) + +- . + +## v1.7.10 + +- Speed up selection for large libraries. +- Support Japanese translations. +- Fix video thumbnail generation on drag and drop. + +## v1.7.9 - Light mode. - Faster and more stable thumbnail generation. - Support `.supplemental-metadata` JSON files in Google Takeout. -- . ## v1.7.8 diff --git a/desktop/build/io.ente.photos.appdata.xml b/desktop/build/io.ente.photos.appdata.xml index 40ff45176d..591f51232f 100644 --- a/desktop/build/io.ente.photos.appdata.xml +++ b/desktop/build/io.ente.photos.appdata.xml @@ -38,8 +38,8 @@ - - https://github.com/ente-io/photos-desktop/releases/tag/v1.7.8 + + https://github.com/ente-io/photos-desktop/releases diff --git a/desktop/package.json b/desktop/package.json index 3859892503..4570c525b0 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -1,6 +1,6 @@ { "name": "ente", - "version": "1.7.9-beta", + "version": "1.7.11-beta", "private": true, "description": "Desktop client for Ente Photos", "repository": "github:ente-io/photos-desktop", diff --git a/desktop/src/main.ts b/desktop/src/main.ts index 8aa32169b1..46aab1bfc1 100644 --- a/desktop/src/main.ts +++ b/desktop/src/main.ts @@ -362,8 +362,18 @@ const createMainWindow = () => { // do it (Step 2) unconditionally (i.e., on macOS too). // // https://www.electronjs.org/docs/latest/tutorial/custom-title-bar#create-a-custom-title-bar + // + // Note that by default on Windows, the color of the WCO title bar + // overlay (three buttons - minimize, maximize, close - on the top + // right) is static, and unlike Linux, doesn't adapt to the theme / + // content. Explicitly choosing a dark background, while it won't work + // always (if the user's theme is light), is better than picking a light + // background since the main image viewer is always dark. titleBarStyle: "hidden", - titleBarOverlay: true, + titleBarOverlay: + process.platform == "win32" + ? { color: "black", symbolColor: "#cdcdcd" } + : true, // The color to show in the window until the web content gets loaded. // https://www.electronjs.org/docs/latest/api/browser-window#setting-the-backgroundcolor-property // diff --git a/desktop/src/main/ipc.ts b/desktop/src/main/ipc.ts index ff98e2ec88..b0a2eccf0c 100644 --- a/desktop/src/main/ipc.ts +++ b/desktop/src/main/ipc.ts @@ -41,6 +41,7 @@ import { fsRm, fsRmdir, fsWriteFile, + fsWriteFileViaBackup, } from "./services/fs"; import { convertToJPEG, generateImageThumbnail } from "./services/image"; import { logout } from "./services/logout"; @@ -154,6 +155,12 @@ export const attachIPCHandlers = () => { fsWriteFile(path, contents), ); + ipcMain.handle( + "fsWriteFileViaBackup", + (_, path: string, contents: string) => + fsWriteFileViaBackup(path, contents), + ); + ipcMain.handle("fsIsDir", (_, dirPath: string) => fsIsDir(dirPath)); ipcMain.handle("fsFindFiles", (_, folderPath: string) => diff --git a/desktop/src/main/services/fs.ts b/desktop/src/main/services/fs.ts index cdbded0beb..6cfe101ebf 100644 --- a/desktop/src/main/services/fs.ts +++ b/desktop/src/main/services/fs.ts @@ -24,6 +24,12 @@ export const fsReadTextFile = async (filePath: string) => export const fsWriteFile = (path: string, contents: string) => fs.writeFile(path, contents, { flush: true }); +export const fsWriteFileViaBackup = async (path: string, contents: string) => { + const backupPath = path + ".backup"; + await fs.writeFile(backupPath, contents, { flush: true }); + return fs.rename(backupPath, path); +}; + export const fsIsDir = async (dirPath: string) => { if (!existsSync(dirPath)) return false; const stat = await fs.stat(dirPath); diff --git a/desktop/src/preload.ts b/desktop/src/preload.ts index cb9d594511..3fab447722 100644 --- a/desktop/src/preload.ts +++ b/desktop/src/preload.ts @@ -178,6 +178,9 @@ const fsReadTextFile = (path: string) => const fsWriteFile = (path: string, contents: string) => ipcRenderer.invoke("fsWriteFile", path, contents); +const fsWriteFileViaBackup = (path: string, contents: string) => + ipcRenderer.invoke("fsWriteFileViaBackup", path, contents); + const fsIsDir = (dirPath: string) => ipcRenderer.invoke("fsIsDir", dirPath); // - Conversion @@ -373,6 +376,7 @@ contextBridge.exposeInMainWorld("electron", { rm: fsRm, readTextFile: fsReadTextFile, writeFile: fsWriteFile, + writeFileViaBackup: fsWriteFileViaBackup, isDir: fsIsDir, findFiles: fsFindFiles, }, diff --git a/docs/docs/.vitepress/sidebar.ts b/docs/docs/.vitepress/sidebar.ts index 361811638a..40123e4795 100644 --- a/docs/docs/.vitepress/sidebar.ts +++ b/docs/docs/.vitepress/sidebar.ts @@ -135,8 +135,12 @@ export const sidebar = [ link: "/photos/faq/hidden-and-archive", }, { - text: "Machine Learning", - link: "/photos/faq/machine-learning", + text: "Face recognition", + link: "/photos/faq/face-recognition", + }, + { + text: "Video streaming", + link: "/photos/faq/video-streaming", }, ], }, @@ -263,6 +267,10 @@ export const sidebar = [ text: "Hosting Ente without Docker", link: "/self-hosting/guides/standalone-ente", }, + { + text: "Ente via Tailscale (Community)", + link: "/self-hosting/guides/Tailscale.md", + }, { text: "Configure CLI for Self Hosted Instance", link: "/self-hosting/guides/selfhost-cli", diff --git a/docs/docs/photos/faq/machine-learning.md b/docs/docs/photos/faq/face-recognition.md similarity index 80% rename from docs/docs/photos/faq/machine-learning.md rename to docs/docs/photos/faq/face-recognition.md index 53bf5ca558..24c9650345 100644 --- a/docs/docs/photos/faq/machine-learning.md +++ b/docs/docs/photos/faq/face-recognition.md @@ -1,10 +1,10 @@ --- -title: Machine Learning FAQ +title: Face recognition description: - Frequently asked questions about several features of Ente's ML suite + Frequently asked questions about Ente's face recognition --- -# Machine Learning +# Face recognition ## Can I merge or de-merge persons recognized by the app? @@ -19,7 +19,7 @@ instead of typing the name again, tap on the already given name that should now be listed. De-merging a certain grouping can be done by going to the person, pressing -`review suggestions` and then the top right `history icon`. Now press on the +`Review suggestions` and then the top right `History icon`. Now press on the `minus icon` beside the group you want to de-merge. ### Desktop @@ -29,6 +29,16 @@ selecting an existing person, and use the "Review suggestions" sheet to de-merge previously merged persons (click the top right history icon on the suggestion sheet to see the previous merges, and if necessary, undo them). +## How can I remove an incorrectly grouped face from a person? + +On our mobile app, open up the person from the People section, click on the +three dots to open up overflow menu, and click on Edit. Now you will be +presented with the list of all photos that were merged to create this person. + +You can click on the merged photos and select the photos you think are +incorrectly grouped (by long-pressing on them) and select "Remove" from the +action bar that pops up to remove any incorrect faces. + ## How do I change the cover for a recognized person? ### Mobile diff --git a/docs/docs/photos/faq/general.md b/docs/docs/photos/faq/general.md index 0e911f82e6..72bbbf6ea2 100644 --- a/docs/docs/photos/faq/general.md +++ b/docs/docs/photos/faq/general.md @@ -112,3 +112,7 @@ https://github.com/ente-io/ente/tree/main/cli#readme. Currently, the Ente mobile app allows you to see a map view of all the albums by clicking on "Your map" under "Locations" on the search screen. + +## How to reset my password if I lost it? + +On the login page, enter your email and click on Forgot Password. Then, enter your recovery key and create a new password. \ No newline at end of file diff --git a/docs/docs/photos/faq/hidden-and-archive.md b/docs/docs/photos/faq/hidden-and-archive.md index bdfb6f1b08..d9106da121 100644 --- a/docs/docs/photos/faq/hidden-and-archive.md +++ b/docs/docs/photos/faq/hidden-and-archive.md @@ -19,9 +19,6 @@ Keep in mind that hidden items will still show up in the "On device" albums within Ente as long as they are present in your native gallery. But once you remove them from your device, they'll stop showing up here. -Hiding is currently only supported in the Ente mobile app, and items hidden from -the mobile app will not be visible in the web and desktop app. - For more details, see [features/hide](/photos/features/hide). ### Archive diff --git a/docs/docs/photos/faq/metadata.md b/docs/docs/photos/faq/metadata.md index 936a3e67dc..03cf10da65 100644 --- a/docs/docs/photos/faq/metadata.md +++ b/docs/docs/photos/faq/metadata.md @@ -1,12 +1,11 @@ --- title: Metadata -description: Handling of metadata, in particular creation dates, in Ente Photos +description: Handling of metadata in Ente Photos --- # Metadata -This document describes Ente's handling of metadata, in particular photo -creation dates. +This document describes Ente's handling of metadata ## Import @@ -46,7 +45,7 @@ importing that folder into Ente**. This way, we will be able to always correctly map, for example, `flower.jpeg` and `flower.json` and show the same date for `flower.jpeg` that you would've seen within Google Photos. -### Screenshots +### File name In case the photo does not have a date in the Exif data (and it is not a Google takeout), for example, for screenshots or Whatsapp forwards, Ente will still try @@ -57,6 +56,28 @@ and deduce the correct date for the file from the name of the file. > This process works great most of the time, but it is inherently based on > heuristics and is not exact. +If we are unable to decipher the creation time from these 3 sources, we will set +the upload time as the photo's creation time. + +## Modifications + +Ente supports modifications to the following metadata: +- File name +- Date & time +- Location + +The first two options are available on both mobile and desktop, while the +ability to update location is only available within our mobile apps. + +### Bulk modifications + +You can bulk-edit creation time of photos from our desktop app, by +multi-selecting items and selecting the "Fix time" option from the action bar. + +You can bulk-edit location coordinates of photos from our mobile app, by +multi-selecting items and selecting the "Edit location" option from the action +bar. + ## Export Ente guarantees that you will get back the _exact_ same original photos and diff --git a/docs/docs/photos/faq/security-and-privacy.md b/docs/docs/photos/faq/security-and-privacy.md index 0ee3f9b024..6f40ab043f 100644 --- a/docs/docs/photos/faq/security-and-privacy.md +++ b/docs/docs/photos/faq/security-and-privacy.md @@ -47,6 +47,9 @@ availability and durability. Our [reliability document](https://ente.io/reliability) provides in-depth information about our storage infrastructure and data replication strategies. +In short, we store 3 copies of your data, across 3 different providers, in 3 +different countries. One of them is in an underground fall-out shelter in Paris. + ### How does Ente's encryption compare to industry standards? Our encryption model goes beyond industry standards. While many services use @@ -55,14 +58,17 @@ in the unlikely event of a server breach, your data remains protected. ## Account Security -### What happens if I forget my password? +### What happens if I forget my password? {#account-recovery} -You can reset your password using your recovery key that was provided to you -during account creation. Please store this key securely, as it's your lifeline -if you forget your password. +If you are logged into Ente on any of your existing devices, you can use that +device to reset your password and use your new password to log in. -If you lose both your password and recovery key, we cannot recover your account -or data due to our end-to-end encrypted architecture. +If you are logged out of Ente on all your devices, you can reset your password +using your recovery key that was provided to you during account creation. + +If you are logged out of Ente on all your devices and you have lost both your +password and recovery key, we cannot recover your account or data due to our +end-to-end encrypted architecture. If you wish to delete your account in such scenarios, please reach out to support@ente.io and we will help you out. diff --git a/docs/docs/photos/faq/subscription.md b/docs/docs/photos/faq/subscription.md index 475fbc524b..aa799127bc 100644 --- a/docs/docs/photos/faq/subscription.md +++ b/docs/docs/photos/faq/subscription.md @@ -157,6 +157,21 @@ The same applies to monthly plans. If you prefer to have this credit refunded to your original payment method, please contact support@ente.io, and we'll assist you. +## How can I update my payment method? + +You can view and manage your payment method by clicking on the green +subscription card within the Ente app, and selecting the "Manage payment method" +button. + +You will be able to see all of your previous invoices, with details regarding +their payment status. In case of failed payments, you will also have an option +to retry those charges. + +## How can I cancel my subscription? + +You can cancel your subscription by clicking on the green subscription card +within the Ente app, and selecting the "Cancel subscription" button. + ## Is there an x GB plan? We have experimented quite a bit and have found it hard to design a single diff --git a/docs/docs/photos/faq/video-streaming.md b/docs/docs/photos/faq/video-streaming.md new file mode 100644 index 0000000000..59a34907f6 --- /dev/null +++ b/docs/docs/photos/faq/video-streaming.md @@ -0,0 +1,63 @@ +--- +title: Video streaming FAQ +description: + Frequently asked questions about Ente's video streaming feature +--- + +# Video streaming + +> [!NOTE] +> +> Video streaming is available in beta on mobile apps starting v0.9.98. + +### How to enable video streaming? + +- Open Settings -> General -> Advanced +- Switch on the toggle for `Video streaming` + +### What happens when I enable video streaming? + +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. + +### How can I view video streams? + +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. + +Processed videos will show a `Play stream` button, clicking which will load and +play the stream. + +Clicking on the `Info` icon within the original video will show details about +the generated stream. + +### What is a stream? + +Stream is an encrypted HLS file with an `.m3u8` playlist that helps play a video +with support for seeking **without** downloading the full file. + +Currently it converts videos into `720p` with `2mbps` bitrate in `H.264` format. +The generated stream is single blob (encrypted with AES) while the playlist file +(`.m3u8`) is another blob (encrypted using XChaCha20). + +We cannot read the contents, duration or the number of chunks within the +generated stream. + +### Will streams consume space in my storage? + +While this feature is in beta, we will not count the storage consumed by your +streams against your storage quota. This may change in the future. If it does, +we will provide an option to opt-in to one of the following: +1. Original videos only +2. Compressed streams only +3. Both + +### Something doesn't seem right, what to do? + +As video streaming is still in beta, some things might not work correctly. +Please create a thread within the `#feedback` channel on +[Discord](https://discord.com/channels/948937918347608085/1121126215995113552) +or reach out to [support@ente.io](mailto:support@ente.io). diff --git a/docs/docs/photos/features/background.md b/docs/docs/photos/features/background.md index e64939fd6a..ffed413f74 100644 --- a/docs/docs/photos/features/background.md +++ b/docs/docs/photos/features/background.md @@ -43,6 +43,10 @@ need to disable this "Optimize battery usage" mode in the system settings for Ente if you wish for Ente to automatically back up your photos in the background. +On Android versions 15 and later, if an app is in private space and the private +space is locked, Android doesn’t allow the app to run any background processes. +As a result, background sync will not work. + ### Desktop In addition to our mobile apps, the background sync also works on our desktop diff --git a/docs/docs/photos/features/machine-learning.md b/docs/docs/photos/features/machine-learning.md index 7e14bafa81..215c1d98a8 100644 --- a/docs/docs/photos/features/machine-learning.md +++ b/docs/docs/photos/features/machine-learning.md @@ -50,5 +50,5 @@ end-to-end encrypted security that we use for syncing your photos. Note that the desktop app does not currently support modifying the face groupings, that is only supported by the mobile app. -For more information on how to use Machine Learning please check out -[the FAQ](../faq/machine-learning). +For more information on how to use Machine Learning for face recognition please +check out [the FAQ](../faq/face-recognition). diff --git a/docs/docs/photos/features/passkeys.md b/docs/docs/photos/features/passkeys.md index 5f7329bed7..fcbfc5b180 100644 --- a/docs/docs/photos/features/passkeys.md +++ b/docs/docs/photos/features/passkeys.md @@ -59,3 +59,6 @@ then select the "Recover two-factor" option in the error message that gets shown. This will take you to a place where you can enter your Ente recovery key and login into your account. Now you can go to the _Passkey_ page to delete the lost passkey and/or add a new one. + +If you have lost access to both your passkey and recovery key, please reach out +to [support@ente.io](mailto:support@ente.io) for help. diff --git a/docs/docs/photos/features/trash.md b/docs/docs/photos/features/trash.md index f324b48e9c..84a685279b 100644 --- a/docs/docs/photos/features/trash.md +++ b/docs/docs/photos/features/trash.md @@ -10,3 +10,9 @@ automatically deleted from Trash after 30 days. You can manaully select photos to permanently delete or completely empty the trash if you wish. Items in trash are included in your used storage calculation. + +## Recovery + +If you have deleted items accidentally, you can recover them from Trash by +selecting these items, and clicking the "Restore" button on the action bar that +pops up. diff --git a/docs/docs/photos/troubleshooting/sharing-logs.md b/docs/docs/photos/troubleshooting/sharing-logs.md index cd9417fac3..4e2eb2d2d3 100644 --- a/docs/docs/photos/troubleshooting/sharing-logs.md +++ b/docs/docs/photos/troubleshooting/sharing-logs.md @@ -20,23 +20,25 @@ the logs just make the process a bit faster and easier. - Select for the option to _Report a Bug_. - Tap on _Report a bug_. +## Desktop and Web + +- Open settings (click on the three horizontal lines button located at the top + left corner of the screen). +- Click on the _Help_ option towards the bottom of settings. +- Click on _View logs_. This will show you the location of the logs on your + system (desktop), or download them from the browser onto your computer (web). +- Go back to settings. +- Click on _Support_. This will open your email client where you can attach the + logs in the email and describe the issue. + ## Desktop -- Click on _Help_ menu at the top of your screen, and select the _View logs_ - option. -- Open settings (click on the three horizontal lines button located at the top - left corner of the screen). -- Click on _Support_. This will open your email client where you can attach the - logs in the email and describe the issue. +On the desktop app, you can also directly view the logs on your computer at the +following locations: -## Web - -- Open settings (click on the three horizontal lines button located at the top - left corner of the screen). -- Click on _Debug Logs_ towards the bottom of settings. -- Click on _Download logs_ -- Click on _Support_. This will open your email client where you can attach the - logs in the email and describe the issue. +- macOS: `~/Library/Logs/ente/ente.log` +- Linux: `~/.config/ente/logs/ente.log` +- Windows: `%USERPROFILE%\AppData\Roaming\ente\logs\ente.log` ## Send email manually diff --git a/docs/docs/self-hosting/guides/Tailscale.md b/docs/docs/self-hosting/guides/Tailscale.md new file mode 100644 index 0000000000..1f0a7593ed --- /dev/null +++ b/docs/docs/self-hosting/guides/Tailscale.md @@ -0,0 +1,286 @@ +--- +title: Self Hosting with Tailscale (Community) +description: Guides for self-hosting Ente Photos and/or Ente Auth with Tailscale +--- +# Guide + +This guide aims to achieve self-hosting Ente photos or Ente-Auth with tailscale (TSDPROXY) without exposing any port OR if someone is behind CGNAT and cannot open any port on the internet but want to run their own selfhosted service for themselves, friends and family only. + +Before getting start keep the following NOTE in mind. + +> [!NOTE] +> If someone is behind double or triple CGNAT; must install tailscale system wide by running `curl -fsSL https://tailscale.com/install.sh | sh` in your linux terminal and `sudo tailscale up` otherwise dns resolver will fail and uploading will not work. This is not necessary for those who are not behing CGNAT. +> This guide also work on docker rootless and normal. + +> [!CAUTION] +Remember that current docker update 28.0.0 has some bug and cannot connect to external network. Make sure to install docker-ce 27.5.0, docker-ce-rootless-extras 27.5.0 and docker-ce-cli 27.5.0. Hopefully docker 28.1.0 will resolve this issue in next week. Refrence links are [Moby Github Repo Issues 49511](https://github.com/moby/moby/issues/49511) and [Moby Github Repo Issues 49519](https://github.com/moby/moby/issues/49519) + +> [!IMPORTANT] +> For Docker rootless, the user must have local permissions for all directories required by the Ente-photos self-hosted server. This can be achieved by running `sudo chown -R 1000:1000 /home/ubuntu/docker/ente`. In the Linux terminal, you can check the UID with `id -u` or simply `id`. The first user typically has UID 1000. +> To allow listening and pinging on any port without root privileges, create a file called `/etc/sysctl.d/99-rootless.conf` with the following content: +> ``` +> net.ipv4.ip_unprivileged_port_start=0 +> net.ipv4.ping_group_range = 0 2147483647 +> ``` +> than run `sudo sysctl --system`. +> Create `~/.config/systemd/user/docker.service.d/override.conf` with the following content: +> ``` +> [Service] +> Environment="DOCKERD_ROOTLESS_ROOTLESSKIT_NET=slirp4netns" +> Environment="DOCKERD_ROOTLESS_ROOTLESSKIT_PORT_DRIVER=slirp4netns" +> ``` +> and Restart the docker daemon +> `systemctl --user restart docker` +> Instead of `--volume /var/run/docker.sock:/var/run/docker.sock` in TSDPROXY compose.yaml, use `--volume $XDG_RUNTIME_DIR/docker.sock:/var/run/docker.sock` + +## GETTING START WITH SETUP ## +First of all create a directory `sudo mkdir -p /home/ubuntu/docker/tsdproxy/config` than `cd docker/tsdproxy` and create compose.yaml file by running `sudo nano compose.yaml`. Populate it with the following: +``` +services: + tsdproxy: + image: almeidapaulopt/tsdproxy + container_name: tsdproxy + restart: unless-stopped + environment: + TZ: Asia/Singapur # change me + volumes: + - $XDG_RUNTIME_DIR/docker.sock:/var/run/docker.sock # for docker rootless otherwise /var/run/docker.sock:/var/run/docker.sock + - tsdproxy_data:/data + - /home/lee/docker/tsdproxy/config:/config + networks: + - proxy + labels: # giving the labels here will create tsdproxy instance in tailscale admin counsle and GUI can be accessable through tailscale if device is connected + - tsdproxy.enable=true + - tsdproxy.name=tsdproxy + - tsdproxy.ephemeral=false # this is optional but useful + +volumes: + tsdproxy_data: + name: tsdproxy_data + +networks: + proxy: + name: proxy +``` +Now login into your tailscale account admin counsle > settings > keys > Generate authkey. Give any description and must select resuable, because the key get purged if not selected after rebooting machine. It is advisable to create **Tags** in **ACLs settings** `tag: tsdproxy` `tag: ente` `tag: minio` as well. This will create a tag nodes with no key expirory. One is safe to reboot restart docker or machine. +> Copy the generated authkey as it is shown only once. +Make tsdproxy.yaml file in `cd docker/tsdproxy/config` by running `sudo nano tsdproxy.yaml` and pupolate it with the following contant: +``` +defaultproxyprovider: default +docker: + local: + host: unix:///var/run/docker.sock + defaultproxyprovider: default +files: {} +tailscale: + providers: + default: + authkey: "" + authkeyfile: "/config/authkey" + controlurl: https://controlplane.tailscale.com + datadir: /data/ +http: + hostname: 0.0.0.0 + port: 8080 +log: + level: info + json: false +proxyaccesslog: true +``` +In the same directory run `sudo nano authkey` and paste the authkey just copied earlier from tailscale admin counsel. +> Here Tailscale (TSDPROXY) setup is complet in all respect. Just run `docker compose up -d`. Check your tailscale amdin counsel and you will see tsdproxy node up and running. Make sure that **HTTPS** is enabled in tailscale DNS settings. +> You can visit the TSDPROXY web GUI by https://tsdproxy.xyz.ts.net. (xyz is change value for everyone) + +## ente Part ## +First make the following necessary files/directories: +``` +sudo mkdir -p /home/ubuntu/docker/ente/custom-logs +sudo mkdir -p /home/ubuntu/docker/ente/data +sudo mkdir -p /home/ubuntu/docker/ente/minio-data +sudo mkdir -p /home/ubuntu/docker/ente/postgres-data +sudo mkdir -p /home/ubuntu/docker/ente/scripts/compose +``` +Than give user permission for each of the above directory. `sudo chown -R 1000:1000 /home/ubuntu/docker/ente/custom-logs` etc etc. Make sure not to skip `/home/ubuntu/docker/tsdproxy/config` + +`cd docker/ente/script/compose` and run `sudo nano credentials.yaml` than populate it with the following: +``` +db: + host: postgres + port: 5432 + name: ente_db + user: pguser # change me + password: pgpass #change me + +s3: + are_local_buckets: true + b2-eu-cen: + key: test # change me + secret: testtest # change me + endpoint: https://minio.xyz.ts.net + region: eu-central-2 + bucket: b2-eu-cen + wasabi-eu-central-2-v3: + key: test # change me + secret: testtest # change me + endpoint: localhost:3200 + region: eu-central-2 + bucket: wasabi-eu-central-2-v3 + compliance: false + scw-eu-fr-v3: + key: test # change me + secret: testtest # change me + endpoint: localhost:3200 + region: eu-central-2 + bucket: scw-eu-fr-v3 +``` + +In the same directory run `sudo nano minio-provision.sh` and populate it with the following contant: +``` +#!/bin/sh + +# Script used to prepare the minio instance that runs as part of the development +# Docker compose cluster. + +while ! mc config host add h0 http://minio:3200 test testtest #(change me) +do + echo "waiting for minio..." + sleep 0.5 +done + +cd /data + +mc mb -p b2-eu-cen +mc mb -p wasabi-eu-central-2-v3 +mc mb -p scw-eu-fr-v3 +``` + +Now `cd docker/ente` and run `sudo nano docker-compose.yaml` and populate it with the following: +``` +services: + museum: + image: ghcr.io/ente-io/server + ports: + - 9080:8080 # 9080 because tsdproxy is running on 8080:8080 + # - 2112:2112 # Prometheus metrics + depends_on: + postgres: + condition: service_healthy + environment: + # Pass-in the config to connect to the DB and MinIO + ENTE_CREDENTIALS_FILE: /credentials.yaml + # ENTE_CLI_SECRETS_PATH: /cli-data/secret.txt + # ENTE_CLI_CONFIG_PATH: /cli-data/ + volumes: + - /home/ubuntu/docker/ente/custom-logs:/var/logs + - /home/ubuntu/docker/ente/museum.yaml:/museum.yaml:ro + - /home/ubuntu/docker/ente/scripts/compose/credentials.yaml:/credentials.yaml:ro + #- /home/ubuntu/docker/ente/cli-data:/cli-data + #- /home/ubuntu/docker/ente/exports/ente-photos:/exports + - /home/ubuntu/docker/ente/data:/data:ro + networks: + - ente + - proxy + labels: + tsdproxy.enable: "true" + tsdproxy.name: "ente" + +# # Resolve "localhost:3200" in the museum container to the minio container. + socat: + image: alpine/socat + network_mode: service:museum + depends_on: + - museum + command: "TCP-LISTEN:3200,fork,reuseaddr TCP:minio:3200" + + postgres: + image: postgres:15 + ports: + - 5432:5432 + environment: + POSTGRES_USER: pguser # change me + POSTGRES_PASSWORD: pgpass # change me + POSTGRES_DB: ente_db + # Wait for postgres to be accept connections before starting museum. + healthcheck: + test: + [ + "CMD", + "pg_isready", + "-q", + "-d", + "ente_db", + "-U", + "pguser" # change it accouding to the POSTGRES_USER: pguser + ] + start_period: 40s + start_interval: 1s + volumes: + - /home/ubuntu/docker/ente/postgres-data:/var/lib/postgresql/data + networks: + - ente + + minio: + image: minio/minio + # Use different ports than the minio defaults to avoid conflicting + # with the ports used by Prometheus. + ports: + - 3200:3200 # API + - 3201:3201 # Console + environment: + MINIO_ROOT_USER: test # change me + MINIO_ROOT_PASSWORD: testtest # change me + MINIO_SERVER_URL: https://minio.xyz.ts.net + command: server /data --address ":3200" --console-address ":3201" + volumes: + - /home/ubuntu/docker/ente/minio-data:/data + networks: + - ente + - proxy + labels: + tsdproxy.enable: "true" + tsdproxy.name: "minio" + + minio-provision: + image: minio/mc + depends_on: + - minio + volumes: + - /home/ubuntu/docker/ente/scripts/compose/minio-provision.sh:/provision.sh:ro + - /home/ubuntu/docker/ente/minio-data:/data + networks: + - ente + entrypoint: sh /provision.sh + + +networks: + ente: + name: ente + + proxy: + external: true +``` + +> Thats it. Run `docker compose up -d`. Wait till every container become healthy. Open web browser. Make sure tailscale is installed on the machine. Visit https://ente.xyz.ts.net/ping. It will pong. All good if you see it. First time it will take minute or two to get SSL cert. Downnload Desktop or mobile app. Tap 7 time on the screen, which will prompt developer mode. Add https://ente.xyz.ts.net. Add new user. When asked for OTP. Just go to linux terminal and run `docker logs ente-museum-1`. Search for userauth. Feed the six digit and Done. + +> For getting 100TB (limitless) storage. Just Install ente-cli for windows. Extract it and add folder. Name it **export**. Add config.yaml file along and populate it with the following: +``` +endpoint: + api: "https://ente.xyz.ts.net" + accounts: "http://localhost:3001" + +log: false +``` +Right-Click in the directory where you have extracted ente-cli. Select `open in terminal`. Run +``` +.\ente.exe account bob # change bob to yours +``` +Hit Enter twice. +For export directory, just write export. As already created **export** folder earlier. +**Write email. The one which is already used befor when creating ente account in ente desktop app.** +Type the same Password used before for the account.Run +``` +.\ente.ext account list +``` +This will list all account details. Copy Acount ID. +> Navigate to museum.yaml file. `cd docker/ente`. Run `sudo nano museum.yaml` and add the account ID under Admins. Delete any previous entries. +Restart ente-museum-1 container from linux terminal. Run `docker restart ente-museum-1`. All well, now you will have 100TB storage. Repeat if for any other accounts you want to give unlimited storage access. diff --git a/docs/docs/self-hosting/guides/configuring-s3.md b/docs/docs/self-hosting/guides/configuring-s3.md index 8da933d686..dcc209e8b1 100644 --- a/docs/docs/self-hosting/guides/configuring-s3.md +++ b/docs/docs/self-hosting/guides/configuring-s3.md @@ -54,20 +54,30 @@ The same principle applies if you're deploying to your custom domain. ## Replication -If you're wondering why there are 3 buckets on MinIO UI - that's because our -production instance uses these to perform [replication](https://ente.io/reliability/). - -In a self hosted Ente Instance replication is turned off by default. -When replication is turned off, only the first bucket (`b2-eu-cen`) is used, -and you can ignore the other two. Use the `s3.hot_storage.primary` option -if you'd like to set one of the other predefined buckets as the primary bucket. - > [!IMPORTANT] > As of now, Replication works only if all the 3 storage type > needs are fulfilled (1 Hot, 1 Cold and 1 Glacier Storage). > > [Reference](https://github.com/ente-io/ente/discussions/3167#discussioncomment-10585970) +If you're wondering why there are 3 buckets on MinIO UI - that's because our +production instance uses these to perform [replication](https://ente.io/reliability/). + +If you're also wondering about why the bucket names are specifically what they are, +it's because that is exactly what we are using on our production instance. +We use `b2-eu-cen` as hot, `wasabi-eu-central-2-v3` as cold (also the secondary hot) +and `scw-eu-fr-v3` as glacier storage. As of now, all of this is hardcoded. +Hence, the same hardcoded configuration is applied when you self host Ente. + +In a Self hosted Ente Instance replication is turned off by default. +When replication is turned off, only the first bucket (`b2-eu-cen`) is used, +and the other two are ignored. Only the names here are specifically fixed, but +in the configuration body you can put any other keys. It does not have any relation +with `b2`, `wasabi` or even `scaleway`. + +Use the `s3.hot_storage.primary` option if you'd like to set one of the other +predefined buckets as the primary bucket. + ## SSL Configuration > [!NOTE] diff --git a/infra/services/grafana/README.md b/infra/services/grafana/README.md new file mode 100644 index 0000000000..5c9ef8ef96 --- /dev/null +++ b/infra/services/grafana/README.md @@ -0,0 +1,37 @@ +# Grafana + +Grafana data is stored in a persistent Docker volume named `grafana-storage`. To +create a backup of this, use + +```sh +docker run --rm \ + --mount source=grafana-storage,target=/g \ + -v $(pwd):/backup \ + busybox \ + tar -cvzf /backup/grafana-storage.backup.tar.gz /g +``` + +## Installation + +Restore the volume: + +```sh +docker run --rm \ + --mount source=grafana-storage,target=/g \ + -v $(pwd):/backup \ + busybox \ + tar -xvzf /backup/grafana-storage.backup.tar.gz -C / +``` + +Add the Grafana nginx config + +```sh +sudo mv grafana.nginx.conf /root/nginx/conf.d +``` + +and reload the nginx service before starting Grafana for the first time. + +```sh +sudo systemctl reload nginx +sudo systemctl start grafana +``` diff --git a/infra/services/grafana/grafana.nginx.conf b/infra/services/grafana/grafana.nginx.conf new file mode 100644 index 0000000000..046f42153c --- /dev/null +++ b/infra/services/grafana/grafana.nginx.conf @@ -0,0 +1,24 @@ +# Needed for web sockets +map $http_upgrade $connection_upgrade { + default upgrade; + '' close; +} + +server { + listen 443 ssl http2; + listen [::]:443 ssl http2; + ssl_certificate /etc/ssl/certs/cert.pem; + ssl_certificate_key /etc/ssl/private/key.pem; + + server_name grafana.ente.io; + + location / { + proxy_pass http://host.docker.internal:3001; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + } +} diff --git a/infra/services/grafana/grafana.service b/infra/services/grafana/grafana.service new file mode 100644 index 0000000000..22003a5bc5 --- /dev/null +++ b/infra/services/grafana/grafana.service @@ -0,0 +1,14 @@ +[Unit] +Documentation=https://grafana.com/docs/grafana/latest/setup-grafana/configure-docker/ +Requires=docker.service +After=docker.service + +[Service] +ExecStartPre=docker pull grafana/grafana-oss +ExecStartPre=-docker stop grafana +ExecStartPre=-docker rm grafana +ExecStart=docker run --name grafana \ + -p 3001:3001 \ + -v grafana-storage:/var/lib/grafana \ + -e "GF_SERVER_HTTP_PORT=3001" \ + grafana/grafana-oss diff --git a/mobile/.metadata b/mobile/.metadata index 01d2dcb9ad..98bea013b5 100644 --- a/mobile/.metadata +++ b/mobile/.metadata @@ -4,7 +4,7 @@ # This file should be version controlled and should not be manually edited. version: - revision: 0b8abb4724aa590dd0f429683339b1e045a1594d - channel: stable + revision: "2663184aa79047d0a33a14a3b607954f8fdd8730" + channel: "stable" project_type: app diff --git a/mobile/android/.gitignore b/mobile/android/.gitignore index bc2100d8f7..55afd919c6 100644 --- a/mobile/android/.gitignore +++ b/mobile/android/.gitignore @@ -5,3 +5,9 @@ gradle-wrapper.jar /gradlew.bat /local.properties GeneratedPluginRegistrant.java + +# Remember to never publicly share your keystore. +# See https://flutter.dev/to/reference-keystore +key.properties +**/*.keystore +**/*.jks diff --git a/mobile/android/app/build.gradle b/mobile/android/app/build.gradle index ad09d00b94..98d792b9f3 100644 --- a/mobile/android/app/build.gradle +++ b/mobile/android/app/build.gradle @@ -28,13 +28,23 @@ if (keystorePropertiesFile.exists()) { } android { - compileSdkVersion 34 - ndkVersion "26.0.10792818" + namespace = "io.ente.photos" + compileSdk = 35 + ndkVersion = flutter.ndkVersion + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_1_8 + } sourceSets { main.java.srcDirs += 'src/main/kotlin' } - + lintOptions { disable 'InvalidPackage' warningsAsErrors false @@ -42,11 +52,11 @@ android { } defaultConfig { - applicationId "io.ente.photos" - minSdkVersion 26 - targetSdkVersion 34 - versionCode flutterVersionCode.toInteger() - versionName flutterVersionName + applicationId = "io.ente.photos" + minSdk = 26 + targetSdk = flutter.targetSdkVersion + versionCode = flutter.versionCode + versionName = flutter.versionName testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" multiDexEnabled true consumerProguardFiles 'proguard-rules.pro' @@ -115,14 +125,14 @@ rootProject.allprojects { if (details.requested.group == 'com.github.bumptech.glide' && details.requested.name.contains('glide')) { details.useVersion "4.15.1" - } } + } } } } flutter { - source '../..' + source = "../.." } dependencies { diff --git a/mobile/android/app/proguard-rules.pro b/mobile/android/app/proguard-rules.pro index f35c179f30..797953c7f9 100644 --- a/mobile/android/app/proguard-rules.pro +++ b/mobile/android/app/proguard-rules.pro @@ -2,3 +2,6 @@ # To ensure that stack traces is unambiguous # https://developer.android.com/studio/build/shrink-code#decode-stack-trace -keepattributes LineNumberTable,SourceFile + +-keep class org.chromium.net.** { *; } +-keep class org.xmlpull.v1.** { *; } \ No newline at end of file diff --git a/mobile/android/build.gradle b/mobile/android/build.gradle index b8ba53408f..9a142ee48d 100644 --- a/mobile/android/build.gradle +++ b/mobile/android/build.gradle @@ -12,18 +12,15 @@ allprojects { // url "${project(':background_fetch').projectDir}/libs" // } } - ext { - compileSdkVersion = 34 - targetSdkVersion = 34 - appCompatVersion = "1.7.0" - } } -rootProject.buildDir = '../build' +rootProject.buildDir = "../build" subprojects { project.buildDir = "${rootProject.buildDir}/${project.name}" - project.evaluationDependsOn(':app') +} +subprojects { + project.evaluationDependsOn(":app") } tasks.register("clean", Delete) { diff --git a/mobile/android/gradle.properties b/mobile/android/gradle.properties index ed508580fa..fce68493b1 100644 --- a/mobile/android/gradle.properties +++ b/mobile/android/gradle.properties @@ -1,3 +1,3 @@ org.gradle.jvmargs=-Xmx4608m android.useAndroidX=true -android.enableJetifier=true +android.enableJetifier=true \ No newline at end of file diff --git a/mobile/android/gradle/wrapper/gradle-wrapper.properties b/mobile/android/gradle/wrapper/gradle-wrapper.properties index a7e3608ded..7bb2df6ba6 100644 --- a/mobile/android/gradle/wrapper/gradle-wrapper.properties +++ b/mobile/android/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,5 @@ -#Fri Jun 23 08:50:38 CEST 2017 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-all.zip -distributionSha256Sum=a8da5b02437a60819cad23e10fc7e9cf32bcb57029d9cb277e26eeff76ce014b +distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-all.zip diff --git a/mobile/android/settings.gradle b/mobile/android/settings.gradle index 0fff0ecaf2..b9e43bd376 100644 --- a/mobile/android/settings.gradle +++ b/mobile/android/settings.gradle @@ -18,8 +18,8 @@ pluginManagement { plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" - id "com.android.application" version "7.1.2" apply false - id "org.jetbrains.kotlin.android" version "1.8.21" apply false + id "com.android.application" version "8.1.0" apply false + id "org.jetbrains.kotlin.android" version "1.8.22" apply false } -include ":app" \ No newline at end of file +include ":app" diff --git a/mobile/fastlane/metadata/android/tr/full_description.txt b/mobile/fastlane/metadata/android/tr/full_description.txt index 2362583f60..a978db8478 100644 --- a/mobile/fastlane/metadata/android/tr/full_description.txt +++ b/mobile/fastlane/metadata/android/tr/full_description.txt @@ -1,6 +1,6 @@ -ente, fotoğraflarınızı ve videolarınızı yedeklemek ve paylaşmak için basit bir uygulamadır. +ente, fotoğraflarınızı ve videolarınızı yedekleyip paylaşmanızı sağlayan kullanimi kolay bir uygulamadır. -Google Fotoğraflar'a gizlilik dostu bir alternatif arıyorsanız doğru yere geldiniz. Ente ile uçtan uca şifrelenmiş olarak (e2ee) saklanırlar. Bu, onları yalnızca sizin görebileceğiniz anlamına gelir. +Anılarınızı saklamak için gizlilik dostu bir alternatif arıyorsanız, doğru yere geldiniz. Ente ile uçtan uca şifrelenmiş olarak (e2ee) saklanırlar. Bu, onları yalnızca sizin görebileceğiniz anlamına gelir. Android, iOS, web ve masaüstünde açık kaynaklı uygulamalarımız var ve fotoğraflarınız bunların tümü arasında uçtan uca şifrelenmiş (e2ee) şekilde sorunsuz bir şekilde senkronize edilecek. diff --git a/mobile/fastlane/metadata/ios/tr/name.txt b/mobile/fastlane/metadata/ios/tr/name.txt index 04e5a9c011..06a3a6ebe6 100644 --- a/mobile/fastlane/metadata/ios/tr/name.txt +++ b/mobile/fastlane/metadata/ios/tr/name.txt @@ -1 +1 @@ -ente fotoğraf uygulaması +Ente Fotoğraflar diff --git a/mobile/fastlane/metadata/ios/tr/subtitle.txt b/mobile/fastlane/metadata/ios/tr/subtitle.txt index cbc438b5f0..e0b41db095 100644 --- a/mobile/fastlane/metadata/ios/tr/subtitle.txt +++ b/mobile/fastlane/metadata/ios/tr/subtitle.txt @@ -1 +1 @@ -Şifrelenmiş depolama sistemi +Ente - şifrelenmiş depolama sistemi diff --git a/mobile/ios/Podfile.lock b/mobile/ios/Podfile.lock index 4a76000080..f7c69935af 100644 --- a/mobile/ios/Podfile.lock +++ b/mobile/ios/Podfile.lock @@ -5,6 +5,8 @@ PODS: - Flutter - connectivity_plus (0.0.1): - Flutter + - cupertino_http (0.0.1): + - Flutter - FlutterMacOS - dart_ui_isolate (0.0.1): - Flutter @@ -19,31 +21,31 @@ PODS: - Flutter - file_saver (0.0.1): - Flutter - - Firebase/CoreOnly (11.2.0): - - FirebaseCore (= 11.2.0) - - Firebase/Messaging (11.2.0): + - Firebase/CoreOnly (11.8.0): + - FirebaseCore (~> 11.8.0) + - Firebase/Messaging (11.8.0): - Firebase/CoreOnly - - FirebaseMessaging (~> 11.2.0) - - firebase_core (3.6.0): - - Firebase/CoreOnly (= 11.2.0) + - FirebaseMessaging (~> 11.8.0) + - firebase_core (3.12.0): + - Firebase/CoreOnly (= 11.8.0) - Flutter - - firebase_messaging (15.1.3): - - Firebase/Messaging (= 11.2.0) + - firebase_messaging (15.2.3): + - Firebase/Messaging (= 11.8.0) - firebase_core - Flutter - - FirebaseCore (11.2.0): - - FirebaseCoreInternal (~> 11.0) + - FirebaseCore (11.8.1): + - FirebaseCoreInternal (~> 11.8.0) - GoogleUtilities/Environment (~> 8.0) - GoogleUtilities/Logger (~> 8.0) - - FirebaseCoreInternal (11.6.0): + - FirebaseCoreInternal (11.8.0): - "GoogleUtilities/NSData+zlib (~> 8.0)" - - FirebaseInstallations (11.4.0): - - FirebaseCore (~> 11.0) + - FirebaseInstallations (11.8.0): + - FirebaseCore (~> 11.8.0) - GoogleUtilities/Environment (~> 8.0) - GoogleUtilities/UserDefaults (~> 8.0) - PromisesObjC (~> 2.4) - - FirebaseMessaging (11.2.0): - - FirebaseCore (~> 11.0) + - FirebaseMessaging (11.8.0): + - FirebaseCore (~> 11.8.0) - FirebaseInstallations (~> 11.0) - GoogleDataTransport (~> 10.0) - GoogleUtilities/AppDelegateSwizzler (~> 8.0) @@ -54,7 +56,7 @@ PODS: - Flutter (1.0.0) - flutter_email_sender (0.0.1): - Flutter - - flutter_image_compress (1.0.0): + - flutter_image_compress_common (1.0.0): - Flutter - Mantle - SDWebImage @@ -68,7 +70,7 @@ PODS: - OrderedSet (~> 6.0.3) - flutter_local_notifications (0.0.1): - Flutter - - flutter_native_splash (0.0.1): + - flutter_native_splash (2.4.3): - Flutter - flutter_secure_storage (6.0.0): - Flutter @@ -76,7 +78,6 @@ PODS: - Flutter - fluttertoast (0.0.2): - Flutter - - Toast - GoogleDataTransport (10.1.0): - nanopb (~> 3.30910.0) - PromisesObjC (~> 2.4) @@ -115,17 +116,17 @@ PODS: - FlutterMacOS - integration_test (0.0.1): - Flutter - - libwebp (1.3.2): - - libwebp/demux (= 1.3.2) - - libwebp/mux (= 1.3.2) - - libwebp/sharpyuv (= 1.3.2) - - libwebp/webp (= 1.3.2) - - libwebp/demux (1.3.2): + - libwebp (1.5.0): + - libwebp/demux (= 1.5.0) + - libwebp/mux (= 1.5.0) + - libwebp/sharpyuv (= 1.5.0) + - libwebp/webp (= 1.5.0) + - libwebp/demux (1.5.0): - libwebp/webp - - libwebp/mux (1.3.2): + - libwebp/mux (1.5.0): - libwebp/demux - - libwebp/sharpyuv (1.3.2) - - libwebp/webp (1.3.2): + - libwebp/sharpyuv (1.5.0) + - libwebp/webp (1.5.0): - libwebp/sharpyuv - local_auth_darwin (0.0.1): - Flutter @@ -141,8 +142,6 @@ PODS: - Flutter - media_kit_libs_ios_video (1.0.4): - Flutter - - media_kit_native_event_loop (1.0.0): - - Flutter - media_kit_video (0.0.1): - Flutter - motion_sensors (0.0.1): @@ -158,6 +157,8 @@ PODS: - nanopb/encode (3.30910.0) - native_video_player (1.0.0): - Flutter + - objective_c (0.0.1): + - Flutter - onnxruntime (0.0.1): - Flutter - onnxruntime-objc (= 1.18.0) @@ -182,21 +183,19 @@ PODS: - privacy_screen (0.0.1): - Flutter - PromisesObjC (2.4.0) - - receive_sharing_intent (1.8.0): + - receive_sharing_intent (1.8.1): - Flutter - - screen_brightness_ios (0.1.0): - - Flutter - - SDWebImage (5.20.0): - - SDWebImage/Core (= 5.20.0) - - SDWebImage/Core (5.20.0) + - SDWebImage (5.21.0): + - SDWebImage/Core (= 5.21.0) + - SDWebImage/Core (5.21.0) - SDWebImageWebPCoder (0.14.6): - libwebp (~> 1.0) - SDWebImage/Core (~> 5.17) - - Sentry/HybridSDK (8.36.0) - - sentry_flutter (8.9.0): + - Sentry/HybridSDK (8.44.0) + - sentry_flutter (8.13.2): - Flutter - FlutterMacOS - - Sentry/HybridSDK (= 8.36.0) + - Sentry/HybridSDK (= 8.44.0) - share_plus (0.0.1): - Flutter - shared_preferences_foundation (0.0.1): @@ -205,28 +204,28 @@ PODS: - sqflite_darwin (0.0.4): - Flutter - FlutterMacOS - - "sqlite3 (3.46.1+1)": - - "sqlite3/common (= 3.46.1+1)" - - "sqlite3/common (3.46.1+1)" - - "sqlite3/dbstatvtab (3.46.1+1)": + - sqlite3 (3.49.1): + - sqlite3/common (= 3.49.1) + - sqlite3/common (3.49.1) + - sqlite3/dbstatvtab (3.49.1): - sqlite3/common - - "sqlite3/fts5 (3.46.1+1)": + - sqlite3/fts5 (3.49.1): - sqlite3/common - - "sqlite3/perf-threadsafe (3.46.1+1)": + - sqlite3/perf-threadsafe (3.49.1): - sqlite3/common - - "sqlite3/rtree (3.46.1+1)": + - sqlite3/rtree (3.49.1): - sqlite3/common - sqlite3_flutter_libs (0.0.1): - Flutter - - "sqlite3 (~> 3.46.0+1)" + - FlutterMacOS + - sqlite3 (~> 3.49.0) - sqlite3/dbstatvtab - sqlite3/fts5 - sqlite3/perf-threadsafe - sqlite3/rtree - system_info_plus (0.0.1): - Flutter - - Toast (4.1.1) - - ua_client_hints (1.4.0): + - ua_client_hints (1.4.1): - Flutter - uni_links (0.0.1): - Flutter @@ -246,7 +245,8 @@ PODS: DEPENDENCIES: - background_fetch (from `.symlinks/plugins/background_fetch/ios`) - battery_info (from `.symlinks/plugins/battery_info/ios`) - - connectivity_plus (from `.symlinks/plugins/connectivity_plus/darwin`) + - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`) + - cupertino_http (from `.symlinks/plugins/cupertino_http/darwin`) - dart_ui_isolate (from `.symlinks/plugins/dart_ui_isolate/ios`) - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) - ffmpeg_kit_flutter_full_gpl (from `.symlinks/plugins/ffmpeg_kit_flutter_full_gpl/ios`) @@ -255,7 +255,7 @@ DEPENDENCIES: - firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`) - Flutter (from `Flutter`) - flutter_email_sender (from `.symlinks/plugins/flutter_email_sender/ios`) - - flutter_image_compress (from `.symlinks/plugins/flutter_image_compress/ios`) + - flutter_image_compress_common (from `.symlinks/plugins/flutter_image_compress_common/ios`) - flutter_inappwebview_ios (from `.symlinks/plugins/flutter_inappwebview_ios/ios`) - flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`) - flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`) @@ -272,12 +272,12 @@ DEPENDENCIES: - maps_launcher (from `.symlinks/plugins/maps_launcher/ios`) - media_extension (from `.symlinks/plugins/media_extension/ios`) - media_kit_libs_ios_video (from `.symlinks/plugins/media_kit_libs_ios_video/ios`) - - media_kit_native_event_loop (from `.symlinks/plugins/media_kit_native_event_loop/ios`) - media_kit_video (from `.symlinks/plugins/media_kit_video/ios`) - motion_sensors (from `.symlinks/plugins/motion_sensors/ios`) - motionphoto (from `.symlinks/plugins/motionphoto/ios`) - move_to_background (from `.symlinks/plugins/move_to_background/ios`) - native_video_player (from `.symlinks/plugins/native_video_player/ios`) + - objective_c (from `.symlinks/plugins/objective_c/ios`) - onnxruntime (from `.symlinks/plugins/onnxruntime/ios`) - open_mail_app (from `.symlinks/plugins/open_mail_app/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) @@ -286,12 +286,11 @@ DEPENDENCIES: - photo_manager (from `.symlinks/plugins/photo_manager/ios`) - privacy_screen (from `.symlinks/plugins/privacy_screen/ios`) - receive_sharing_intent (from `.symlinks/plugins/receive_sharing_intent/ios`) - - screen_brightness_ios (from `.symlinks/plugins/screen_brightness_ios/ios`) - sentry_flutter (from `.symlinks/plugins/sentry_flutter/ios`) - share_plus (from `.symlinks/plugins/share_plus/ios`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`) - - sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/ios`) + - sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/darwin`) - system_info_plus (from `.symlinks/plugins/system_info_plus/ios`) - ua_client_hints (from `.symlinks/plugins/ua_client_hints/ios`) - uni_links (from `.symlinks/plugins/uni_links/ios`) @@ -322,7 +321,6 @@ SPEC REPOS: - SDWebImageWebPCoder - Sentry - sqlite3 - - Toast EXTERNAL SOURCES: background_fetch: @@ -330,7 +328,9 @@ EXTERNAL SOURCES: battery_info: :path: ".symlinks/plugins/battery_info/ios" connectivity_plus: - :path: ".symlinks/plugins/connectivity_plus/darwin" + :path: ".symlinks/plugins/connectivity_plus/ios" + cupertino_http: + :path: ".symlinks/plugins/cupertino_http/darwin" dart_ui_isolate: :path: ".symlinks/plugins/dart_ui_isolate/ios" device_info_plus: @@ -347,8 +347,8 @@ EXTERNAL SOURCES: :path: Flutter flutter_email_sender: :path: ".symlinks/plugins/flutter_email_sender/ios" - flutter_image_compress: - :path: ".symlinks/plugins/flutter_image_compress/ios" + flutter_image_compress_common: + :path: ".symlinks/plugins/flutter_image_compress_common/ios" flutter_inappwebview_ios: :path: ".symlinks/plugins/flutter_inappwebview_ios/ios" flutter_local_notifications: @@ -381,8 +381,6 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/media_extension/ios" media_kit_libs_ios_video: :path: ".symlinks/plugins/media_kit_libs_ios_video/ios" - media_kit_native_event_loop: - :path: ".symlinks/plugins/media_kit_native_event_loop/ios" media_kit_video: :path: ".symlinks/plugins/media_kit_video/ios" motion_sensors: @@ -393,6 +391,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/move_to_background/ios" native_video_player: :path: ".symlinks/plugins/native_video_player/ios" + objective_c: + :path: ".symlinks/plugins/objective_c/ios" onnxruntime: :path: ".symlinks/plugins/onnxruntime/ios" open_mail_app: @@ -409,8 +409,6 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/privacy_screen/ios" receive_sharing_intent: :path: ".symlinks/plugins/receive_sharing_intent/ios" - screen_brightness_ios: - :path: ".symlinks/plugins/screen_brightness_ios/ios" sentry_flutter: :path: ".symlinks/plugins/sentry_flutter/ios" share_plus: @@ -420,7 +418,7 @@ EXTERNAL SOURCES: sqflite_darwin: :path: ".symlinks/plugins/sqflite_darwin/darwin" sqlite3_flutter_libs: - :path: ".symlinks/plugins/sqlite3_flutter_libs/ios" + :path: ".symlinks/plugins/sqlite3_flutter_libs/darwin" system_info_plus: :path: ".symlinks/plugins/system_info_plus/ios" ua_client_hints: @@ -439,82 +437,81 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/wakelock_plus/ios" SPEC CHECKSUMS: - background_fetch: 39f11371c0dce04b001c4bfd5e782bcccb0a85e2 - battery_info: 09f5c9ee65394f2291c8c6227bedff345b8a730c - connectivity_plus: ddd7f30999e1faaef5967c23d5b6d503d10434db - dart_ui_isolate: d5bcda83ca4b04f129d70eb90110b7a567aece14 - device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6 + background_fetch: 94b36ee293e82972852dba8ede1fbcd3bd3d9d57 + battery_info: 83f3aae7be2fccefab1d2bf06b8aa96f11c8bcdd + connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd + cupertino_http: 94ac07f5ff090b8effa6c5e2c47871d48ab7c86c + dart_ui_isolate: 46f6714abe6891313267153ef6f9748d8ecfcab1 + device_info_plus: 335f3ce08d2e174b9fdc3db3db0f4e3b1f66bd89 ffmpeg-kit-ios-full-gpl: 80adc341962e55ef709e36baa8ed9a70cf4ea62b - ffmpeg_kit_flutter_full_gpl: 8d15c14c0c3aba616fac04fe44b3d27d02e3c330 - file_saver: 503e386464dbe118f630e17b4c2e1190fa0cf808 - Firebase: 98e6bf5278170668a7983e12971a66b2cd57fc8c - firebase_core: 2bedc3136ec7c7b8561c6123ed0239387b53f2af - firebase_messaging: 15d114e1a41fc31e4fbabcd48d765a19eec94a38 - FirebaseCore: a282032ae9295c795714ded2ec9c522fc237f8da - FirebaseCoreInternal: d98ab91e2d80a56d7b246856a8885443b302c0c2 - FirebaseInstallations: 6ef4a1c7eb2a61ee1f74727d7f6ce2e72acf1414 - FirebaseMessaging: c9ec7b90c399c7a6100297e9d16f8a27fc7f7152 + ffmpeg_kit_flutter_full_gpl: ce18b888487c05c46ed252cd2e7956812f2e3bd1 + file_saver: 6cdbcddd690cb02b0c1a0c225b37cd805c2bf8b6 + Firebase: d80354ed7f6df5f9aca55e9eb47cc4b634735eaf + firebase_core: 6cbed78b4f298ed103a9fd034e6dbc846320480f + firebase_messaging: 5e0adf2eb18b0ee59aa0c109314c091a0497ecac + FirebaseCore: 99fe0c4b44a39f37d99e6404e02009d2db5d718d + FirebaseCoreInternal: df24ce5af28864660ecbd13596fc8dd3a8c34629 + FirebaseInstallations: 6c963bd2a86aca0481eef4f48f5a4df783ae5917 + FirebaseMessaging: 487b634ccdf6f7b7ff180fdcb2a9935490f764e8 Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 - flutter_email_sender: 02d7443217d8c41483223627972bfdc09f74276b - flutter_image_compress: 5a5e9aee05b6553048b8df1c3bc456d0afaac433 - flutter_inappwebview_ios: 6f63631e2c62a7c350263b13fa5427aedefe81d4 - flutter_local_notifications: 4cde75091f6327eb8517fa068a0a5950212d2086 - flutter_native_splash: edf599c81f74d093a4daf8e17bd7a018854bc778 - flutter_secure_storage: 23fc622d89d073675f2eaa109381aefbcf5a49be - flutter_sodium: c84426b4de738514b5b66cfdeb8a06634e72fe0b - fluttertoast: e9a18c7be5413da53898f660530c56f35edfba9c + flutter_email_sender: aa1e9772696691d02cd91fea829856c11efb8e58 + flutter_image_compress_common: 1697a328fd72bfb335507c6bca1a65fa5ad87df1 + flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99 + flutter_local_notifications: ad39620c743ea4c15127860f4b5641649a988100 + flutter_native_splash: 6cad9122ea0fad137d23137dd14b937f3e90b145 + flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13 + flutter_sodium: 7e4621538491834eba53bd524547854bcbbd6987 + fluttertoast: 2c67e14dce98bbdb200df9e1acf610d7a6264ea1 GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7 GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d - home_widget: 0434835a4c9a75704264feff6be17ea40e0f0d57 - image_editor_common: d6f6644ae4a6de80481e89fe6d0a8c49e30b4b43 - image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1 - in_app_purchase_storekit: 8c3b0b3eb1b0f04efbff401c3de6266d4258d433 - integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573 - libwebp: 1786c9f4ff8a279e4dac1e8f385004d5fc253009 - local_auth_darwin: 66e40372f1c29f383a314c738c7446e2f7fdadc3 - local_auth_ios: 5046a18c018dd973247a0564496c8898dbb5adf9 + home_widget: f169fc41fd807b4d46ab6615dc44d62adbf9f64f + image_editor_common: 3de87e7c4804f4ae24c8f8a998362b98c105cac1 + image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a + in_app_purchase_storekit: d1a48cb0f8b29dbf5f85f782f5dd79b21b90a5e6 + integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e + libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8 + local_auth_darwin: 553ce4f9b16d3fdfeafce9cf042e7c9f77c1c391 + local_auth_ios: f7a1841beef3151d140a967c2e46f30637cdf451 Mantle: c5aa8794a29a022dfbbfc9799af95f477a69b62d - maps_launcher: 2e5b6a2d664ec6c27f82ffa81b74228d770ab203 - media_extension: 6d30dc1431ebaa63f43c397c37917b1a0a597a4c - media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1 - media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a - media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e - motion_sensors: 03f55b7c637a7e365a0b5f9697a449f9059d5d91 - motionphoto: d4a432b8c8f22fb3ad966258597c0103c9c5ff16 - move_to_background: 39a5b79b26d577b0372cbe8a8c55e7aa9fcd3a2d + 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 nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 - native_video_player: d12af78a1a4a8cf09775a5177d5b392def6fd23c - onnxruntime: e7c2ae44385191eaad5ae64c935a72debaddc997 + native_video_player: e363dd14f6a498ad8a8f7e6486a0db046ad19f13 + objective_c: 89e720c30d716b036faf9c9684022048eee1eee2 + onnxruntime: f9b296392c96c42882be020a59dbeac6310d81b2 onnxruntime-c: a909204639a1f035f575127ac406f781ac797c9c onnxruntime-objc: b6fab0f1787aa6f7190c2013f03037df4718bd8b - open_mail_app: 794172f6a22cd16319d3ddaf45e945b2f74952b0 + open_mail_app: 7314a609e88eed22d53671279e189af7a0ab0f11 OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94 - package_info_plus: 115f4ad11e0698c8c1c5d8a689390df880f47e85 - path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 - permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2 - photo_manager: ff695c7a1dd5bc379974953a2b5c0a293f7c4c8a - privacy_screen: 1a131c052ceb3c3659934b003b0d397c2381a24e + package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 + path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 + permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d + photo_manager: d2fbcc0f2d82458700ee6256a15018210a81d413 + privacy_screen: 3159a541f5d3a31bea916cfd4e58f9dc722b3fd4 PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 - receive_sharing_intent: df9c334dc9feadcbd3266e5cb49c8443405e1c9f - screen_brightness_ios: 715ca807df953bf676d339f11464e438143ee625 - SDWebImage: 73c6079366fea25fa4bb9640d5fb58f0893facd8 + receive_sharing_intent: 222384f00ffe7e952bbfabaa9e3967cb87e5fe00 + SDWebImage: f84b0feeb08d2d11e6a9b843cb06d75ebf5b8868 SDWebImageWebPCoder: e38c0a70396191361d60c092933e22c20d5b1380 - Sentry: f8374b5415bc38dfb5645941b3ae31230fbeae57 - sentry_flutter: 0eb93e5279eb41e2392212afe1ccd2fecb4f8cbe - share_plus: 8875f4f2500512ea181eef553c3e27dba5135aad - shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 - sqflite_darwin: a553b1fd6fe66f53bbb0fe5b4f5bab93f08d7a13 - sqlite3: 0bb0e6389d824e40296f531b858a2a0b71c0d2fb - sqlite3_flutter_libs: c00457ebd31e59fa6bb830380ddba24d44fbcd3b - system_info_plus: 5393c8da281d899950d751713575fbf91c7709aa - Toast: 1f5ea13423a1e6674c4abdac5be53587ae481c4e - ua_client_hints: 46bb5817a868f9e397c0ba7e3f2f5c5d90c35156 - uni_links: d97da20c7701486ba192624d99bffaaffcfc298a - url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe - video_player_avfoundation: 7c6c11d8470e1675df7397027218274b6d2360b3 - video_thumbnail: c4e2a3c539e247d4de13cd545344fd2d26ffafd1 - volume_controller: 531ddf792994285c9b17f9d8a7e4dcdd29b3eae9 - wakelock_plus: 8b09852c8876491e4b6d179e17dfe2a0b5f60d47 + Sentry: 0f9bc9adfc0b960e7f3bb5ec67e9a3d8193f3bdb + sentry_flutter: f4a0466dc8855998ffd59378ec33507c7dc32d7b + share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a + shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 + sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 + sqlite3: fc1400008a9b3525f5914ed715a5d1af0b8f4983 + sqlite3_flutter_libs: 3c323550ef3b928bc0aa9513c841e45a7d242832 + system_info_plus: 555ce7047fbbf29154726db942ae785c29211740 + ua_client_hints: 92fe0d139619b73ec9fcb46cc7e079a26178f586 + uni_links: f191d616c4db8750f74c72c988e79a83dd297fac + url_launcher_ios: 694010445543906933d732453a59da0a173ae33d + video_player_avfoundation: 2cef49524dd1f16c5300b9cd6efd9611ce03639b + video_thumbnail: 584ccfa55d8fd2f3d5507218b0a18d84c839c620 + volume_controller: 3657a1f65bedb98fa41ff7dc5793537919f31b12 + wakelock_plus: 04623e3f525556020ebd4034310f20fe7fda8b49 PODFILE CHECKSUM: 20e086e6008977d43a3d40260f3f9bffcac748dd diff --git a/mobile/ios/Runner.xcodeproj/project.pbxproj b/mobile/ios/Runner.xcodeproj/project.pbxproj index 65b1ae54d5..183710c51f 100644 --- a/mobile/ios/Runner.xcodeproj/project.pbxproj +++ b/mobile/ios/Runner.xcodeproj/project.pbxproj @@ -288,15 +288,15 @@ "${BUILT_PRODUCTS_DIR}/SDWebImage/SDWebImage.framework", "${BUILT_PRODUCTS_DIR}/SDWebImageWebPCoder/SDWebImageWebPCoder.framework", "${BUILT_PRODUCTS_DIR}/Sentry/Sentry.framework", - "${BUILT_PRODUCTS_DIR}/Toast/Toast.framework", "${BUILT_PRODUCTS_DIR}/background_fetch/background_fetch.framework", "${BUILT_PRODUCTS_DIR}/battery_info/battery_info.framework", "${BUILT_PRODUCTS_DIR}/connectivity_plus/connectivity_plus.framework", + "${BUILT_PRODUCTS_DIR}/cupertino_http/cupertino_http.framework", "${BUILT_PRODUCTS_DIR}/dart_ui_isolate/dart_ui_isolate.framework", "${BUILT_PRODUCTS_DIR}/device_info_plus/device_info_plus.framework", "${BUILT_PRODUCTS_DIR}/file_saver/file_saver.framework", "${BUILT_PRODUCTS_DIR}/flutter_email_sender/flutter_email_sender.framework", - "${BUILT_PRODUCTS_DIR}/flutter_image_compress/flutter_image_compress.framework", + "${BUILT_PRODUCTS_DIR}/flutter_image_compress_common/flutter_image_compress_common.framework", "${BUILT_PRODUCTS_DIR}/flutter_inappwebview_ios/flutter_inappwebview_ios.framework", "${BUILT_PRODUCTS_DIR}/flutter_local_notifications/flutter_local_notifications.framework", "${BUILT_PRODUCTS_DIR}/flutter_native_splash/flutter_native_splash.framework", @@ -314,20 +314,19 @@ "${BUILT_PRODUCTS_DIR}/maps_launcher/maps_launcher.framework", "${BUILT_PRODUCTS_DIR}/media_extension/media_extension.framework", "${BUILT_PRODUCTS_DIR}/media_kit_libs_ios_video/media_kit_libs_ios_video.framework", - "${BUILT_PRODUCTS_DIR}/media_kit_native_event_loop/media_kit_native_event_loop.framework", "${BUILT_PRODUCTS_DIR}/media_kit_video/media_kit_video.framework", "${BUILT_PRODUCTS_DIR}/motion_sensors/motion_sensors.framework", "${BUILT_PRODUCTS_DIR}/motionphoto/motionphoto.framework", "${BUILT_PRODUCTS_DIR}/move_to_background/move_to_background.framework", "${BUILT_PRODUCTS_DIR}/nanopb/nanopb.framework", "${BUILT_PRODUCTS_DIR}/native_video_player/native_video_player.framework", + "${BUILT_PRODUCTS_DIR}/objective_c/objective_c.framework", "${BUILT_PRODUCTS_DIR}/open_mail_app/open_mail_app.framework", "${BUILT_PRODUCTS_DIR}/package_info_plus/package_info_plus.framework", "${BUILT_PRODUCTS_DIR}/path_provider_foundation/path_provider_foundation.framework", "${BUILT_PRODUCTS_DIR}/photo_manager/photo_manager.framework", "${BUILT_PRODUCTS_DIR}/privacy_screen/privacy_screen.framework", "${BUILT_PRODUCTS_DIR}/receive_sharing_intent/receive_sharing_intent.framework", - "${BUILT_PRODUCTS_DIR}/screen_brightness_ios/screen_brightness_ios.framework", "${BUILT_PRODUCTS_DIR}/sentry_flutter/sentry_flutter.framework", "${BUILT_PRODUCTS_DIR}/share_plus/share_plus.framework", "${BUILT_PRODUCTS_DIR}/shared_preferences_foundation/shared_preferences_foundation.framework", @@ -383,15 +382,15 @@ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SDWebImage.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SDWebImageWebPCoder.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Sentry.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Toast.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/background_fetch.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/battery_info.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/connectivity_plus.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/cupertino_http.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/dart_ui_isolate.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/device_info_plus.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/file_saver.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_email_sender.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_image_compress.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_image_compress_common.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_inappwebview_ios.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_local_notifications.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/flutter_native_splash.framework", @@ -409,20 +408,19 @@ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/maps_launcher.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/media_extension.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/media_kit_libs_ios_video.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/media_kit_native_event_loop.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/media_kit_video.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/motion_sensors.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/motionphoto.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/move_to_background.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/nanopb.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/native_video_player.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/objective_c.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/open_mail_app.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/package_info_plus.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/path_provider_foundation.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/photo_manager.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/privacy_screen.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/receive_sharing_intent.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/screen_brightness_ios.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/sentry_flutter.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/share_plus.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/shared_preferences_foundation.framework", diff --git a/mobile/lib/app.dart b/mobile/lib/app.dart index 96111aa10b..4596e06710 100644 --- a/mobile/lib/app.dart +++ b/mobile/lib/app.dart @@ -16,7 +16,7 @@ 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/sync_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"; import "package:photos/utils/intent_util.dart"; diff --git a/mobile/lib/core/configuration.dart b/mobile/lib/core/configuration.dart index e15193a5d3..e18f4e3a40 100644 --- a/mobile/lib/core/configuration.dart +++ b/mobile/lib/core/configuration.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import "dart:io"; import 'package:bip39/bip39.dart' as bip39; +import "package:ente_crypto/ente_crypto.dart"; import "package:flutter/services.dart"; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:logging/logging.dart'; @@ -12,16 +13,16 @@ import 'package:photos/core/error-reporting/super_logging.dart'; import 'package:photos/core/event_bus.dart'; import 'package:photos/db/collections_db.dart'; import 'package:photos/db/files_db.dart'; -import 'package:photos/db/memories_db.dart'; +import "package:photos/db/memories_db.dart"; import "package:photos/db/ml/db.dart"; import 'package:photos/db/trash_db.dart'; import 'package:photos/db/upload_locks_db.dart'; import "package:photos/events/endpoint_updated_event.dart"; import 'package:photos/events/signed_in_event.dart'; import 'package:photos/events/user_logged_out_event.dart'; -import 'package:photos/models/key_attributes.dart'; -import 'package:photos/models/key_gen_result.dart'; -import 'package:photos/models/private_key_attributes.dart'; +import 'package:photos/models/api/user/key_attributes.dart'; +import 'package:photos/models/api/user/key_gen_result.dart'; +import 'package:photos/models/api/user/private_key_attributes.dart'; import 'package:photos/services/collections_service.dart'; import 'package:photos/services/favorites_service.dart'; import "package:photos/services/home_widget_service.dart"; @@ -29,8 +30,7 @@ import 'package:photos/services/ignored_files_service.dart'; import "package:photos/services/machine_learning/face_ml/person/person_service.dart"; import 'package:photos/services/memories_service.dart'; import 'package:photos/services/search_service.dart'; -import 'package:photos/services/sync_service.dart'; -import 'package:photos/utils/crypto_util.dart'; +import 'package:photos/services/sync/sync_service.dart'; import 'package:photos/utils/file_uploader.dart'; import "package:photos/utils/lock_screen_settings.dart"; import 'package:photos/utils/validator_util.dart'; @@ -248,7 +248,7 @@ class Configuration { // decrypt the master key final kekSalt = CryptoUtil.getSaltToDeriveKey(); final derivedKeyResult = await CryptoUtil.deriveSensitiveKey( - utf8.encode(password) as Uint8List, + utf8.encode(password), kekSalt, ); final loginKey = await CryptoUtil.deriveLoginKey(derivedKeyResult.key); @@ -294,7 +294,7 @@ class Configuration { // decrypt the master key final kekSalt = CryptoUtil.getSaltToDeriveKey(); final derivedKeyResult = await CryptoUtil.deriveSensitiveKey( - utf8.encode(password) as Uint8List, + utf8.encode(password), kekSalt, ); final loginKey = await CryptoUtil.deriveLoginKey(derivedKeyResult.key); @@ -332,7 +332,7 @@ class Configuration { // Derive key-encryption-key from the entered password and existing // mem and ops limits keyEncryptionKey ??= await CryptoUtil.deriveKey( - utf8.encode(password) as Uint8List, + utf8.encode(password), CryptoUtil.base642bin(attributes.kekSalt), attributes.memLimit!, attributes.opsLimit!, diff --git a/mobile/lib/core/constants.dart b/mobile/lib/core/constants.dart index f55c3344ed..58f1fb6963 100644 --- a/mobile/lib/core/constants.dart +++ b/mobile/lib/core/constants.dart @@ -113,3 +113,5 @@ final tempDirCleanUpInterval = kDebugMode const kFilterChipHeight = 32.0; const kMaxAppbarFilters = 14; + +const kLivePhotoHashSeparator = ':'; diff --git a/mobile/lib/core/error-reporting/super_logging.dart b/mobile/lib/core/error-reporting/super_logging.dart index 7c0bef58be..dbe2258098 100644 --- a/mobile/lib/core/error-reporting/super_logging.dart +++ b/mobile/lib/core/error-reporting/super_logging.dart @@ -18,6 +18,7 @@ import 'package:photos/core/error-reporting/tunneled_transport.dart'; import "package:photos/core/errors.dart"; import 'package:photos/models/typedefs.dart'; import "package:photos/utils/device_info.dart"; +import "package:photos/utils/ram_check_util.dart"; import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -205,6 +206,12 @@ class SuperLogging { }), ); + unawaited( + checkDeviceTotalRAM().then((ram) { + if (ram != null) $.info("Device RAM: ${ram}MB"); + }), + ); + if (appConfig.body == null) return; if (enable && sentryIsEnabled) { @@ -236,7 +243,7 @@ class SuperLogging { } static _shouldSkipSentry(Object error) { - if (error is DioError) { + if (error is DioException) { return true; } final bool result = error is StorageLimitExceededError || diff --git a/mobile/lib/core/errors.dart b/mobile/lib/core/errors.dart index f1c7c24b3c..0c0d16949a 100644 --- a/mobile/lib/core/errors.dart +++ b/mobile/lib/core/errors.dart @@ -20,7 +20,7 @@ extension InvalidReasonExn on InvalidReason { class InvalidFileError extends ArgumentError { final InvalidReason reason; - InvalidFileError(String message, this.reason) : super(message); + InvalidFileError(String super.message, this.reason); @override String toString() { @@ -58,24 +58,22 @@ bool isHandledSyncError(Object errObj) { class LockAlreadyAcquiredError extends Error {} +class LockFreedError extends Error{} + class UnauthorizedError extends Error {} class RequestCancelledError extends Error {} class InvalidSyncStatusError extends AssertionError { - InvalidSyncStatusError(String message) : super(message); + InvalidSyncStatusError(String super.message); } class UnauthorizedEditError extends AssertionError {} class InvalidStateError extends AssertionError { - InvalidStateError(String message) : super(message); + InvalidStateError(String super.message); } -class KeyDerivationError extends Error {} - -class LoginKeyDerivationError extends Error {} - class SrpSetupNotCompleteError extends Error {} class SharingNotPermittedForFreeAccountsError extends Error {} diff --git a/mobile/lib/core/network/network.dart b/mobile/lib/core/network/network.dart index a55a1a807e..ba2540b401 100644 --- a/mobile/lib/core/network/network.dart +++ b/mobile/lib/core/network/network.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'package:dio/dio.dart'; +import 'package:native_dio_adapter/native_dio_adapter.dart'; import 'package:package_info_plus/package_info_plus.dart'; import "package:photos/core/configuration.dart"; import "package:photos/core/event_bus.dart"; @@ -8,18 +9,17 @@ import 'package:photos/core/network/ente_interceptor.dart'; import "package:photos/events/endpoint_updated_event.dart"; import "package:ua_client_hints/ua_client_hints.dart"; -int kConnectTimeout = 15000; - class NetworkClient { late Dio _dio; late Dio _enteDio; + static const kConnectTimeout = 15; Future init(PackageInfo packageInfo) async { final String ua = await userAgent(); final endpoint = Configuration.instance.getHttpEndpoint(); _dio = Dio( BaseOptions( - connectTimeout: kConnectTimeout, + connectTimeout: const Duration(seconds: kConnectTimeout), headers: { HttpHeaders.userAgentHeader: ua, 'X-Client-Version': packageInfo.version, @@ -30,7 +30,7 @@ class NetworkClient { _enteDio = Dio( BaseOptions( baseUrl: endpoint, - connectTimeout: kConnectTimeout, + connectTimeout: const Duration(seconds: kConnectTimeout), headers: { HttpHeaders.userAgentHeader: ua, 'X-Client-Version': packageInfo.version, @@ -38,6 +38,10 @@ class NetworkClient { }, ), ); + + _dio.httpClientAdapter = NativeAdapter(); + _enteDio.httpClientAdapter = NativeAdapter(); + _setupInterceptors(endpoint); Bus.instance.on().listen((event) { diff --git a/mobile/lib/data/years.dart b/mobile/lib/data/years.dart index 2b2e04d911..5f496d2a68 100644 --- a/mobile/lib/data/years.dart +++ b/mobile/lib/data/years.dart @@ -1,4 +1,4 @@ -import 'package:photos/utils/date_time_util.dart'; +import 'package:photos/utils/standalone/date_time.dart'; class YearsData { final List yearsData = []; diff --git a/mobile/lib/db/collections_db.dart b/mobile/lib/db/collections_db.dart index 178e81302e..c73b9a8e35 100644 --- a/mobile/lib/db/collections_db.dart +++ b/mobile/lib/db/collections_db.dart @@ -251,20 +251,20 @@ class CollectionsDB { Map _getRowForCollection(Collection collection) { final row = {}; row[columnID] = collection.id; - row[columnOwner] = collection.owner!.toJson(); + row[columnOwner] = collection.owner.toJson(); row[columnEncryptedKey] = collection.encryptedKey; row[columnKeyDecryptionNonce] = collection.keyDecryptionNonce; row[columnName] = collection.name; row[columnEncryptedName] = collection.encryptedName; row[columnNameDecryptionNonce] = collection.nameDecryptionNonce; - row[columnType] = Collection.typeToString(collection.type); + row[columnType] = typeToString(collection.type); row[columnEncryptedPath] = collection.attributes.encryptedPath; row[columnPathDecryptionNonce] = collection.attributes.pathDecryptionNonce; row[columnVersion] = collection.attributes.version; row[columnSharees] = - json.encode(collection.sharees?.map((x) => x?.toMap()).toList()); + json.encode(collection.sharees.map((x) => x.toMap()).toList()); row[columnPublicURLs] = - json.encode(collection.publicURLs?.map((x) => x?.toMap()).toList()); + json.encode(collection.publicURLs.map((x) => x.toMap()).toList()); row[columnUpdationTime] = collection.updationTime; if (collection.isDeleted) { row[columnIsDeleted] = _sqlBoolTrue; @@ -290,7 +290,7 @@ class CollectionsDB { row[columnName], row[columnEncryptedName], row[columnNameDecryptionNonce], - Collection.typeFromString(row[columnType]), + typeFromString(row[columnType]), CollectionAttributes( encryptedPath: row[columnEncryptedPath], pathDecryptionNonce: row[columnPathDecryptionNonce], diff --git a/mobile/lib/utils/sqlite_util.dart b/mobile/lib/db/enum/conflict_algo.dart similarity index 100% rename from mobile/lib/utils/sqlite_util.dart rename to mobile/lib/db/enum/conflict_algo.dart diff --git a/mobile/lib/db/file_updation_db.dart b/mobile/lib/db/file_updation_db.dart index 3d781b7867..e4f79380a8 100644 --- a/mobile/lib/db/file_updation_db.dart +++ b/mobile/lib/db/file_updation_db.dart @@ -14,7 +14,6 @@ class FileUpdationDB { static const tableName = 're_upload_tracker'; static const columnLocalID = 'local_id'; static const columnReason = 'reason'; - static const livePhotoCheck = 'livePhotoCheck'; static const androidMissingGPS = 'androidMissingGPS'; static const modificationTimeUpdated = 'modificationTimeUpdated'; diff --git a/mobile/lib/db/files_db.dart b/mobile/lib/db/files_db.dart index 4452f12097..b4bffc79a3 100644 --- a/mobile/lib/db/files_db.dart +++ b/mobile/lib/db/files_db.dart @@ -6,6 +6,7 @@ import 'package:flutter/foundation.dart'; import 'package:logging/logging.dart'; import 'package:path/path.dart'; import 'package:path_provider/path_provider.dart'; +import "package:photos/db/enum/conflict_algo.dart"; import "package:photos/extensions/stop_watch.dart"; import 'package:photos/models/backup_status.dart'; import 'package:photos/models/file/file.dart'; @@ -15,8 +16,6 @@ import 'package:photos/models/location/location.dart'; import "package:photos/models/metadata/common_keys.dart"; import "package:photos/services/filter/db_filters.dart"; import 'package:photos/utils/file_uploader_util.dart'; -import "package:photos/utils/primitive_wrapper.dart"; -import "package:photos/utils/sqlite_util.dart"; import 'package:sqlite_async/sqlite_async.dart'; class FilesDB { @@ -468,72 +467,61 @@ class FilesDB { final startTime = DateTime.now(); final db = await sqliteAsyncDB; - ///Strong batch counter in an object so that it gets passed by reference - ///Primitives are passed by value - final genIdNotNullbatchCounter = PrimitiveWrapper(0); - final genIdNullbatchCounter = PrimitiveWrapper(0); - final genIdNullParameterSets = >[]; - final genIdNotNullParameterSets = >[]; + final withIdParams = >[]; + const withIdColumnNames = _columnNames; + final withoutIdParams = >[]; + final withoutIdColumns = + _columnNames.where((column) => column != columnGeneratedID).toList(); - final genIdNullcolumnNames = - _columnNames.where((element) => element != columnGeneratedID); + // Sort files into appropriate parameter sets + for (final file in files) { + if (file.generatedID == null) { + withoutIdParams.add(_getParameterSetForFile(file)); - for (EnteFile file in files) { - final fileGenIdIsNull = file.generatedID == null; - - if (!fileGenIdIsNull) { - await _batchAndInsertFile( - file, - conflictAlgorithm, - db, - genIdNotNullParameterSets, - genIdNotNullbatchCounter, - isGenIdNull: fileGenIdIsNull, - ); + if (withoutIdParams.length == 400) { + await _insertBatch( + conflictAlgorithm, + withoutIdColumns, + db, + withoutIdParams, + ); + withoutIdParams.clear(); + } } else { - await _batchAndInsertFile( - file, - conflictAlgorithm, - db, - genIdNullParameterSets, - genIdNullbatchCounter, - isGenIdNull: fileGenIdIsNull, - ); + withIdParams.add(_getParameterSetForFile(file)); + if (withIdParams.length == 400) { + await _insertBatch( + conflictAlgorithm, + withIdColumnNames, + db, + withIdParams, + ); + withIdParams.clear(); + } } } - if (genIdNotNullbatchCounter.value > 0) { + // Insert any remaining files + if (withIdParams.isNotEmpty) { await _insertBatch( conflictAlgorithm, - _columnNames, + withIdColumnNames, db, - genIdNotNullParameterSets, + withIdParams, ); - genIdNotNullbatchCounter.value = 0; - genIdNotNullParameterSets.clear(); - } - if (genIdNullbatchCounter.value > 0) { - await _insertBatch( - conflictAlgorithm, - genIdNullcolumnNames, - db, - genIdNullParameterSets, - ); - genIdNullbatchCounter.value = 0; - genIdNullParameterSets.clear(); } - final endTime = DateTime.now(); - final duration = Duration( - microseconds: - endTime.microsecondsSinceEpoch - startTime.microsecondsSinceEpoch, - ); + if (withoutIdParams.isNotEmpty) { + await _insertBatch( + conflictAlgorithm, + withoutIdColumns, + db, + withoutIdParams, + ); + } + final duration = DateTime.now().difference(startTime); _logger.info( - "Batch insert of " + - files.length.toString() + - " took " + - duration.inMilliseconds.toString() + - "ms.", + "Batch insert of ${files.length} took ${duration.inMilliseconds}ms.", ); } @@ -1272,15 +1260,26 @@ class FilesDB { ); } - Future> getLocalFiles(List localIDs) async { + Future> getLocalFiles( + List localIDs, { + bool dedupeByLocalID = false, + }) async { + late final String query; final inParam = localIDs.map((id) => "'$id'").join(','); final db = await instance.sqliteAsyncDB; - final results = await db.getAll( - ''' + if (dedupeByLocalID) { + query = ''' + SELECT * FROM $filesTable + WHERE $columnLocalID IN ($inParam) + GROUP BY $columnLocalID; + '''; + } else { + query = ''' SELECT * FROM $filesTable WHERE $columnLocalID IN ($inParam); - ''', - ); + '''; + } + final results = await db.getAll(query); return convertToFiles(results); } @@ -1733,6 +1732,7 @@ class FilesDB { Future> getAllFilesAfterDate({ required FileType fileType, required DateTime beginDate, + required int userID, }) async { final db = await instance.sqliteAsyncDB; final results = await db.getAll( @@ -1741,6 +1741,7 @@ class FilesDB { WHERE $columnFileType = ? AND $columnCreationTime > ? AND $columnUploadedFileID != -1 + AND $columnOwnerID = $userID ORDER BY $columnCreationTime DESC ''', [getInt(fileType), beginDate.microsecondsSinceEpoch], @@ -1941,28 +1942,6 @@ class FilesDB { return values; } - Future _batchAndInsertFile( - EnteFile file, - SqliteAsyncConflictAlgorithm conflictAlgorithm, - SqliteDatabase db, - List> parameterSets, - PrimitiveWrapper batchCounter, { - required bool isGenIdNull, - }) async { - parameterSets.add(_getParameterSetForFile(file)); - batchCounter.value++; - - final columnNames = isGenIdNull - ? _columnNames.where((column) => column != columnGeneratedID) - : _columnNames; - if (batchCounter.value == 400) { - _logger.info("Inserting batch with genIdNull: $isGenIdNull"); - await _insertBatch(conflictAlgorithm, columnNames, db, parameterSets); - batchCounter.value = 0; - parameterSets.clear(); - } - } - Future _insertBatch( SqliteAsyncConflictAlgorithm conflictAlgorithm, Iterable columnNames, diff --git a/mobile/lib/db/memories_db.dart b/mobile/lib/db/memories_db.dart index eb84627b41..acee44fec9 100644 --- a/mobile/lib/db/memories_db.dart +++ b/mobile/lib/db/memories_db.dart @@ -3,7 +3,7 @@ import 'dart:io'; import 'package:path/path.dart'; import 'package:path_provider/path_provider.dart'; -import 'package:photos/models/memory.dart'; +import 'package:photos/models/memories/memory.dart'; import 'package:sqflite/sqflite.dart'; class MemoriesDB { diff --git a/mobile/lib/db/ml/base.dart b/mobile/lib/db/ml/base.dart index c2685b1214..a99f70f813 100644 --- a/mobile/lib/db/ml/base.dart +++ b/mobile/lib/db/ml/base.dart @@ -108,9 +108,6 @@ abstract class IMLDataDB { Future> getAllClipVectors(); Future> clipIndexedFileWithVersion(); - Future> getClipVectorsForFileIDs( - Iterable fileIDs, - ); Future getClipIndexedFileCount({int minimumMlVersion}); Future putClip(List embeddings); Future deleteClipEmbeddings(List fileIDs); diff --git a/mobile/lib/db/ml/db.dart b/mobile/lib/db/ml/db.dart index 3ea5d88c53..124d5052d8 100644 --- a/mobile/lib/db/ml/db.dart +++ b/mobile/lib/db/ml/db.dart @@ -1196,22 +1196,6 @@ class MLDataDB extends IMLDataDB { return _convertToVectors(results); } - @override - Future> getClipVectorsForFileIDs( - Iterable fileIDs, - ) async { - final db = await MLDataDB.instance.asyncDB; - final results = await db.getAll( - 'SELECT * FROM $clipTable WHERE $fileIDColumn IN (${fileIDs.join(", ")})', - ); - final Map embeddings = {}; - for (final result in results) { - final embedding = _getVectorFromRow(result); - embeddings[embedding.fileID] = embedding; - } - return embeddings; - } - // Get indexed FileIDs @override Future> clipIndexedFileWithVersion() async { diff --git a/mobile/lib/db/trash_db.dart b/mobile/lib/db/trash_db.dart index a4a4ae3dbf..d16fcae958 100644 --- a/mobile/lib/db/trash_db.dart +++ b/mobile/lib/db/trash_db.dart @@ -101,20 +101,6 @@ class TrashDB { await db.delete(tableName); } - // getRecentlyTrashedFile returns the file which was trashed recently - Future getRecentlyTrashedFile() async { - final db = await instance.database; - final rows = await db.query( - tableName, - orderBy: '$columnTrashDeleteBy DESC', - limit: 1, - ); - if (rows.isEmpty) { - return null; - } - return _getTrashFromRow(rows[0]); - } - Future count() async { final db = await instance.database; final count = Sqflite.firstIntValue( @@ -156,15 +142,6 @@ class TrashDB { ); } - Future insert(TrashFile trash) async { - final db = await instance.database; - return db.insert( - tableName, - _getRowForTrash(trash), - conflictAlgorithm: ConflictAlgorithm.replace, - ); - } - Future delete(List uploadedFileIDs) async { final db = await instance.database; return db.delete( diff --git a/mobile/lib/db/upload_locks_db.dart b/mobile/lib/db/upload_locks_db.dart index 0c4b112634..ab786d5c78 100644 --- a/mobile/lib/db/upload_locks_db.dart +++ b/mobile/lib/db/upload_locks_db.dart @@ -45,6 +45,14 @@ class UploadLocksDB { columnPartStatus: "part_status", ); + static const _streamUploadErrorTable = ( + table: "stream_upload_error", + columnUploadedFileID: "uploaded_file_id", + columnErrorMessage: "error_message", + columnLastAttemptedAt: "last_attempted_at", + columnCreatedAt: "created_at", + ); + static final initializationScript = [ ..._createUploadLocksTable(), ]; @@ -118,6 +126,14 @@ class UploadLocksDB { PRIMARY KEY (${_partsTable.columnObjectKey}, ${_partsTable.columnPartNumber}) ) ''', + ''' + CREATE TABLE IF NOT EXISTS ${_streamUploadErrorTable.table} ( + ${_streamUploadErrorTable.columnUploadedFileID} INTEGER PRIMARY KEY, + ${_streamUploadErrorTable.columnErrorMessage} TEXT NOT NULL, + ${_streamUploadErrorTable.columnLastAttemptedAt} INTEGER NOT NULL, + ${_streamUploadErrorTable.columnCreatedAt} INTEGER DEFAULT CURRENT_TIMESTAMP NOT NULL + ) + ''', ]; } @@ -291,6 +307,68 @@ class UploadLocksDB { ); } + Future appendStreamEntry( + int uploadedFileID, + String errorMessage, + ) async { + final db = await UploadLocksDB.instance.database; + + await db.insert( + _streamUploadErrorTable.table, + { + _streamUploadErrorTable.columnUploadedFileID: uploadedFileID, + _streamUploadErrorTable.columnErrorMessage: errorMessage, + _streamUploadErrorTable.columnLastAttemptedAt: + DateTime.now().millisecondsSinceEpoch, + }, + conflictAlgorithm: ConflictAlgorithm.replace, + ); + } + + Future updateStreamStatus( + int uploadedFileID, + String errorMessage, + ) async { + final db = await instance.database; + await db.update( + _streamUploadErrorTable.table, + { + _streamUploadErrorTable.columnErrorMessage: errorMessage, + _streamUploadErrorTable.columnLastAttemptedAt: + DateTime.now().millisecondsSinceEpoch, + }, + where: '${_streamUploadErrorTable.columnUploadedFileID} = ?', + whereArgs: [uploadedFileID], + ); + } + + Future deleteStreamUploadErrorEntry(int uploadedFileID) async { + final db = await instance.database; + return await db.delete( + _streamUploadErrorTable.table, + where: '${_streamUploadErrorTable.columnUploadedFileID} = ?', + whereArgs: [uploadedFileID], + ); + } + + Future> getStreamUploadError() { + return instance.database.then((db) async { + final rows = await db.query( + _streamUploadErrorTable.table, + columns: [ + _streamUploadErrorTable.columnUploadedFileID, + _streamUploadErrorTable.columnErrorMessage, + ], + ); + final map = {}; + for (final row in rows) { + map[row[_streamUploadErrorTable.columnUploadedFileID] as int] = + row[_streamUploadErrorTable.columnErrorMessage] as String; + } + return map; + }); + } + Future createTrackUploadsEntry( String localId, String fileHash, diff --git a/mobile/lib/emergency/emergency_page.dart b/mobile/lib/emergency/emergency_page.dart index 04c60b7923..a4b218badb 100644 --- a/mobile/lib/emergency/emergency_page.dart +++ b/mobile/lib/emergency/emergency_page.dart @@ -23,9 +23,9 @@ import "package:photos/ui/components/models/button_type.dart"; import "package:photos/ui/components/notification_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/notification/toast.dart"; import "package:photos/ui/sharing/user_avator_widget.dart"; import "package:photos/utils/navigation_util.dart"; -import "package:photos/utils/toast_util.dart"; class EmergencyPage extends StatefulWidget { const EmergencyPage({ diff --git a/mobile/lib/emergency/emergency_service.dart b/mobile/lib/emergency/emergency_service.dart index 10d6ad7b0f..1afb01adc5 100644 --- a/mobile/lib/emergency/emergency_service.dart +++ b/mobile/lib/emergency/emergency_service.dart @@ -3,18 +3,18 @@ import "dart:math"; import "dart:typed_data"; import "package:dio/dio.dart"; +import "package:ente_crypto/ente_crypto.dart"; import "package:flutter/cupertino.dart"; import "package:logging/logging.dart"; import "package:photos/core/configuration.dart"; import "package:photos/core/network/network.dart"; import "package:photos/emergency/model.dart"; import "package:photos/generated/l10n.dart"; +import "package:photos/models/api/user/key_attributes.dart"; +import "package:photos/models/api/user/set_keys_request.dart"; import "package:photos/models/api/user/srp.dart"; -import "package:photos/models/key_attributes.dart"; -import "package:photos/models/set_keys_request.dart"; -import "package:photos/services/user_service.dart"; +import "package:photos/services/account/user_service.dart"; import "package:photos/ui/common/user_dialogs.dart"; -import "package:photos/utils/crypto_util.dart"; import "package:photos/utils/dialog_util.dart"; import "package:photos/utils/email_util.dart"; import "package:pointycastle/pointycastle.dart"; diff --git a/mobile/lib/emergency/other_contact_page.dart b/mobile/lib/emergency/other_contact_page.dart index f6f6787911..89235aa330 100644 --- a/mobile/lib/emergency/other_contact_page.dart +++ b/mobile/lib/emergency/other_contact_page.dart @@ -7,7 +7,7 @@ import "package:photos/emergency/model.dart"; import "package:photos/emergency/recover_others_account.dart"; import "package:photos/generated/l10n.dart"; import "package:photos/l10n/l10n.dart"; -import "package:photos/models/key_attributes.dart"; +import "package:photos/models/api/user/key_attributes.dart"; import "package:photos/theme/colors.dart"; import "package:photos/theme/ente_theme.dart"; import "package:photos/ui/components/action_sheet_widget.dart"; @@ -17,9 +17,9 @@ import "package:photos/ui/components/menu_item_widget/menu_item_widget.dart"; import "package:photos/ui/components/menu_section_title.dart"; import "package:photos/ui/components/models/button_type.dart"; import "package:photos/ui/components/title_bar_title_widget.dart"; -import "package:photos/utils/date_time_util.dart"; import "package:photos/utils/dialog_util.dart"; import "package:photos/utils/navigation_util.dart"; +import "package:photos/utils/standalone/date_time.dart"; // OtherContactPage is used to start recovery process for other user's account // Based on the state of the contact & recovery session, it will show diff --git a/mobile/lib/emergency/recover_others_account.dart b/mobile/lib/emergency/recover_others_account.dart index d29683ecfd..4c31c971e4 100644 --- a/mobile/lib/emergency/recover_others_account.dart +++ b/mobile/lib/emergency/recover_others_account.dart @@ -1,5 +1,6 @@ import "dart:convert"; +import "package:ente_crypto/ente_crypto.dart"; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:logging/logging.dart'; @@ -7,12 +8,11 @@ import 'package:password_strength/password_strength.dart'; import "package:photos/emergency/emergency_service.dart"; import "package:photos/emergency/model.dart"; import "package:photos/generated/l10n.dart"; -import "package:photos/models/key_attributes.dart"; -import "package:photos/models/set_keys_request.dart"; +import "package:photos/models/api/user/key_attributes.dart"; +import "package:photos/models/api/user/set_keys_request.dart"; import 'package:photos/ui/common/dynamic_fab.dart'; -import "package:photos/utils/crypto_util.dart"; +import 'package:photos/ui/notification/toast.dart'; import 'package:photos/utils/dialog_util.dart'; -import 'package:photos/utils/toast_util.dart'; class RecoverOthersAccount extends StatefulWidget { final String recoveryKey; @@ -325,7 +325,7 @@ class _RecoverOthersAccountState extends State { // decrypt the master key final kekSalt = CryptoUtil.getSaltToDeriveKey(); final derivedKeyResult = await CryptoUtil.deriveSensitiveKey( - utf8.encode(password) as Uint8List, + utf8.encode(password), kekSalt, ); final loginKey = await CryptoUtil.deriveLoginKey(derivedKeyResult.key); diff --git a/mobile/lib/emergency/select_contact_page.dart b/mobile/lib/emergency/select_contact_page.dart index c12be89bbd..5253d01f52 100644 --- a/mobile/lib/emergency/select_contact_page.dart +++ b/mobile/lib/emergency/select_contact_page.dart @@ -6,8 +6,8 @@ import "package:photos/emergency/emergency_service.dart"; import "package:photos/emergency/model.dart"; import "package:photos/generated/l10n.dart"; import "package:photos/models/api/collection/user.dart"; +import "package:photos/services/account/user_service.dart"; import 'package:photos/services/collections_service.dart'; -import "package:photos/services/user_service.dart"; import 'package:photos/theme/ente_theme.dart'; import 'package:photos/ui/actions/collection/collection_sharing_actions.dart'; import 'package:photos/ui/components/buttons/button_widget.dart'; @@ -322,22 +322,20 @@ class _AddContactPage extends State { final int ownerID = Configuration.instance.getUserID()!; existingEmails.add(Configuration.instance.getEmail()!); for (final c in CollectionsService.instance.getActiveCollections()) { - if (c.owner?.id == ownerID) { - for (final User? u in c.sharees ?? []) { - if (u != null && - u.id != null && + if (c.owner.id == ownerID) { + for (final User u in c.sharees) { + if (u.id != null && u.email.isNotEmpty && !existingEmails.contains(u.email)) { existingEmails.add(u.email); suggestedUsers.add(u); } } - } else if (c.owner != null && - c.owner!.id != null && - c.owner!.email.isNotEmpty && - !existingEmails.contains(c.owner!.email)) { - existingEmails.add(c.owner!.email); - suggestedUsers.add(c.owner!); + } else if (c.owner.id != null && + c.owner.email.isNotEmpty && + !existingEmails.contains(c.owner.email)) { + existingEmails.add(c.owner.email); + suggestedUsers.add(c.owner); } } final cachedUserDetails = UserService.instance.getCachedUserDetails(); diff --git a/mobile/lib/ente_theme_data.dart b/mobile/lib/ente_theme_data.dart index 6ee3384ac4..3e5665d56e 100644 --- a/mobile/lib/ente_theme_data.dart +++ b/mobile/lib/ente_theme_data.dart @@ -15,7 +15,7 @@ final lightThemeData = ThemeData( colorScheme: const ColorScheme.light( primary: Colors.black, secondary: Color.fromARGB(255, 163, 163, 163), - background: Colors.white, + surface: Colors.white, surfaceTint: Colors.transparent, ), outlinedButtonTheme: buildOutlinedButtonThemeData( @@ -70,13 +70,13 @@ final lightThemeData = ThemeData( color: Colors.black, width: 2, ), - fillColor: MaterialStateProperty.resolveWith((states) { - return states.contains(MaterialState.selected) + fillColor: WidgetStateProperty.resolveWith((states) { + return states.contains(WidgetState.selected) ? const Color.fromRGBO(0, 0, 0, 1) : const Color.fromRGBO(255, 255, 255, 1); }), - checkColor: MaterialStateProperty.resolveWith((states) { - return states.contains(MaterialState.selected) + checkColor: WidgetStateProperty.resolveWith((states) { + return states.contains(WidgetState.selected) ? const Color.fromRGBO(255, 255, 255, 1) : const Color.fromRGBO(0, 0, 0, 1); }), @@ -93,7 +93,7 @@ final darkThemeData = ThemeData( hintColor: const Color.fromRGBO(158, 158, 158, 1), colorScheme: const ColorScheme.dark( primary: Colors.white, - background: Color.fromRGBO(0, 0, 0, 1), + surface: Color.fromRGBO(0, 0, 0, 1), secondary: Color.fromARGB(255, 163, 163, 163), surfaceTint: Colors.transparent, ), @@ -145,15 +145,15 @@ final darkThemeData = ThemeData( color: Colors.grey, width: 2, ), - fillColor: MaterialStateProperty.resolveWith((states) { - if (states.contains(MaterialState.selected)) { + fillColor: WidgetStateProperty.resolveWith((states) { + if (states.contains(WidgetState.selected)) { return const Color.fromRGBO(158, 158, 158, 1); } else { return const Color.fromRGBO(0, 0, 0, 1); } }), - checkColor: MaterialStateProperty.resolveWith((states) { - if (states.contains(MaterialState.selected)) { + checkColor: WidgetStateProperty.resolveWith((states) { + if (states.contains(WidgetState.selected)) { return const Color.fromRGBO(0, 0, 0, 1); } else { return const Color.fromRGBO(158, 158, 158, 1); @@ -378,17 +378,17 @@ OutlinedButtonThemeData buildOutlinedButtonThemeData({ fontSize: 18, ), ).copyWith( - backgroundColor: MaterialStateProperty.resolveWith( - (Set states) { - if (states.contains(MaterialState.disabled)) { + backgroundColor: WidgetStateProperty.resolveWith( + (Set states) { + if (states.contains(WidgetState.disabled)) { return bgDisabled; } return bgEnabled; }, ), - foregroundColor: MaterialStateProperty.resolveWith( - (Set states) { - if (states.contains(MaterialState.disabled)) { + foregroundColor: WidgetStateProperty.resolveWith( + (Set states) { + if (states.contains(WidgetState.disabled)) { return fgDisabled; } return fgEnabled; @@ -426,21 +426,21 @@ ElevatedButtonThemeData buildElevatedButtonThemeData({ SwitchThemeData getSwitchThemeData(Color activeColor) { return SwitchThemeData( thumbColor: - MaterialStateProperty.resolveWith((Set states) { - if (states.contains(MaterialState.disabled)) { + WidgetStateProperty.resolveWith((Set states) { + if (states.contains(WidgetState.disabled)) { return null; } - if (states.contains(MaterialState.selected)) { + if (states.contains(WidgetState.selected)) { return activeColor; } return null; }), trackColor: - MaterialStateProperty.resolveWith((Set states) { - if (states.contains(MaterialState.disabled)) { + WidgetStateProperty.resolveWith((Set states) { + if (states.contains(WidgetState.disabled)) { return null; } - if (states.contains(MaterialState.selected)) { + if (states.contains(WidgetState.selected)) { return activeColor; } return null; diff --git a/mobile/lib/events/seekbar_triggered_event.dart b/mobile/lib/events/seekbar_triggered_event.dart new file mode 100644 index 0000000000..33d5aa3014 --- /dev/null +++ b/mobile/lib/events/seekbar_triggered_event.dart @@ -0,0 +1,7 @@ +import "package:photos/events/event.dart"; + +class SeekbarTriggeredEvent extends Event { + final int position; + + SeekbarTriggeredEvent({required this.position}); +} diff --git a/mobile/lib/events/stream_switched_event.dart b/mobile/lib/events/stream_switched_event.dart new file mode 100644 index 0000000000..0716e6d18e --- /dev/null +++ b/mobile/lib/events/stream_switched_event.dart @@ -0,0 +1,10 @@ +import "package:photos/events/event.dart"; + +class StreamSwitchedEvent extends Event { + final bool selectedPreview; + final PlayerType type; + + StreamSwitchedEvent(this.selectedPreview, this.type); +} + +enum PlayerType { mediaKit, nativeVideoPlayer } diff --git a/mobile/lib/gateways/cast_gw.dart b/mobile/lib/gateways/cast_gw.dart index bc8af897ab..3db3894ad4 100644 --- a/mobile/lib/gateways/cast_gw.dart +++ b/mobile/lib/gateways/cast_gw.dart @@ -12,7 +12,7 @@ class CastGateway { ); return response.data["publicKey"]; } catch (e) { - if (e is DioError && e.response != null) { + if (e is DioException && e.response != null) { if (e.response!.statusCode == 404) { return null; } else if (e.response!.statusCode == 403) { diff --git a/mobile/lib/gateways/entity_gw.dart b/mobile/lib/gateways/entity_gw.dart index a88d31eeb9..0e4b8bb2bc 100644 --- a/mobile/lib/gateways/entity_gw.dart +++ b/mobile/lib/gateways/entity_gw.dart @@ -32,7 +32,7 @@ class EntityGateway { }, ); return EntityKey.fromMap(response.data); - } on DioError catch (e) { + } on DioException catch (e) { if (e.response != null && (e.response!.statusCode ?? 0) == 404) { throw EntityKeyNotFound(); } else { diff --git a/mobile/lib/generated/intl/messages_ar.dart b/mobile/lib/generated/intl/messages_ar.dart index 7a0264a378..1ce7ba622c 100644 --- a/mobile/lib/generated/intl/messages_ar.dart +++ b/mobile/lib/generated/intl/messages_ar.dart @@ -45,8 +45,6 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("ما من مفتاح استرداد؟"), "noRecoveryKeyNoDecryption": MessageLookupByLibrary.simpleMessage( "لا يمكن فك تشفير بياناتك دون كلمة المرور أو مفتاح الاسترداد بسبب طبيعة بروتوكول التشفير الخاص بنا من النهاية إلى النهاية"), - "orPickFromYourContacts": - MessageLookupByLibrary.simpleMessage("or pick from your contacts"), "recoverButton": MessageLookupByLibrary.simpleMessage("استرداد"), "recoverySuccessful": MessageLookupByLibrary.simpleMessage("نجح الاسترداد!"), diff --git a/mobile/lib/generated/intl/messages_be.dart b/mobile/lib/generated/intl/messages_be.dart index 19d365e183..a4a49d3b64 100644 --- a/mobile/lib/generated/intl/messages_be.dart +++ b/mobile/lib/generated/intl/messages_be.dart @@ -175,8 +175,6 @@ class MessageLookup extends MessageLookupByLibrary { "notifications": MessageLookupByLibrary.simpleMessage("Апавяшчэнні"), "ok": MessageLookupByLibrary.simpleMessage("Добра"), "oops": MessageLookupByLibrary.simpleMessage("Вой"), - "orPickFromYourContacts": - MessageLookupByLibrary.simpleMessage("or pick from your contacts"), "password": MessageLookupByLibrary.simpleMessage("Пароль"), "passwordChangedSuccessfully": MessageLookupByLibrary.simpleMessage("Пароль паспяхова зменены"), diff --git a/mobile/lib/generated/intl/messages_bg.dart b/mobile/lib/generated/intl/messages_bg.dart index 85a7ce6d64..e887127f40 100644 --- a/mobile/lib/generated/intl/messages_bg.dart +++ b/mobile/lib/generated/intl/messages_bg.dart @@ -21,8 +21,5 @@ class MessageLookup extends MessageLookupByLibrary { String get localeName => 'bg'; final messages = _notInlinedMessages(_notInlinedMessages); - static Map _notInlinedMessages(_) => { - "orPickFromYourContacts": - MessageLookupByLibrary.simpleMessage("or pick from your contacts") - }; + static Map _notInlinedMessages(_) => {}; } diff --git a/mobile/lib/generated/intl/messages_ca.dart b/mobile/lib/generated/intl/messages_ca.dart index 615d5c12da..84dea987b0 100644 --- a/mobile/lib/generated/intl/messages_ca.dart +++ b/mobile/lib/generated/intl/messages_ca.dart @@ -21,8 +21,5 @@ class MessageLookup extends MessageLookupByLibrary { String get localeName => 'ca'; final messages = _notInlinedMessages(_notInlinedMessages); - static Map _notInlinedMessages(_) => { - "orPickFromYourContacts": - MessageLookupByLibrary.simpleMessage("or pick from your contacts") - }; + static Map _notInlinedMessages(_) => {}; } diff --git a/mobile/lib/generated/intl/messages_cs.dart b/mobile/lib/generated/intl/messages_cs.dart index 024158d797..226e365e9c 100644 --- a/mobile/lib/generated/intl/messages_cs.dart +++ b/mobile/lib/generated/intl/messages_cs.dart @@ -26,8 +26,6 @@ class MessageLookup extends MessageLookupByLibrary { "Jaký je váš hlavní důvod, proč mažete svůj účet?"), "checkInboxAndSpamFolder": MessageLookupByLibrary.simpleMessage( "Zkontrolujte prosím svou doručenou poštu (a spam) pro dokončení ověření"), - "incorrectRecoveryKeyBody": MessageLookupByLibrary.simpleMessage(""), - "orPickFromYourContacts": - MessageLookupByLibrary.simpleMessage("or pick from your contacts") + "incorrectRecoveryKeyBody": MessageLookupByLibrary.simpleMessage("") }; } diff --git a/mobile/lib/generated/intl/messages_da.dart b/mobile/lib/generated/intl/messages_da.dart index dd67ae7466..8eef3e4a80 100644 --- a/mobile/lib/generated/intl/messages_da.dart +++ b/mobile/lib/generated/intl/messages_da.dart @@ -241,8 +241,6 @@ class MessageLookup extends MessageLookupByLibrary { "oops": MessageLookupByLibrary.simpleMessage("Ups"), "oopsSomethingWentWrong": MessageLookupByLibrary.simpleMessage("Ups, noget gik galt"), - "orPickFromYourContacts": - MessageLookupByLibrary.simpleMessage("or pick from your contacts"), "password": MessageLookupByLibrary.simpleMessage("Adgangskode"), "passwordChangedSuccessfully": MessageLookupByLibrary.simpleMessage( "Adgangskoden er blevet ændret"), diff --git a/mobile/lib/generated/intl/messages_de.dart b/mobile/lib/generated/intl/messages_de.dart index 8a87f776ad..d8210c8f63 100644 --- a/mobile/lib/generated/intl/messages_de.dart +++ b/mobile/lib/generated/intl/messages_de.dart @@ -20,37 +20,39 @@ typedef String MessageIfAbsent(String messageStr, List args); class MessageLookup extends MessageLookupByLibrary { String get localeName => 'de'; - static String m9(count) => - "${Intl.plural(count, one: 'Teilnehmer', other: 'Teilnehmer')} hinzufügen"; + static String m9(title) => "${title} (Ich)"; static String m10(count) => + "${Intl.plural(count, one: 'Teilnehmer', other: 'Teilnehmer')} hinzufügen"; + + static String m11(count) => "${Intl.plural(count, one: 'Element hinzufügen', other: 'Elemente hinzufügen')}"; - static String m11(storageAmount, endDate) => + static String m12(storageAmount, endDate) => "Dein ${storageAmount} Add-on ist gültig bis ${endDate}"; - static String m12(count) => + static String m13(count) => "${Intl.plural(count, one: 'Betrachter', other: 'Betrachter')} hinzufügen"; - static String m13(emailOrName) => "Von ${emailOrName} hinzugefügt"; + static String m14(emailOrName) => "Von ${emailOrName} hinzugefügt"; - static String m14(albumName) => "Erfolgreich zu ${albumName} hinzugefügt"; + static String m15(albumName) => "Erfolgreich zu ${albumName} hinzugefügt"; - static String m15(count) => + static String m16(count) => "${Intl.plural(count, zero: 'Keine Teilnehmer', one: '1 Teilnehmer', other: '${count} Teilnehmer')}"; - static String m16(versionValue) => "Version: ${versionValue}"; + static String m17(versionValue) => "Version: ${versionValue}"; - static String m17(freeAmount, storageUnit) => + static String m18(freeAmount, storageUnit) => "${freeAmount} ${storageUnit} frei"; - static String m18(paymentProvider) => + static String m19(paymentProvider) => "Bitte kündige dein aktuelles Abo über ${paymentProvider} zuerst"; static String m3(user) => "Der Nutzer \"${user}\" wird keine weiteren Fotos zum Album hinzufügen können.\n\nJedoch kann er weiterhin vorhandene Bilder, welche durch ihn hinzugefügt worden sind, wieder entfernen"; - static String m19(isFamilyMember, storageAmountInGb) => + static String m20(isFamilyMember, storageAmountInGb) => "${Intl.select(isFamilyMember, { 'true': 'Deine Familiengruppe hat bereits ${storageAmountInGb} GB erhalten', @@ -58,213 +60,222 @@ class MessageLookup extends MessageLookupByLibrary { 'other': 'Du hast bereits ${storageAmountInGb} GB erhalten!', })}"; - static String m20(albumName) => + static String m21(albumName) => "Kollaborativer Link für ${albumName} erstellt"; - static String m21(count) => + static String m22(count) => "${Intl.plural(count, zero: '0 Mitarbeiter hinzugefügt', one: '1 Mitarbeiter hinzugefügt', other: '${count} Mitarbeiter hinzugefügt')}"; - static String m22(email, numOfDays) => + static String m23(email, numOfDays) => "Du bist dabei, ${email} als vertrauenswürdigen Kontakt hinzuzufügen. Die Person wird in der Lage sein, dein Konto wiederherzustellen, wenn du für ${numOfDays} Tage abwesend bist."; - static String m23(familyAdminEmail) => + static String m24(familyAdminEmail) => "Bitte kontaktiere ${familyAdminEmail} um dein Abo zu verwalten"; - static String m24(provider) => + static String m25(provider) => "Bitte kontaktiere uns über support@ente.io, um dein ${provider} Abo zu verwalten."; - static String m25(endpoint) => "Verbunden mit ${endpoint}"; + static String m26(endpoint) => "Verbunden mit ${endpoint}"; - static String m26(count) => + static String m27(count) => "${Intl.plural(count, one: 'Lösche ${count} Element', other: 'Lösche ${count} Elemente')}"; - static String m27(currentlyDeleting, totalCount) => + static String m28(currentlyDeleting, totalCount) => "Lösche ${currentlyDeleting} / ${totalCount}"; - static String m28(albumName) => + static String m29(albumName) => "Der öffentliche Link zum Zugriff auf \"${albumName}\" wird entfernt."; - static String m29(supportEmail) => + static String m30(supportEmail) => "Bitte sende eine E-Mail an ${supportEmail} von deiner registrierten E-Mail-Adresse"; - static String m30(count, storageSaved) => + static String m31(count, storageSaved) => "Du hast ${Intl.plural(count, one: '${count} duplizierte Datei', other: '${count} dupliziere Dateien')} gelöscht und (${storageSaved}!) freigegeben"; - static String m31(count, formattedSize) => + static String m32(count, formattedSize) => "${count} Dateien, ${formattedSize} jede"; - static String m32(newEmail) => "E-Mail-Adresse geändert zu ${newEmail}"; + static String m33(newEmail) => "E-Mail-Adresse geändert zu ${newEmail}"; - static String m33(email) => + static String m34(email) => "${email} hat kein Ente-Konto."; + + static String m35(email) => "${email} hat kein Ente-Konto.\n\nSende eine Einladung, um Fotos zu teilen."; - static String m34(text) => "Zusätzliche Fotos für ${text} gefunden"; + static String m36(text) => "Zusätzliche Fotos für ${text} gefunden"; - static String m35(count, formattedNumber) => + static String m37(count, formattedNumber) => "${Intl.plural(count, one: '1 Datei', other: '${formattedNumber} Dateien')} auf diesem Gerät wurde(n) sicher gespeichert"; - static String m36(count, formattedNumber) => + static String m38(count, formattedNumber) => "${Intl.plural(count, one: '1 Datei', other: '${formattedNumber} Dateien')} in diesem Album wurde(n) sicher gespeichert"; static String m4(storageAmountInGB) => "${storageAmountInGB} GB jedes Mal, wenn sich jemand mit deinem Code für einen bezahlten Tarif anmeldet"; - static String m37(endDate) => "Kostenlose Demo verfügbar bis zum ${endDate}"; + static String m39(endDate) => "Kostenlose Demo verfügbar bis zum ${endDate}"; - static String m38(count) => + static String m40(count) => "Du kannst immernoch über Ente ${Intl.plural(count, one: 'darauf', other: 'auf sie')} zugreifen, solange du ein aktives Abo hast"; - static String m39(sizeInMBorGB) => "${sizeInMBorGB} freigeben"; + static String m41(sizeInMBorGB) => "${sizeInMBorGB} freigeben"; - static String m40(count, formattedSize) => + static String m42(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 m41(currentlyProcessing, totalCount) => + static String m43(currentlyProcessing, totalCount) => "Verarbeite ${currentlyProcessing} / ${totalCount}"; - static String m42(count) => + static String m44(count) => "${Intl.plural(count, one: '${count} Objekt', other: '${count} Objekte')}"; - static String m43(email) => + static String m45(email) => "${email} hat dich eingeladen, ein vertrauenswürdiger Kontakt zu werden"; - static String m44(expiryTime) => "Link läuft am ${expiryTime} ab"; + static String m46(expiryTime) => "Link läuft am ${expiryTime} ab"; + + static String m47(email) => "Person mit ${email} verknüpfen"; + + static String m48(personName, email) => + "Dies wird ${personName} mit ${email} verknüpfen"; static String m5(count, formattedCount) => "${Intl.plural(count, zero: 'keine Erinnerungsstücke', one: '${formattedCount} Erinnerung', other: '${formattedCount} Erinnerungsstücke')}"; - static String m45(count) => + static String m49(count) => "${Intl.plural(count, one: 'Element verschieben', other: 'Elemente verschieben')}"; - static String m46(albumName) => "Erfolgreich zu ${albumName} hinzugefügt"; + static String m50(albumName) => "Erfolgreich zu ${albumName} hinzugefügt"; - static String m47(personName) => "Keine Vorschläge für ${personName}"; + static String m51(personName) => "Keine Vorschläge für ${personName}"; - static String m48(name) => "Nicht ${name}?"; + static String m52(name) => "Nicht ${name}?"; - static String m49(familyAdminEmail) => + static String m53(familyAdminEmail) => "Bitte wende Dich an ${familyAdminEmail}, um den Code zu ändern."; static String m0(passwordStrengthValue) => "Passwortstärke: ${passwordStrengthValue}"; - static String m50(providerName) => + static String m54(providerName) => "Bitte kontaktiere den Support von ${providerName}, falls etwas abgebucht wurde"; - static String m51(count) => + static String m55(count) => "${Intl.plural(count, zero: '0 Fotos', one: '1 Foto', other: '${count} Fotos')}"; - static String m52(endDate) => + static String m56(endDate) => "Kostenlose Testversion gültig bis ${endDate}.\nDu kannst anschließend ein bezahltes Paket auswählen."; - static String m53(toEmail) => "Bitte sende uns eine E-Mail an ${toEmail}"; + static String m57(toEmail) => "Bitte sende uns eine E-Mail an ${toEmail}"; - static String m54(toEmail) => "Bitte sende die Protokolle an ${toEmail}"; + static String m58(toEmail) => "Bitte sende die Protokolle an ${toEmail}"; - static String m55(folderName) => "Verarbeite ${folderName}..."; + static String m59(folderName) => "Verarbeite ${folderName}..."; - static String m56(storeName) => "Bewerte uns auf ${storeName}"; + static String m60(storeName) => "Bewerte uns auf ${storeName}"; - static String m57(days, email) => + static String m61(name) => "Du wurdest an ${name} neu zugewiesen"; + + static String m62(days, email) => "Du kannst nach ${days} Tagen auf das Konto zugreifen. Eine Benachrichtigung wird an ${email} versendet."; - static String m58(email) => + static String m63(email) => "Du kannst jetzt das Konto von ${email} wiederherstellen, indem du ein neues Passwort setzt."; - static String m59(email) => + static String m64(email) => "${email} versucht, dein Konto wiederherzustellen."; - static String m60(storageInGB) => + static String m65(storageInGB) => "3. Ihr beide erhaltet ${storageInGB} GB* kostenlos"; - static String m61(userEmail) => + static String m66(userEmail) => "${userEmail} wird aus diesem geteilten Album entfernt\n\nAlle von ihnen hinzugefügte Fotos werden ebenfalls aus dem Album entfernt"; - static String m62(endDate) => "Erneuert am ${endDate}"; + static String m67(endDate) => "Erneuert am ${endDate}"; - static String m63(count) => + static String m68(count) => "${Intl.plural(count, one: '${count} Ergebnis gefunden', other: '${count} Ergebnisse gefunden')}"; - static String m64(snapshotLength, searchLength) => + static String m69(snapshotLength, searchLength) => "Abschnittslänge stimmt nicht überein: ${snapshotLength} != ${searchLength}"; static String m6(count) => "${count} ausgewählt"; - static String m65(count, yourCount) => + static String m70(count, yourCount) => "${count} ausgewählt (${yourCount} von Ihnen)"; - static String m66(verificationID) => + static String m71(verificationID) => "Hier ist meine Verifizierungs-ID: ${verificationID} für ente.io."; static String m7(verificationID) => "Hey, kannst du bestätigen, dass dies deine ente.io Verifizierungs-ID ist: ${verificationID}"; - static String m67(referralCode, referralStorageInGB) => + static String m72(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 m68(numberOfPeople) => + static String m73(numberOfPeople) => "${Intl.plural(numberOfPeople, zero: 'Teile mit bestimmten Personen', one: 'Teilen mit 1 Person', other: 'Teilen mit ${numberOfPeople} Personen')}"; - static String m69(emailIDs) => "Geteilt mit ${emailIDs}"; + static String m74(emailIDs) => "Geteilt mit ${emailIDs}"; - static String m70(fileType) => + static String m75(fileType) => "Dieses ${fileType} wird von deinem Gerät gelöscht."; - static String m71(fileType) => + static String m76(fileType) => "Diese Datei ist sowohl in Ente als auch auf deinem Gerät."; - static String m72(fileType) => "Diese Datei wird von Ente gelöscht."; + static String m77(fileType) => "Diese Datei wird von Ente gelöscht."; static String m1(storageAmountInGB) => "${storageAmountInGB} GB"; - static String m73( + static String m78( usedAmount, usedStorageUnit, totalAmount, totalStorageUnit) => "${usedAmount} ${usedStorageUnit} von ${totalAmount} ${totalStorageUnit} verwendet"; - static String m74(id) => + static String m79(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 m75(endDate) => "Dein Abo endet am ${endDate}"; + static String m80(endDate) => "Dein Abo endet am ${endDate}"; - static String m76(completed, total) => + static String m81(completed, total) => "${completed}/${total} Erinnerungsstücke gesichert"; - static String m77(ignoreReason) => + static String m82(ignoreReason) => "Zum Hochladen tippen, Hochladen wird derzeit ignoriert, da ${ignoreReason}"; static String m8(storageAmountInGB) => "Diese erhalten auch ${storageAmountInGB} GB"; - static String m78(email) => "Dies ist ${email}s Verifizierungs-ID"; + static String m83(email) => "Dies ist ${email}s Verifizierungs-ID"; - static String m79(count) => + static String m84(count) => "${Intl.plural(count, zero: 'Demnächst', one: '1 Tag', other: '${count} Tage')}"; - static String m80(email) => + static String m85(email) => "Du wurdest von ${email} eingeladen, ein Kontakt für das digitale Erbe zu werden."; - static String m81(galleryType) => + static String m86(galleryType) => "Der Galerie-Typ ${galleryType} unterstützt kein Umbenennen"; - static String m82(ignoreReason) => + static String m87(ignoreReason) => "Upload wird aufgrund von ${ignoreReason} ignoriert"; - static String m83(count) => "Sichere ${count} Erinnerungsstücke..."; + static String m88(count) => "Sichere ${count} Erinnerungsstücke..."; - static String m84(endDate) => "Gültig bis ${endDate}"; + static String m89(endDate) => "Gültig bis ${endDate}"; - static String m85(email) => "Verifiziere ${email}"; + static String m90(email) => "Verifiziere ${email}"; - static String m86(count) => + static String m91(count) => "${Intl.plural(count, zero: '0 Betrachter hinzugefügt', one: '1 Betrachter hinzugefügt', other: '${count} Betrachter hinzugefügt')}"; static String m2(email) => "Wir haben eine E-Mail an ${email} gesendet"; - static String m87(count) => + static String m92(count) => "${Intl.plural(count, one: 'vor einem Jahr', other: 'vor ${count} Jahren')}"; - static String m88(storageSaved) => + static String m93(storageSaved) => "Du hast ${storageSaved} erfolgreich freigegeben!"; final messages = _notInlinedMessages(_notInlinedMessages); @@ -278,6 +289,7 @@ class MessageLookup extends MessageLookupByLibrary { "account": MessageLookupByLibrary.simpleMessage("Konto"), "accountIsAlreadyConfigured": MessageLookupByLibrary.simpleMessage( "Das Konto ist bereits konfiguriert."), + "accountOwnerPersonAppbarTitle": m9, "accountWelcomeBack": MessageLookupByLibrary.simpleMessage("Willkommen zurück!"), "ackPasswordLostWarning": MessageLookupByLibrary.simpleMessage( @@ -291,11 +303,11 @@ class MessageLookup extends MessageLookupByLibrary { "Neue E-Mail-Adresse hinzufügen"), "addCollaborator": MessageLookupByLibrary.simpleMessage("Bearbeiter hinzufügen"), - "addCollaborators": m9, + "addCollaborators": m10, "addFiles": MessageLookupByLibrary.simpleMessage("Dateien hinzufügen"), "addFromDevice": MessageLookupByLibrary.simpleMessage("Vom Gerät hinzufügen"), - "addItem": m10, + "addItem": m11, "addLocation": MessageLookupByLibrary.simpleMessage("Ort hinzufügen"), "addLocationButton": MessageLookupByLibrary.simpleMessage("Hinzufügen"), "addMore": MessageLookupByLibrary.simpleMessage("Mehr hinzufügen"), @@ -307,7 +319,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Neue Person hinzufügen"), "addOnPageSubtitle": MessageLookupByLibrary.simpleMessage("Details der Add-ons"), - "addOnValidTill": m11, + "addOnValidTill": m12, "addOns": MessageLookupByLibrary.simpleMessage("Add-ons"), "addPhotos": MessageLookupByLibrary.simpleMessage("Fotos hinzufügen"), "addSelected": @@ -320,12 +332,12 @@ class MessageLookup extends MessageLookupByLibrary { "addTrustedContact": MessageLookupByLibrary.simpleMessage( "Vertrauenswürdigen Kontakt hinzufügen"), "addViewer": MessageLookupByLibrary.simpleMessage("Album teilen"), - "addViewers": m12, + "addViewers": m13, "addYourPhotosNow": MessageLookupByLibrary.simpleMessage("Füge deine Foto jetzt hinzu"), "addedAs": MessageLookupByLibrary.simpleMessage("Hinzugefügt als"), - "addedBy": m13, - "addedSuccessfullyTo": m14, + "addedBy": m14, + "addedSuccessfullyTo": m15, "addingToFavorites": MessageLookupByLibrary.simpleMessage( "Wird zu Favoriten hinzugefügt..."), "advanced": MessageLookupByLibrary.simpleMessage("Erweitert"), @@ -336,7 +348,7 @@ class MessageLookup extends MessageLookupByLibrary { "after1Week": MessageLookupByLibrary.simpleMessage("Nach 1 Woche"), "after1Year": MessageLookupByLibrary.simpleMessage("Nach 1 Jahr"), "albumOwner": MessageLookupByLibrary.simpleMessage("Besitzer"), - "albumParticipantsCount": m15, + "albumParticipantsCount": m16, "albumTitle": MessageLookupByLibrary.simpleMessage("Albumtitel"), "albumUpdated": MessageLookupByLibrary.simpleMessage("Album aktualisiert"), @@ -386,7 +398,7 @@ class MessageLookup extends MessageLookupByLibrary { "appLock": MessageLookupByLibrary.simpleMessage("App-Sperre"), "appLockDescriptions": MessageLookupByLibrary.simpleMessage( "Wähle zwischen dem Standard-Sperrbildschirm deines Gerätes und einem eigenen Sperrbildschirm mit PIN oder Passwort."), - "appVersion": m16, + "appVersion": m17, "appleId": MessageLookupByLibrary.simpleMessage("Apple ID"), "apply": MessageLookupByLibrary.simpleMessage("Anwenden"), "applyCodeTitle": MessageLookupByLibrary.simpleMessage("Code nutzen"), @@ -470,7 +482,7 @@ class MessageLookup extends MessageLookupByLibrary { "autoPairDesc": MessageLookupByLibrary.simpleMessage( "Automatisches Verbinden funktioniert nur mit Geräten, die Chromecast unterstützen."), "available": MessageLookupByLibrary.simpleMessage("Verfügbar"), - "availableStorageSpace": m17, + "availableStorageSpace": m18, "backedUpFolders": MessageLookupByLibrary.simpleMessage("Gesicherte Ordner"), "backup": MessageLookupByLibrary.simpleMessage("Backup"), @@ -510,7 +522,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Wiederherstellung abbrechen"), "cancelAccountRecoveryBody": MessageLookupByLibrary.simpleMessage( "Bist du sicher, dass du die Wiederherstellung abbrechen möchtest?"), - "cancelOtherSubscription": m18, + "cancelOtherSubscription": m19, "cancelSubscription": MessageLookupByLibrary.simpleMessage("Abonnement kündigen"), "cannotAddMorePhotosAfterBecomingViewer": m3, @@ -564,7 +576,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Freien Speicher einlösen"), "claimMore": MessageLookupByLibrary.simpleMessage("Mehr einlösen!"), "claimed": MessageLookupByLibrary.simpleMessage("Eingelöst"), - "claimedStorageSoFar": m19, + "claimedStorageSoFar": m20, "cleanUncategorized": MessageLookupByLibrary.simpleMessage("Unkategorisiert leeren"), "cleanUncategorizedDescription": MessageLookupByLibrary.simpleMessage( @@ -593,12 +605,12 @@ class MessageLookup extends MessageLookupByLibrary { "Erstelle einen Link, mit dem andere Fotos in dem geteilten Album sehen und selbst welche hinzufügen können - ohne dass sie die ein Ente-Konto oder die App benötigen. Ideal um gemeinsam Fotos von Events zu sammeln."), "collaborativeLink": MessageLookupByLibrary.simpleMessage("Gemeinschaftlicher Link"), - "collaborativeLinkCreatedFor": m20, + "collaborativeLinkCreatedFor": m21, "collaborator": MessageLookupByLibrary.simpleMessage("Bearbeiter"), "collaboratorsCanAddPhotosAndVideosToTheSharedAlbum": MessageLookupByLibrary.simpleMessage( "Bearbeiter können Fotos & Videos zu dem geteilten Album hinzufügen."), - "collaboratorsSuccessfullyAdded": m21, + "collaboratorsSuccessfullyAdded": m22, "collageLayout": MessageLookupByLibrary.simpleMessage("Layout"), "collageSaved": MessageLookupByLibrary.simpleMessage( "Collage in Galerie gespeichert"), @@ -615,7 +627,7 @@ class MessageLookup extends MessageLookupByLibrary { "Bist du sicher, dass du die Zwei-Faktor-Authentifizierung (2FA) deaktivieren willst?"), "confirmAccountDeletion": MessageLookupByLibrary.simpleMessage("Kontolöschung bestätigen"), - "confirmAddingTrustedContact": m22, + "confirmAddingTrustedContact": m23, "confirmDeletePrompt": MessageLookupByLibrary.simpleMessage( "Ja, ich möchte dieses Konto und alle enthaltenen Daten über alle Apps hinweg endgültig löschen."), "confirmPassword": @@ -628,10 +640,10 @@ class MessageLookup extends MessageLookupByLibrary { "Bestätige deinen Wiederherstellungsschlüssel"), "connectToDevice": MessageLookupByLibrary.simpleMessage("Mit Gerät verbinden"), - "contactFamilyAdmin": m23, + "contactFamilyAdmin": m24, "contactSupport": MessageLookupByLibrary.simpleMessage("Support kontaktieren"), - "contactToManageSubscription": m24, + "contactToManageSubscription": m25, "contacts": MessageLookupByLibrary.simpleMessage("Kontakte"), "contents": MessageLookupByLibrary.simpleMessage("Inhalte"), "continueLabel": MessageLookupByLibrary.simpleMessage("Weiter"), @@ -679,7 +691,7 @@ class MessageLookup extends MessageLookupByLibrary { "currentlyRunning": MessageLookupByLibrary.simpleMessage("läuft gerade"), "custom": MessageLookupByLibrary.simpleMessage("Benutzerdefiniert"), - "customEndpoint": m25, + "customEndpoint": m26, "darkTheme": MessageLookupByLibrary.simpleMessage("Dunkel"), "dayToday": MessageLookupByLibrary.simpleMessage("Heute"), "dayYesterday": MessageLookupByLibrary.simpleMessage("Gestern"), @@ -717,11 +729,11 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Vom Gerät löschen"), "deleteFromEnte": MessageLookupByLibrary.simpleMessage("Von Ente löschen"), - "deleteItemCount": m26, + "deleteItemCount": m27, "deleteLocation": MessageLookupByLibrary.simpleMessage("Standort löschen"), "deletePhotos": MessageLookupByLibrary.simpleMessage("Fotos löschen"), - "deleteProgress": m27, + "deleteProgress": m28, "deleteReason1": MessageLookupByLibrary.simpleMessage( "Es fehlt eine zentrale Funktion, die ich benötige"), "deleteReason2": MessageLookupByLibrary.simpleMessage( @@ -759,7 +771,7 @@ class MessageLookup extends MessageLookupByLibrary { "Zuschauer können weiterhin Screenshots oder mit anderen externen Programmen Kopien der Bilder machen."), "disableDownloadWarningTitle": MessageLookupByLibrary.simpleMessage("Bitte beachten Sie:"), - "disableLinkMessage": m28, + "disableLinkMessage": m29, "disableTwofactor": MessageLookupByLibrary.simpleMessage( "Zweiten Faktor (2FA) deaktivieren"), "disablingTwofactorAuthentication": @@ -795,6 +807,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage( "Möchtest du deine Änderungen verwerfen?"), "done": MessageLookupByLibrary.simpleMessage("Fertig"), + "dontSave": MessageLookupByLibrary.simpleMessage("Nicht speichern"), "doubleYourStorage": MessageLookupByLibrary.simpleMessage("Speicherplatz verdoppeln"), "download": MessageLookupByLibrary.simpleMessage("Herunterladen"), @@ -802,9 +815,9 @@ class MessageLookup extends MessageLookupByLibrary { "Herunterladen fehlgeschlagen"), "downloading": MessageLookupByLibrary.simpleMessage("Wird heruntergeladen..."), - "dropSupportEmail": m29, - "duplicateFileCountWithStorageSaved": m30, - "duplicateItemsGroup": m31, + "dropSupportEmail": m30, + "duplicateFileCountWithStorageSaved": m31, + "duplicateItemsGroup": m32, "edit": MessageLookupByLibrary.simpleMessage("Bearbeiten"), "editLocation": MessageLookupByLibrary.simpleMessage("Standort bearbeiten"), @@ -820,8 +833,9 @@ class MessageLookup extends MessageLookupByLibrary { "email": MessageLookupByLibrary.simpleMessage("E-Mail"), "emailAlreadyRegistered": MessageLookupByLibrary.simpleMessage( "E-Mail ist bereits registriert."), - "emailChangedTo": m32, - "emailNoEnteAccount": m33, + "emailChangedTo": m33, + "emailDoesNotHaveEnteAccount": m34, + "emailNoEnteAccount": m35, "emailNotRegistered": MessageLookupByLibrary.simpleMessage("E-Mail nicht registriert."), "emailVerificationToggle": @@ -906,12 +920,13 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Daten exportieren"), "extraPhotosFound": MessageLookupByLibrary.simpleMessage("Zusätzliche Fotos gefunden"), - "extraPhotosFoundFor": m34, + "extraPhotosFoundFor": m36, "faceNotClusteredYet": MessageLookupByLibrary.simpleMessage( "Gesicht ist noch nicht gruppiert, bitte komm später zurück"), "faceRecognition": MessageLookupByLibrary.simpleMessage("Gesichtserkennung"), "faces": MessageLookupByLibrary.simpleMessage("Gesichter"), + "failed": MessageLookupByLibrary.simpleMessage("Fehlgeschlagen"), "failedToApplyCode": MessageLookupByLibrary.simpleMessage( "Der Code konnte nicht aktiviert werden"), "failedToCancel": @@ -956,8 +971,8 @@ class MessageLookup extends MessageLookupByLibrary { "fileTypes": MessageLookupByLibrary.simpleMessage("Dateitypen"), "fileTypesAndNames": MessageLookupByLibrary.simpleMessage("Dateitypen und -namen"), - "filesBackedUpFromDevice": m35, - "filesBackedUpInAlbum": m36, + "filesBackedUpFromDevice": m37, + "filesBackedUpInAlbum": m38, "filesDeleted": MessageLookupByLibrary.simpleMessage("Dateien gelöscht"), "filesSavedToGallery": MessageLookupByLibrary.simpleMessage( @@ -980,23 +995,23 @@ class MessageLookup extends MessageLookupByLibrary { "Freier Speicherplatz nutzbar"), "freeTrial": MessageLookupByLibrary.simpleMessage("Kostenlose Testphase"), - "freeTrialValidTill": m37, - "freeUpAccessPostDelete": m38, - "freeUpAmount": m39, + "freeTrialValidTill": m39, + "freeUpAccessPostDelete": m40, + "freeUpAmount": m41, "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": m40, + "freeUpSpaceSaving": m42, "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": m41, + "genericProgress": m43, "goToSettings": MessageLookupByLibrary.simpleMessage("Zu den Einstellungen"), "googlePlayId": MessageLookupByLibrary.simpleMessage("Google Play ID"), @@ -1057,6 +1072,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Indizierte Elemente"), "indexingIsPaused": MessageLookupByLibrary.simpleMessage( "Die Indizierung ist unterbrochen. Sie wird automatisch fortgesetzt, wenn das Gerät bereit ist."), + "ineligible": MessageLookupByLibrary.simpleMessage("Unzulässig"), "info": MessageLookupByLibrary.simpleMessage("Info"), "insecureDevice": MessageLookupByLibrary.simpleMessage("Unsicheres Gerät"), @@ -1082,7 +1098,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": m44, "itemsShowTheNumberOfDaysRemainingBeforePermanentDeletion": MessageLookupByLibrary.simpleMessage( "Elemente zeigen die Anzahl der Tage bis zum dauerhaften Löschen an"), @@ -1090,6 +1106,8 @@ class MessageLookup extends MessageLookupByLibrary { "Ausgewählte Elemente werden aus diesem Album entfernt"), "join": MessageLookupByLibrary.simpleMessage("Beitreten"), "joinAlbum": MessageLookupByLibrary.simpleMessage("Album beitreten"), + "joinAlbumConfirmationDialogBody": MessageLookupByLibrary.simpleMessage( + "Wenn du einem Album beitrittst, wird deine E-Mail-Adresse für seine Teilnehmer sichtbar."), "joinAlbumSubtext": MessageLookupByLibrary.simpleMessage( "um deine Fotos anzuzeigen und hinzuzufügen"), "joinAlbumSubtextViewer": MessageLookupByLibrary.simpleMessage( @@ -1113,25 +1131,34 @@ class MessageLookup extends MessageLookupByLibrary { "legacy": MessageLookupByLibrary.simpleMessage("Digitales Erbe"), "legacyAccounts": MessageLookupByLibrary.simpleMessage("Digital geerbte Konten"), - "legacyInvite": m43, + "legacyInvite": m45, "legacyPageDesc": MessageLookupByLibrary.simpleMessage( "Das digitale Erbe erlaubt vertrauenswürdigen Kontakten den Zugriff auf dein Konto in deiner Abwesenheit."), "legacyPageDesc2": MessageLookupByLibrary.simpleMessage( "Vertrauenswürdige Kontakte können eine Kontowiederherstellung einleiten und, wenn dies nicht innerhalb von 30 Tagen blockiert wird, dein Passwort und den Kontozugriff zurücksetzen."), "light": MessageLookupByLibrary.simpleMessage("Hell"), "lightTheme": MessageLookupByLibrary.simpleMessage("Hell"), - "link": MessageLookupByLibrary.simpleMessage("Link"), + "link": MessageLookupByLibrary.simpleMessage("Verknüpfen"), "linkCopiedToClipboard": MessageLookupByLibrary.simpleMessage( "Link in Zwischenablage kopiert"), "linkDeviceLimit": MessageLookupByLibrary.simpleMessage("Geräte-Limit"), + "linkEmail": + MessageLookupByLibrary.simpleMessage("E-Mail-Adresse verknüpfen"), + "linkEmailToContactBannerCaption": + MessageLookupByLibrary.simpleMessage("für schnelleres Teilen"), "linkEnabled": MessageLookupByLibrary.simpleMessage("Aktiviert"), "linkExpired": MessageLookupByLibrary.simpleMessage("Abgelaufen"), - "linkExpiresOn": m44, + "linkExpiresOn": m46, "linkExpiry": MessageLookupByLibrary.simpleMessage("Ablaufdatum des Links"), "linkHasExpired": MessageLookupByLibrary.simpleMessage("Link ist abgelaufen"), "linkNeverExpires": MessageLookupByLibrary.simpleMessage("Niemals"), + "linkPerson": MessageLookupByLibrary.simpleMessage("Person verknüpfen"), + "linkPersonCaption": MessageLookupByLibrary.simpleMessage( + "um besseres Teilen zu ermöglichen"), + "linkPersonToEmail": m47, + "linkPersonToEmailConfirmation": m48, "livePhotos": MessageLookupByLibrary.simpleMessage("Live-Fotos"), "loadMessage1": MessageLookupByLibrary.simpleMessage( "Du kannst dein Abonnement mit deiner Familie teilen"), @@ -1217,6 +1244,7 @@ class MessageLookup extends MessageLookupByLibrary { "maps": MessageLookupByLibrary.simpleMessage("Karten"), "mastodon": MessageLookupByLibrary.simpleMessage("Mastodon"), "matrix": MessageLookupByLibrary.simpleMessage("Matrix"), + "me": MessageLookupByLibrary.simpleMessage("Ich"), "memoryCount": m5, "merchandise": MessageLookupByLibrary.simpleMessage("Merchandise"), "mergeWithExisting": MessageLookupByLibrary.simpleMessage( @@ -1247,12 +1275,12 @@ class MessageLookup extends MessageLookupByLibrary { "moreDetails": MessageLookupByLibrary.simpleMessage("Weitere Details"), "mostRecent": MessageLookupByLibrary.simpleMessage("Neuste"), "mostRelevant": MessageLookupByLibrary.simpleMessage("Nach Relevanz"), - "moveItem": m45, + "moveItem": m49, "moveToAlbum": MessageLookupByLibrary.simpleMessage("Zum Album verschieben"), "moveToHiddenAlbum": MessageLookupByLibrary.simpleMessage( "Zu verstecktem Album verschieben"), - "movedSuccessfullyTo": m46, + "movedSuccessfullyTo": m50, "movedToTrash": MessageLookupByLibrary.simpleMessage( "In den Papierkorb verschoben"), "movingFilesToAlbum": MessageLookupByLibrary.simpleMessage( @@ -1305,10 +1333,10 @@ class MessageLookup extends MessageLookupByLibrary { "noResults": MessageLookupByLibrary.simpleMessage("Keine Ergebnisse"), "noResultsFound": MessageLookupByLibrary.simpleMessage("Keine Ergebnisse gefunden"), - "noSuggestionsForPerson": m47, + "noSuggestionsForPerson": m51, "noSystemLockFound": MessageLookupByLibrary.simpleMessage("Keine Systemsperre gefunden"), - "notPersonLabel": m48, + "notPersonLabel": m52, "nothingSharedWithYouYet": MessageLookupByLibrary.simpleMessage("Noch nichts mit Dir geteilt"), "nothingToSeeHere": MessageLookupByLibrary.simpleMessage( @@ -1319,7 +1347,7 @@ class MessageLookup extends MessageLookupByLibrary { "onDevice": MessageLookupByLibrary.simpleMessage("Auf dem Gerät"), "onEnte": MessageLookupByLibrary.simpleMessage( "Auf ente"), - "onlyFamilyAdminCanChangeCode": m49, + "onlyFamilyAdminCanChangeCode": m53, "onlyThem": MessageLookupByLibrary.simpleMessage("Nur diese"), "oops": MessageLookupByLibrary.simpleMessage("Hoppla"), "oopsCouldNotSaveEdits": MessageLookupByLibrary.simpleMessage( @@ -1342,8 +1370,6 @@ class MessageLookup extends MessageLookupByLibrary { "Oder mit existierenden zusammenführen"), "orPickAnExistingOne": MessageLookupByLibrary.simpleMessage( "Oder eine vorherige auswählen"), - "orPickFromYourContacts": - MessageLookupByLibrary.simpleMessage("or pick from your contacts"), "pair": MessageLookupByLibrary.simpleMessage("Koppeln"), "pairWithPin": MessageLookupByLibrary.simpleMessage("Mit PIN verbinden"), @@ -1369,7 +1395,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": m50, + "paymentFailedTalkToProvider": m54, "pendingItems": MessageLookupByLibrary.simpleMessage("Ausstehende Elemente"), "pendingSync": @@ -1393,14 +1419,17 @@ class MessageLookup extends MessageLookupByLibrary { "photosAddedByYouWillBeRemovedFromTheAlbum": MessageLookupByLibrary.simpleMessage( "Von dir hinzugefügte Fotos werden vom Album entfernt"), - "photosCount": m51, + "photosCount": m55, "pickCenterPoint": MessageLookupByLibrary.simpleMessage("Mittelpunkt auswählen"), "pinAlbum": MessageLookupByLibrary.simpleMessage("Album anheften"), "pinLock": MessageLookupByLibrary.simpleMessage("PIN-Sperre"), "playOnTv": MessageLookupByLibrary.simpleMessage( "Album auf dem Fernseher wiedergeben"), - "playStoreFreeTrialValidTill": m52, + "playOriginal": + MessageLookupByLibrary.simpleMessage("Original abspielen"), + "playStoreFreeTrialValidTill": m56, + "playStream": MessageLookupByLibrary.simpleMessage("Stream abspielen"), "playstoreSubscription": MessageLookupByLibrary.simpleMessage("PlayStore Abo"), "pleaseCheckYourInternetConnectionAndTryAgain": @@ -1412,14 +1441,14 @@ class MessageLookup extends MessageLookupByLibrary { "pleaseContactSupportIfTheProblemPersists": MessageLookupByLibrary.simpleMessage( "Bitte wenden Sie sich an den Support, falls das Problem weiterhin besteht"), - "pleaseEmailUsAt": m53, + "pleaseEmailUsAt": m57, "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": m54, + "pleaseSendTheLogsTo": m58, "pleaseTryAgain": MessageLookupByLibrary.simpleMessage("Bitte versuche es erneut"), "pleaseVerifyTheCodeYouHaveEntered": @@ -1448,17 +1477,26 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Privates Teilen"), "proceed": MessageLookupByLibrary.simpleMessage("Fortfahren"), "processed": MessageLookupByLibrary.simpleMessage("Verarbeitet"), - "processingImport": m55, + "processing": MessageLookupByLibrary.simpleMessage("In Bearbeitung"), + "processingImport": m59, + "processingVideos": + MessageLookupByLibrary.simpleMessage("Verarbeite Videos"), "publicLinkCreated": MessageLookupByLibrary.simpleMessage("Öffentlicher Link erstellt"), "publicLinkEnabled": MessageLookupByLibrary.simpleMessage("Öffentlicher Link aktiviert"), + "queued": MessageLookupByLibrary.simpleMessage("In der Warteschlange"), "quickLinks": MessageLookupByLibrary.simpleMessage("Quick Links"), "radius": MessageLookupByLibrary.simpleMessage("Umkreis"), "raiseTicket": MessageLookupByLibrary.simpleMessage("Ticket erstellen"), "rateTheApp": MessageLookupByLibrary.simpleMessage("App bewerten"), "rateUs": MessageLookupByLibrary.simpleMessage("Bewerte uns"), - "rateUsOnStore": m56, + "rateUsOnStore": m60, + "reassignMe": + MessageLookupByLibrary.simpleMessage("\"Ich\" neu zuweisen"), + "reassignedToName": m61, + "reassigningLoading": + MessageLookupByLibrary.simpleMessage("Ordne neu zu..."), "recover": MessageLookupByLibrary.simpleMessage("Wiederherstellen"), "recoverAccount": MessageLookupByLibrary.simpleMessage("Konto wiederherstellen"), @@ -1468,7 +1506,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Konto wiederherstellen"), "recoveryInitiated": MessageLookupByLibrary.simpleMessage("Wiederherstellung gestartet"), - "recoveryInitiatedDesc": m57, + "recoveryInitiatedDesc": m62, "recoveryKey": MessageLookupByLibrary.simpleMessage( "Wiederherstellungs-Schlüssel"), "recoveryKeyCopiedToClipboard": MessageLookupByLibrary.simpleMessage( @@ -1483,12 +1521,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": m58, + "recoveryReady": m63, "recoverySuccessful": MessageLookupByLibrary.simpleMessage( "Wiederherstellung erfolgreich!"), "recoveryWarning": MessageLookupByLibrary.simpleMessage( "Ein vertrauenswürdiger Kontakt versucht, auf dein Konto zuzugreifen"), - "recoveryWarningBody": m59, + "recoveryWarningBody": m64, "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": @@ -1504,7 +1542,7 @@ class MessageLookup extends MessageLookupByLibrary { "1. Gib diesen Code an deine Freunde"), "referralStep2": MessageLookupByLibrary.simpleMessage( "2. Sie schließen ein bezahltes Abo ab"), - "referralStep3": m60, + "referralStep3": m65, "referrals": MessageLookupByLibrary.simpleMessage("Weiterempfehlungen"), "referralsAreCurrentlyPaused": MessageLookupByLibrary.simpleMessage( "Einlösungen sind derzeit pausiert"), @@ -1536,7 +1574,7 @@ class MessageLookup extends MessageLookupByLibrary { "removeLink": MessageLookupByLibrary.simpleMessage("Link entfernen"), "removeParticipant": MessageLookupByLibrary.simpleMessage("Teilnehmer entfernen"), - "removeParticipantBody": m61, + "removeParticipantBody": m66, "removePersonLabel": MessageLookupByLibrary.simpleMessage("Personenetikett entfernen"), "removePublicLink": @@ -1556,7 +1594,7 @@ class MessageLookup extends MessageLookupByLibrary { "renameFile": MessageLookupByLibrary.simpleMessage("Datei umbenennen"), "renewSubscription": MessageLookupByLibrary.simpleMessage("Abonnement erneuern"), - "renewsOn": m62, + "renewsOn": m67, "reportABug": MessageLookupByLibrary.simpleMessage("Fehler melden"), "reportBug": MessageLookupByLibrary.simpleMessage("Fehler melden"), "resendEmail": @@ -1588,6 +1626,9 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Nach rechts drehen"), "safelyStored": MessageLookupByLibrary.simpleMessage("Gesichert"), "save": MessageLookupByLibrary.simpleMessage("Speichern"), + "saveChangesBeforeLeavingQuestion": + MessageLookupByLibrary.simpleMessage( + "Änderungen vor dem Verlassen speichern?"), "saveCollage": MessageLookupByLibrary.simpleMessage("Collage speichern"), "saveCopy": MessageLookupByLibrary.simpleMessage("Kopie speichern"), @@ -1635,8 +1676,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": m63, - "searchSectionsLengthMismatch": m64, + "searchResultCount": m68, + "searchSectionsLengthMismatch": m69, "security": MessageLookupByLibrary.simpleMessage("Sicherheit"), "seePublicAlbumLinksInApp": MessageLookupByLibrary.simpleMessage( "Öffentliche Album-Links in der App ansehen"), @@ -1659,7 +1700,11 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("E-Mail-App auswählen"), "selectMorePhotos": MessageLookupByLibrary.simpleMessage("Mehr Fotos auswählen"), + "selectPersonToLink": MessageLookupByLibrary.simpleMessage( + "Person zum Verknüpfen auswählen"), "selectReason": MessageLookupByLibrary.simpleMessage("Grund auswählen"), + "selectYourFace": + MessageLookupByLibrary.simpleMessage("Wähle dein Gesicht"), "selectYourPlan": MessageLookupByLibrary.simpleMessage("Wähle dein Abo aus"), "selectedFilesAreNotOnEnte": MessageLookupByLibrary.simpleMessage( @@ -1671,7 +1716,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage( "Ausgewählte Elemente werden aus allen Alben gelöscht und in den Papierkorb verschoben."), "selectedPhotos": m6, - "selectedPhotosWithYours": m65, + "selectedPhotosWithYours": m70, "send": MessageLookupByLibrary.simpleMessage("Absenden"), "sendEmail": MessageLookupByLibrary.simpleMessage("E-Mail senden"), "sendInvite": MessageLookupByLibrary.simpleMessage("Einladung senden"), @@ -1701,16 +1746,16 @@ class MessageLookup extends MessageLookupByLibrary { "shareAnAlbumNow": MessageLookupByLibrary.simpleMessage("Teile jetzt ein Album"), "shareLink": MessageLookupByLibrary.simpleMessage("Link teilen"), - "shareMyVerificationID": m66, + "shareMyVerificationID": m71, "shareOnlyWithThePeopleYouWant": MessageLookupByLibrary.simpleMessage( "Teile mit ausgewählten Personen"), "shareTextConfirmOthersVerificationID": m7, "shareTextRecommendUsingEnte": MessageLookupByLibrary.simpleMessage( "Hol dir Ente, damit wir ganz einfach Fotos und Videos in Originalqualität teilen können\n\nhttps://ente.io"), - "shareTextReferralCode": m67, + "shareTextReferralCode": m72, "shareWithNonenteUsers": MessageLookupByLibrary.simpleMessage( "Mit Nicht-Ente-Benutzern teilen"), - "shareWithPeopleSectionTitle": m68, + "shareWithPeopleSectionTitle": m73, "shareYourFirstAlbum": MessageLookupByLibrary.simpleMessage("Teile dein erstes Album"), "sharedAlbumSectionDescription": MessageLookupByLibrary.simpleMessage( @@ -1721,7 +1766,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": m69, + "sharedWith": m74, "sharedWithMe": MessageLookupByLibrary.simpleMessage("Mit mir geteilt"), "sharedWithYou": MessageLookupByLibrary.simpleMessage("Mit dir geteilt"), @@ -1737,11 +1782,11 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Andere Geräte abmelden"), "signUpTerms": MessageLookupByLibrary.simpleMessage( "Ich stimme den Nutzungsbedingungen und der Datenschutzerklärung zu"), - "singleFileDeleteFromDevice": m70, + "singleFileDeleteFromDevice": m75, "singleFileDeleteHighlight": MessageLookupByLibrary.simpleMessage( "Es wird aus allen Alben gelöscht."), - "singleFileInBothLocalAndRemote": m71, - "singleFileInRemoteOnly": m72, + "singleFileInBothLocalAndRemote": m76, + "singleFileInRemoteOnly": m77, "skip": MessageLookupByLibrary.simpleMessage("Überspringen"), "social": MessageLookupByLibrary.simpleMessage("Social Media"), "someItemsAreInBothEnteAndYourDevice": @@ -1793,10 +1838,11 @@ class MessageLookup extends MessageLookupByLibrary { "storageInGB": m1, "storageLimitExceeded": MessageLookupByLibrary.simpleMessage( "Speichergrenze überschritten"), - "storageUsageInfo": m73, + "storageUsageInfo": m78, + "streamDetails": MessageLookupByLibrary.simpleMessage("Stream-Details"), "strongStrength": MessageLookupByLibrary.simpleMessage("Stark"), - "subAlreadyLinkedErrMessage": m74, - "subWillBeCancelledOn": m75, + "subAlreadyLinkedErrMessage": m79, + "subWillBeCancelledOn": m80, "subscribe": MessageLookupByLibrary.simpleMessage("Abonnieren"), "subscribeToEnableSharing": MessageLookupByLibrary.simpleMessage( "Du benötigst ein aktives, bezahltes Abonnement, um das Teilen zu aktivieren."), @@ -1813,7 +1859,7 @@ class MessageLookup extends MessageLookupByLibrary { "suggestFeatures": MessageLookupByLibrary.simpleMessage("Verbesserung vorschlagen"), "support": MessageLookupByLibrary.simpleMessage("Support"), - "syncProgress": m76, + "syncProgress": m81, "syncStopped": MessageLookupByLibrary.simpleMessage("Synchronisierung angehalten"), "syncing": MessageLookupByLibrary.simpleMessage("Synchronisiere …"), @@ -1826,7 +1872,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Zum Entsperren antippen"), "tapToUpload": MessageLookupByLibrary.simpleMessage("Zum Hochladen antippen"), - "tapToUploadIsIgnoredDue": m77, + "tapToUploadIsIgnoredDue": m82, "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"), @@ -1866,7 +1912,9 @@ class MessageLookup extends MessageLookupByLibrary { "Diese E-Mail-Adresse wird bereits verwendet"), "thisImageHasNoExifData": MessageLookupByLibrary.simpleMessage( "Dieses Bild hat keine Exif-Daten"), - "thisIsPersonVerificationId": m78, + "thisIsMeExclamation": + MessageLookupByLibrary.simpleMessage("Das bin ich!"), + "thisIsPersonVerificationId": m83, "thisIsYourVerificationId": MessageLookupByLibrary.simpleMessage( "Dies ist deine Verifizierungs-ID"), "thisWillLogYouOutOfTheFollowingDevice": @@ -1891,11 +1939,11 @@ class MessageLookup extends MessageLookupByLibrary { "total": MessageLookupByLibrary.simpleMessage("Gesamt"), "totalSize": MessageLookupByLibrary.simpleMessage("Gesamtgröße"), "trash": MessageLookupByLibrary.simpleMessage("Papierkorb"), - "trashDaysLeft": m79, + "trashDaysLeft": m84, "trim": MessageLookupByLibrary.simpleMessage("Schneiden"), "trustedContacts": MessageLookupByLibrary.simpleMessage("Vertrauenswürdige Kontakte"), - "trustedInviteBody": m80, + "trustedInviteBody": m85, "tryAgain": MessageLookupByLibrary.simpleMessage("Erneut versuchen"), "turnOnBackupForAutoUpload": MessageLookupByLibrary.simpleMessage( "Aktiviere die Sicherung, um neue Dateien in diesem Ordner automatisch zu Ente hochzuladen."), @@ -1914,7 +1962,7 @@ class MessageLookup extends MessageLookupByLibrary { "Zwei-Faktor-Authentifizierung (2FA) erfolgreich zurückgesetzt"), "twofactorSetup": MessageLookupByLibrary.simpleMessage( "Zweiten Faktor (2FA) einrichten"), - "typeOfGallerGallerytypeIsNotSupportedForRename": m81, + "typeOfGallerGallerytypeIsNotSupportedForRename": m86, "unarchive": MessageLookupByLibrary.simpleMessage("Dearchivieren"), "unarchiveAlbum": MessageLookupByLibrary.simpleMessage("Album dearchivieren"), @@ -1938,10 +1986,10 @@ class MessageLookup extends MessageLookupByLibrary { "updatingFolderSelection": MessageLookupByLibrary.simpleMessage( "Ordnerauswahl wird aktualisiert..."), "upgrade": MessageLookupByLibrary.simpleMessage("Upgrade"), - "uploadIsIgnoredDueToIgnorereason": m82, + "uploadIsIgnoredDueToIgnorereason": m87, "uploadingFilesToAlbum": MessageLookupByLibrary.simpleMessage( "Dateien werden ins Album hochgeladen..."), - "uploadingMultipleMemories": m83, + "uploadingMultipleMemories": m88, "uploadingSingleMemory": MessageLookupByLibrary.simpleMessage( "Sichere ein Erinnerungsstück..."), "upto50OffUntil4thDec": MessageLookupByLibrary.simpleMessage( @@ -1960,7 +2008,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Ausgewähltes Foto verwenden"), "usedSpace": MessageLookupByLibrary.simpleMessage("Belegter Speicherplatz"), - "validTill": m84, + "validTill": m89, "verificationFailedPleaseTryAgain": MessageLookupByLibrary.simpleMessage( "Verifizierung fehlgeschlagen, bitte versuchen Sie es erneut"), @@ -1969,7 +2017,7 @@ class MessageLookup extends MessageLookupByLibrary { "verify": MessageLookupByLibrary.simpleMessage("Überprüfen"), "verifyEmail": MessageLookupByLibrary.simpleMessage("E-Mail-Adresse verifizieren"), - "verifyEmailID": m85, + "verifyEmailID": m90, "verifyIDLabel": MessageLookupByLibrary.simpleMessage("Überprüfen"), "verifyPasskey": MessageLookupByLibrary.simpleMessage("Passkey verifizieren"), @@ -1981,6 +2029,8 @@ class MessageLookup extends MessageLookupByLibrary { "videoInfo": MessageLookupByLibrary.simpleMessage("Video-Informationen"), "videoSmallCase": MessageLookupByLibrary.simpleMessage("Video"), + "videoStreaming": + MessageLookupByLibrary.simpleMessage("Video-Streaming"), "videos": MessageLookupByLibrary.simpleMessage("Videos"), "viewActiveSessions": MessageLookupByLibrary.simpleMessage("Aktive Sitzungen anzeigen"), @@ -1996,7 +2046,7 @@ class MessageLookup extends MessageLookupByLibrary { "viewRecoveryKey": MessageLookupByLibrary.simpleMessage( "Wiederherstellungsschlüssel anzeigen"), "viewer": MessageLookupByLibrary.simpleMessage("Zuschauer"), - "viewersSuccessfullyAdded": m86, + "viewersSuccessfullyAdded": m91, "visitWebToManage": MessageLookupByLibrary.simpleMessage( "Bitte rufe \"web.ente.io\" auf, um dein Abo zu verwalten"), "waitingForVerification": @@ -2018,7 +2068,7 @@ class MessageLookup extends MessageLookupByLibrary { "Ein vertrauenswürdiger Kontakt kann helfen, deine Daten wiederherzustellen."), "yearShort": MessageLookupByLibrary.simpleMessage("Jahr"), "yearly": MessageLookupByLibrary.simpleMessage("Jährlich"), - "yearsAgo": m87, + "yearsAgo": m92, "yes": MessageLookupByLibrary.simpleMessage("Ja"), "yesCancel": MessageLookupByLibrary.simpleMessage("Ja, kündigen"), "yesConvertToViewer": MessageLookupByLibrary.simpleMessage( @@ -2050,7 +2100,7 @@ class MessageLookup extends MessageLookupByLibrary { "Du kannst nicht mit dir selbst teilen"), "youDontHaveAnyArchivedItems": MessageLookupByLibrary.simpleMessage( "Du hast keine archivierten Elemente."), - "youHaveSuccessfullyFreedUp": m88, + "youHaveSuccessfullyFreedUp": m93, "yourAccountHasBeenDeleted": MessageLookupByLibrary.simpleMessage( "Dein Benutzerkonto wurde gelöscht"), "yourMap": MessageLookupByLibrary.simpleMessage("Deine Karte"), @@ -2071,9 +2121,6 @@ class MessageLookup extends MessageLookupByLibrary { "Dein Abonnement wurde erfolgreich aktualisiert."), "yourVerificationCodeHasExpired": MessageLookupByLibrary.simpleMessage( "Ihr Bestätigungscode ist abgelaufen"), - "youveNoDuplicateFilesThatCanBeCleared": - MessageLookupByLibrary.simpleMessage( - "Du hast keine Duplikate, die gelöscht werden können"), "youveNoFilesInThisAlbumThatCanBeDeleted": MessageLookupByLibrary.simpleMessage( "Du hast keine Dateien in diesem Album, die gelöscht werden können"), diff --git a/mobile/lib/generated/intl/messages_el.dart b/mobile/lib/generated/intl/messages_el.dart index 096f3e56ad..79c0433b27 100644 --- a/mobile/lib/generated/intl/messages_el.dart +++ b/mobile/lib/generated/intl/messages_el.dart @@ -23,8 +23,6 @@ class MessageLookup extends MessageLookupByLibrary { final messages = _notInlinedMessages(_notInlinedMessages); static Map _notInlinedMessages(_) => { "enterYourEmailAddress": MessageLookupByLibrary.simpleMessage( - "Εισάγετε την διεύθυνση ηλ. ταχυδρομείου σας"), - "orPickFromYourContacts": - MessageLookupByLibrary.simpleMessage("or pick from your contacts") + "Εισάγετε την διεύθυνση ηλ. ταχυδρομείου σας") }; } diff --git a/mobile/lib/generated/intl/messages_en.dart b/mobile/lib/generated/intl/messages_en.dart index 41537dd1ec..d9982f9c27 100644 --- a/mobile/lib/generated/intl/messages_en.dart +++ b/mobile/lib/generated/intl/messages_en.dart @@ -20,258 +20,258 @@ typedef String MessageIfAbsent(String messageStr, List args); class MessageLookup extends MessageLookupByLibrary { String get localeName => 'en'; - static String m89(title) => "${title} (Me)"; - - static String m9(count) => - "${Intl.plural(count, zero: 'Add collaborator', one: 'Add collaborator', other: 'Add collaborators')}"; + static String m9(title) => "${title} (Me)"; static String m10(count) => + "${Intl.plural(count, zero: 'Add collaborator', one: 'Add collaborator', other: 'Add collaborators')}"; + + static String m11(count) => "${Intl.plural(count, one: 'Add item', other: 'Add items')}"; - static String m11(storageAmount, endDate) => + static String m12(storageAmount, endDate) => "Your ${storageAmount} add-on is valid till ${endDate}"; - static String m12(count) => + static String m13(count) => "${Intl.plural(count, zero: 'Add viewer', one: 'Add viewer', other: 'Add viewers')}"; - static String m13(emailOrName) => "Added by ${emailOrName}"; + static String m14(emailOrName) => "Added by ${emailOrName}"; - static String m14(albumName) => "Added successfully to ${albumName}"; + static String m15(albumName) => "Added successfully to ${albumName}"; - static String m15(count) => + static String m16(count) => "${Intl.plural(count, zero: 'No Participants', one: '1 Participant', other: '${count} Participants')}"; - static String m16(versionValue) => "Version: ${versionValue}"; + static String m17(versionValue) => "Version: ${versionValue}"; - static String m17(freeAmount, storageUnit) => + static String m18(freeAmount, storageUnit) => "${freeAmount} ${storageUnit} free"; - static String m18(paymentProvider) => + static String m19(paymentProvider) => "Please cancel your existing subscription from ${paymentProvider} first"; static String m3(user) => "${user} will not be able to add more photos to this album\n\nThey will still be able to remove existing photos added by them"; - static String m19(isFamilyMember, storageAmountInGb) => + static String m20(isFamilyMember, storageAmountInGb) => "${Intl.select(isFamilyMember, { 'true': 'Your family has claimed ${storageAmountInGb} GB so far', 'false': 'You have claimed ${storageAmountInGb} GB so far', 'other': 'You have claimed ${storageAmountInGb} GB so far!', })}"; - static String m20(albumName) => "Collaborative link created for ${albumName}"; + static String m21(albumName) => "Collaborative link created for ${albumName}"; - static String m21(count) => + static String m22(count) => "${Intl.plural(count, zero: 'Added 0 collaborator', one: 'Added 1 collaborator', other: 'Added ${count} collaborators')}"; - static String m22(email, numOfDays) => + static String m23(email, numOfDays) => "You are about to add ${email} as a trusted contact. They will be able to recover your account if you are absent for ${numOfDays} days."; - static String m23(familyAdminEmail) => + static String m24(familyAdminEmail) => "Please contact ${familyAdminEmail} to manage your subscription"; - static String m24(provider) => + static String m25(provider) => "Please contact us at support@ente.io to manage your ${provider} subscription."; - static String m25(endpoint) => "Connected to ${endpoint}"; + static String m26(endpoint) => "Connected to ${endpoint}"; - static String m26(count) => + static String m27(count) => "${Intl.plural(count, one: 'Delete ${count} item', other: 'Delete ${count} items')}"; - static String m27(currentlyDeleting, totalCount) => + static String m28(currentlyDeleting, totalCount) => "Deleting ${currentlyDeleting} / ${totalCount}"; - static String m28(albumName) => + static String m29(albumName) => "This will remove the public link for accessing \"${albumName}\"."; - static String m29(supportEmail) => + static String m30(supportEmail) => "Please drop an email to ${supportEmail} from your registered email address"; - static String m30(count, storageSaved) => + static String m31(count, storageSaved) => "You have cleaned up ${Intl.plural(count, one: '${count} duplicate file', other: '${count} duplicate files')}, saving (${storageSaved}!)"; - static String m31(count, formattedSize) => + static String m32(count, formattedSize) => "${count} files, ${formattedSize} each"; - static String m32(newEmail) => "Email changed to ${newEmail}"; + static String m33(newEmail) => "Email changed to ${newEmail}"; - static String m90(email) => "${email} does not have an Ente account."; + static String m34(email) => "${email} does not have an Ente account."; - static String m33(email) => + static String m35(email) => "${email} does not have an Ente account.\n\nSend them an invite to share photos."; - static String m34(text) => "Extra photos found for ${text}"; + static String m36(text) => "Extra photos found for ${text}"; - static String m35(count, formattedNumber) => + static String m37(count, formattedNumber) => "${Intl.plural(count, one: '1 file', other: '${formattedNumber} files')} on this device have been backed up safely"; - static String m36(count, formattedNumber) => + static String m38(count, formattedNumber) => "${Intl.plural(count, one: '1 file', other: '${formattedNumber} files')} in this album has been backed up safely"; static String m4(storageAmountInGB) => "${storageAmountInGB} GB each time someone signs up for a paid plan and applies your code"; - static String m37(endDate) => "Free trial valid till ${endDate}"; + static String m39(endDate) => "Free trial valid till ${endDate}"; - static String m38(count) => + static String m40(count) => "You can still access ${Intl.plural(count, one: 'it', other: 'them')} on Ente as long as you have an active subscription"; - static String m39(sizeInMBorGB) => "Free up ${sizeInMBorGB}"; + static String m41(sizeInMBorGB) => "Free up ${sizeInMBorGB}"; - static String m40(count, formattedSize) => + static String m42(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 m41(currentlyProcessing, totalCount) => + static String m43(currentlyProcessing, totalCount) => "Processing ${currentlyProcessing} / ${totalCount}"; - static String m42(count) => + static String m44(count) => "${Intl.plural(count, one: '${count} item', other: '${count} items')}"; - static String m43(email) => + static String m45(email) => "${email} has invited you to be a trusted contact"; - static String m44(expiryTime) => "Link will expire on ${expiryTime}"; + static String m46(expiryTime) => "Link will expire on ${expiryTime}"; - static String m91(email) => "Link person to ${email}"; + static String m47(email) => "Link person to ${email}"; - static String m92(personName, email) => + static String m48(personName, email) => "This will link ${personName} to ${email}"; static String m5(count, formattedCount) => "${Intl.plural(count, zero: 'no memories', one: '${formattedCount} memory', other: '${formattedCount} memories')}"; - static String m45(count) => + static String m49(count) => "${Intl.plural(count, one: 'Move item', other: 'Move items')}"; - static String m46(albumName) => "Moved successfully to ${albumName}"; + static String m50(albumName) => "Moved successfully to ${albumName}"; - static String m47(personName) => "No suggestions for ${personName}"; + static String m51(personName) => "No suggestions for ${personName}"; - static String m48(name) => "Not ${name}?"; + static String m52(name) => "Not ${name}?"; - static String m49(familyAdminEmail) => + static String m53(familyAdminEmail) => "Please contact ${familyAdminEmail} to change your code."; static String m0(passwordStrengthValue) => "Password strength: ${passwordStrengthValue}"; - static String m50(providerName) => + static String m54(providerName) => "Please talk to ${providerName} support if you were charged"; - static String m51(count) => + static String m55(count) => "${Intl.plural(count, zero: '0 photo', one: '1 photo', other: '${count} photos')}"; - static String m52(endDate) => + static String m56(endDate) => "Free trial valid till ${endDate}.\nYou can choose a paid plan afterwards."; - static String m53(toEmail) => "Please email us at ${toEmail}"; + static String m57(toEmail) => "Please email us at ${toEmail}"; - static String m54(toEmail) => "Please send the logs to \n${toEmail}"; + static String m58(toEmail) => "Please send the logs to \n${toEmail}"; - static String m55(folderName) => "Processing ${folderName}..."; + static String m59(folderName) => "Processing ${folderName}..."; - static String m56(storeName) => "Rate us on ${storeName}"; + static String m60(storeName) => "Rate us on ${storeName}"; - static String m93(name) => "Reassigned you to ${name}"; + static String m61(name) => "Reassigned you to ${name}"; - static String m57(days, email) => + static String m62(days, email) => "You can access the account after ${days} days. A notification will be sent to ${email}."; - static String m58(email) => + static String m63(email) => "You can now recover ${email}\'s account by setting a new password."; - static String m59(email) => "${email} is trying to recover your account."; + static String m64(email) => "${email} is trying to recover your account."; - static String m60(storageInGB) => + static String m65(storageInGB) => "3. Both of you get ${storageInGB} GB* free"; - static String m61(userEmail) => + static String m66(userEmail) => "${userEmail} will be removed from this shared album\n\nAny photos added by them will also be removed from the album"; - static String m62(endDate) => "Subscription renews on ${endDate}"; + static String m67(endDate) => "Subscription renews on ${endDate}"; - static String m63(count) => + static String m68(count) => "${Intl.plural(count, one: '${count} result found', other: '${count} results found')}"; - static String m64(snapshotLength, searchLength) => + static String m69(snapshotLength, searchLength) => "Sections length mismatch: ${snapshotLength} != ${searchLength}"; static String m6(count) => "${count} selected"; - static String m65(count, yourCount) => + static String m70(count, yourCount) => "${count} selected (${yourCount} yours)"; - static String m66(verificationID) => + static String m71(verificationID) => "Here\'s my verification ID: ${verificationID} for ente.io."; static String m7(verificationID) => "Hey, can you confirm that this is your ente.io verification ID: ${verificationID}"; - static String m67(referralCode, referralStorageInGB) => + static String m72(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 m68(numberOfPeople) => + static String m73(numberOfPeople) => "${Intl.plural(numberOfPeople, zero: 'Share with specific people', one: 'Shared with 1 person', other: 'Shared with ${numberOfPeople} people')}"; - static String m69(emailIDs) => "Shared with ${emailIDs}"; + static String m74(emailIDs) => "Shared with ${emailIDs}"; - static String m70(fileType) => + static String m75(fileType) => "This ${fileType} will be deleted from your device."; - static String m71(fileType) => + static String m76(fileType) => "This ${fileType} is in both Ente and your device."; - static String m72(fileType) => "This ${fileType} will be deleted from Ente."; + static String m77(fileType) => "This ${fileType} will be deleted from Ente."; static String m1(storageAmountInGB) => "${storageAmountInGB} GB"; - static String m73( + static String m78( usedAmount, usedStorageUnit, totalAmount, totalStorageUnit) => "${usedAmount} ${usedStorageUnit} of ${totalAmount} ${totalStorageUnit} used"; - static String m74(id) => + static String m79(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 m75(endDate) => + static String m80(endDate) => "Your subscription will be cancelled on ${endDate}"; - static String m76(completed, total) => + static String m81(completed, total) => "${completed}/${total} memories preserved"; - static String m77(ignoreReason) => + static String m82(ignoreReason) => "Tap to upload, upload is currently ignored due to ${ignoreReason}"; static String m8(storageAmountInGB) => "They also get ${storageAmountInGB} GB"; - static String m78(email) => "This is ${email}\'s Verification ID"; + static String m83(email) => "This is ${email}\'s Verification ID"; - static String m79(count) => + static String m84(count) => "${Intl.plural(count, zero: 'Soon', one: '1 day', other: '${count} days')}"; - static String m80(email) => + static String m85(email) => "You have been invited to be a legacy contact by ${email}."; - static String m81(galleryType) => + static String m86(galleryType) => "Type of gallery ${galleryType} is not supported for rename"; - static String m82(ignoreReason) => "Upload is ignored due to ${ignoreReason}"; + static String m87(ignoreReason) => "Upload is ignored due to ${ignoreReason}"; - static String m83(count) => "Preserving ${count} memories..."; + static String m88(count) => "Preserving ${count} memories..."; - static String m84(endDate) => "Valid till ${endDate}"; + static String m89(endDate) => "Valid till ${endDate}"; - static String m85(email) => "Verify ${email}"; + static String m90(email) => "Verify ${email}"; - static String m86(count) => + static String m91(count) => "${Intl.plural(count, zero: 'Added 0 viewer', one: 'Added 1 viewer', other: 'Added ${count} viewers')}"; static String m2(email) => "We have sent a mail to ${email}"; - static String m87(count) => + static String m92(count) => "${Intl.plural(count, one: '${count} year ago', other: '${count} years ago')}"; - static String m88(storageSaved) => + static String m93(storageSaved) => "You have successfully freed up ${storageSaved}!"; final messages = _notInlinedMessages(_notInlinedMessages); @@ -284,7 +284,7 @@ class MessageLookup extends MessageLookupByLibrary { "account": MessageLookupByLibrary.simpleMessage("Account"), "accountIsAlreadyConfigured": MessageLookupByLibrary.simpleMessage( "Account is already configured."), - "accountOwnerPersonAppbarTitle": m89, + "accountOwnerPersonAppbarTitle": m9, "accountWelcomeBack": MessageLookupByLibrary.simpleMessage("Welcome back!"), "ackPasswordLostWarning": MessageLookupByLibrary.simpleMessage( @@ -296,11 +296,11 @@ class MessageLookup extends MessageLookupByLibrary { "addANewEmail": MessageLookupByLibrary.simpleMessage("Add a new email"), "addCollaborator": MessageLookupByLibrary.simpleMessage("Add collaborator"), - "addCollaborators": m9, + "addCollaborators": m10, "addFiles": MessageLookupByLibrary.simpleMessage("Add Files"), "addFromDevice": MessageLookupByLibrary.simpleMessage("Add from device"), - "addItem": m10, + "addItem": m11, "addLocation": MessageLookupByLibrary.simpleMessage("Add location"), "addLocationButton": MessageLookupByLibrary.simpleMessage("Add"), "addMore": MessageLookupByLibrary.simpleMessage("Add more"), @@ -311,7 +311,7 @@ class MessageLookup extends MessageLookupByLibrary { "addNewPerson": MessageLookupByLibrary.simpleMessage("Add new person"), "addOnPageSubtitle": MessageLookupByLibrary.simpleMessage("Details of add-ons"), - "addOnValidTill": m11, + "addOnValidTill": m12, "addOns": MessageLookupByLibrary.simpleMessage("Add-ons"), "addPhotos": MessageLookupByLibrary.simpleMessage("Add photos"), "addSelected": MessageLookupByLibrary.simpleMessage("Add selected"), @@ -322,12 +322,12 @@ class MessageLookup extends MessageLookupByLibrary { "addTrustedContact": MessageLookupByLibrary.simpleMessage("Add Trusted Contact"), "addViewer": MessageLookupByLibrary.simpleMessage("Add viewer"), - "addViewers": m12, + "addViewers": m13, "addYourPhotosNow": MessageLookupByLibrary.simpleMessage("Add your photos now"), "addedAs": MessageLookupByLibrary.simpleMessage("Added as"), - "addedBy": m13, - "addedSuccessfullyTo": m14, + "addedBy": m14, + "addedSuccessfullyTo": m15, "addingToFavorites": MessageLookupByLibrary.simpleMessage("Adding to favorites..."), "advanced": MessageLookupByLibrary.simpleMessage("Advanced"), @@ -338,7 +338,7 @@ class MessageLookup extends MessageLookupByLibrary { "after1Week": MessageLookupByLibrary.simpleMessage("After 1 week"), "after1Year": MessageLookupByLibrary.simpleMessage("After 1 year"), "albumOwner": MessageLookupByLibrary.simpleMessage("Owner"), - "albumParticipantsCount": m15, + "albumParticipantsCount": m16, "albumTitle": MessageLookupByLibrary.simpleMessage("Album title"), "albumUpdated": MessageLookupByLibrary.simpleMessage("Album updated"), "albums": MessageLookupByLibrary.simpleMessage("Albums"), @@ -384,7 +384,7 @@ class MessageLookup extends MessageLookupByLibrary { "appLock": MessageLookupByLibrary.simpleMessage("App lock"), "appLockDescriptions": MessageLookupByLibrary.simpleMessage( "Choose between your device\'s default lock screen and a custom lock screen with a PIN or password."), - "appVersion": m16, + "appVersion": m17, "appleId": MessageLookupByLibrary.simpleMessage("Apple ID"), "apply": MessageLookupByLibrary.simpleMessage("Apply"), "applyCodeTitle": MessageLookupByLibrary.simpleMessage("Apply code"), @@ -466,7 +466,7 @@ class MessageLookup extends MessageLookupByLibrary { "autoPairDesc": MessageLookupByLibrary.simpleMessage( "Auto pair works only with devices that support Chromecast."), "available": MessageLookupByLibrary.simpleMessage("Available"), - "availableStorageSpace": m17, + "availableStorageSpace": m18, "backedUpFolders": MessageLookupByLibrary.simpleMessage("Backed up folders"), "backup": MessageLookupByLibrary.simpleMessage("Backup"), @@ -503,7 +503,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Cancel recovery"), "cancelAccountRecoveryBody": MessageLookupByLibrary.simpleMessage( "Are you sure you want to cancel recovery?"), - "cancelOtherSubscription": m18, + "cancelOtherSubscription": m19, "cancelSubscription": MessageLookupByLibrary.simpleMessage("Cancel subscription"), "cannotAddMorePhotosAfterBecomingViewer": m3, @@ -554,7 +554,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Claim free storage"), "claimMore": MessageLookupByLibrary.simpleMessage("Claim more!"), "claimed": MessageLookupByLibrary.simpleMessage("Claimed"), - "claimedStorageSoFar": m19, + "claimedStorageSoFar": m20, "cleanUncategorized": MessageLookupByLibrary.simpleMessage("Clean Uncategorized"), "cleanUncategorizedDescription": MessageLookupByLibrary.simpleMessage( @@ -583,12 +583,12 @@ class MessageLookup extends MessageLookupByLibrary { "Create a link to allow people to add and view photos in your shared album without needing an Ente app or account. Great for collecting event photos."), "collaborativeLink": MessageLookupByLibrary.simpleMessage("Collaborative link"), - "collaborativeLinkCreatedFor": m20, + "collaborativeLinkCreatedFor": m21, "collaborator": MessageLookupByLibrary.simpleMessage("Collaborator"), "collaboratorsCanAddPhotosAndVideosToTheSharedAlbum": MessageLookupByLibrary.simpleMessage( "Collaborators can add photos and videos to the shared album."), - "collaboratorsSuccessfullyAdded": m21, + "collaboratorsSuccessfullyAdded": m22, "collageLayout": MessageLookupByLibrary.simpleMessage("Layout"), "collageSaved": MessageLookupByLibrary.simpleMessage("Collage saved to gallery"), @@ -605,7 +605,7 @@ class MessageLookup extends MessageLookupByLibrary { "Are you sure you want to disable two-factor authentication?"), "confirmAccountDeletion": MessageLookupByLibrary.simpleMessage("Confirm Account Deletion"), - "confirmAddingTrustedContact": m22, + "confirmAddingTrustedContact": m23, "confirmDeletePrompt": MessageLookupByLibrary.simpleMessage( "Yes, I want to permanently delete this account and its data across all apps."), "confirmPassword": @@ -618,10 +618,10 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Confirm your recovery key"), "connectToDevice": MessageLookupByLibrary.simpleMessage("Connect to device"), - "contactFamilyAdmin": m23, + "contactFamilyAdmin": m24, "contactSupport": MessageLookupByLibrary.simpleMessage("Contact support"), - "contactToManageSubscription": m24, + "contactToManageSubscription": m25, "contacts": MessageLookupByLibrary.simpleMessage("Contacts"), "contents": MessageLookupByLibrary.simpleMessage("Contents"), "continueLabel": MessageLookupByLibrary.simpleMessage("Continue"), @@ -667,7 +667,7 @@ class MessageLookup extends MessageLookupByLibrary { "currentlyRunning": MessageLookupByLibrary.simpleMessage("currently running"), "custom": MessageLookupByLibrary.simpleMessage("Custom"), - "customEndpoint": m25, + "customEndpoint": m26, "darkTheme": MessageLookupByLibrary.simpleMessage("Dark"), "dayToday": MessageLookupByLibrary.simpleMessage("Today"), "dayYesterday": MessageLookupByLibrary.simpleMessage("Yesterday"), @@ -704,11 +704,11 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Delete from device"), "deleteFromEnte": MessageLookupByLibrary.simpleMessage("Delete from Ente"), - "deleteItemCount": m26, + "deleteItemCount": m27, "deleteLocation": MessageLookupByLibrary.simpleMessage("Delete location"), "deletePhotos": MessageLookupByLibrary.simpleMessage("Delete photos"), - "deleteProgress": m27, + "deleteProgress": m28, "deleteReason1": MessageLookupByLibrary.simpleMessage( "It’s missing a key feature that I need"), "deleteReason2": MessageLookupByLibrary.simpleMessage( @@ -747,7 +747,7 @@ class MessageLookup extends MessageLookupByLibrary { "Viewers can still take screenshots or save a copy of your photos using external tools"), "disableDownloadWarningTitle": MessageLookupByLibrary.simpleMessage("Please note"), - "disableLinkMessage": m28, + "disableLinkMessage": m29, "disableTwofactor": MessageLookupByLibrary.simpleMessage("Disable two-factor"), "disablingTwofactorAuthentication": @@ -789,9 +789,9 @@ class MessageLookup extends MessageLookupByLibrary { "downloadFailed": MessageLookupByLibrary.simpleMessage("Download failed"), "downloading": MessageLookupByLibrary.simpleMessage("Downloading..."), - "dropSupportEmail": m29, - "duplicateFileCountWithStorageSaved": m30, - "duplicateItemsGroup": m31, + "dropSupportEmail": m30, + "duplicateFileCountWithStorageSaved": m31, + "duplicateItemsGroup": m32, "edit": MessageLookupByLibrary.simpleMessage("Edit"), "editLocation": MessageLookupByLibrary.simpleMessage("Edit location"), "editLocationTagTitle": @@ -805,9 +805,9 @@ class MessageLookup extends MessageLookupByLibrary { "email": MessageLookupByLibrary.simpleMessage("Email"), "emailAlreadyRegistered": MessageLookupByLibrary.simpleMessage("Email already registered."), - "emailChangedTo": m32, - "emailDoesNotHaveEnteAccount": m90, - "emailNoEnteAccount": m33, + "emailChangedTo": m33, + "emailDoesNotHaveEnteAccount": m34, + "emailNoEnteAccount": m35, "emailNotRegistered": MessageLookupByLibrary.simpleMessage("Email not registered."), "emailVerificationToggle": @@ -888,7 +888,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Export your data"), "extraPhotosFound": MessageLookupByLibrary.simpleMessage("Extra photos found"), - "extraPhotosFoundFor": m34, + "extraPhotosFoundFor": m36, "faceNotClusteredYet": MessageLookupByLibrary.simpleMessage( "Face not clustered yet, please come back later"), "faceRecognition": @@ -938,8 +938,8 @@ class MessageLookup extends MessageLookupByLibrary { "fileTypes": MessageLookupByLibrary.simpleMessage("File types"), "fileTypesAndNames": MessageLookupByLibrary.simpleMessage("File types and names"), - "filesBackedUpFromDevice": m35, - "filesBackedUpInAlbum": m36, + "filesBackedUpFromDevice": m37, + "filesBackedUpInAlbum": m38, "filesDeleted": MessageLookupByLibrary.simpleMessage("Files deleted"), "filesSavedToGallery": MessageLookupByLibrary.simpleMessage("Files saved to gallery"), @@ -959,22 +959,22 @@ class MessageLookup extends MessageLookupByLibrary { "freeStorageUsable": MessageLookupByLibrary.simpleMessage("Free storage usable"), "freeTrial": MessageLookupByLibrary.simpleMessage("Free trial"), - "freeTrialValidTill": m37, - "freeUpAccessPostDelete": m38, - "freeUpAmount": m39, + "freeTrialValidTill": m39, + "freeUpAccessPostDelete": m40, + "freeUpAmount": m41, "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": m40, + "freeUpSpaceSaving": m42, "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": m41, + "genericProgress": m43, "goToSettings": MessageLookupByLibrary.simpleMessage("Go to settings"), "googlePlayId": MessageLookupByLibrary.simpleMessage("Google Play ID"), "grantFullAccessPrompt": MessageLookupByLibrary.simpleMessage( @@ -1055,7 +1055,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": m44, "itemsShowTheNumberOfDaysRemainingBeforePermanentDeletion": MessageLookupByLibrary.simpleMessage( "Items show the number of days remaining before permanent deletion"), @@ -1085,7 +1085,7 @@ class MessageLookup extends MessageLookupByLibrary { "legacy": MessageLookupByLibrary.simpleMessage("Legacy"), "legacyAccounts": MessageLookupByLibrary.simpleMessage("Legacy accounts"), - "legacyInvite": m43, + "legacyInvite": m45, "legacyPageDesc": MessageLookupByLibrary.simpleMessage( "Legacy allows trusted contacts to access your account in your absence."), "legacyPageDesc2": MessageLookupByLibrary.simpleMessage( @@ -1101,7 +1101,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("for faster sharing"), "linkEnabled": MessageLookupByLibrary.simpleMessage("Enabled"), "linkExpired": MessageLookupByLibrary.simpleMessage("Expired"), - "linkExpiresOn": m44, + "linkExpiresOn": m46, "linkExpiry": MessageLookupByLibrary.simpleMessage("Link expiry"), "linkHasExpired": MessageLookupByLibrary.simpleMessage("Link has expired"), @@ -1109,8 +1109,8 @@ class MessageLookup extends MessageLookupByLibrary { "linkPerson": MessageLookupByLibrary.simpleMessage("Link person"), "linkPersonCaption": MessageLookupByLibrary.simpleMessage( "for better sharing experience"), - "linkPersonToEmail": m91, - "linkPersonToEmailConfirmation": m92, + "linkPersonToEmail": m47, + "linkPersonToEmailConfirmation": m48, "livePhotos": MessageLookupByLibrary.simpleMessage("Live Photos"), "loadMessage1": MessageLookupByLibrary.simpleMessage( "You can share your subscription with your family"), @@ -1224,11 +1224,11 @@ class MessageLookup extends MessageLookupByLibrary { "moreDetails": MessageLookupByLibrary.simpleMessage("More details"), "mostRecent": MessageLookupByLibrary.simpleMessage("Most recent"), "mostRelevant": MessageLookupByLibrary.simpleMessage("Most relevant"), - "moveItem": m45, + "moveItem": m49, "moveToAlbum": MessageLookupByLibrary.simpleMessage("Move to album"), "moveToHiddenAlbum": MessageLookupByLibrary.simpleMessage("Move to hidden album"), - "movedSuccessfullyTo": m46, + "movedSuccessfullyTo": m50, "movedToTrash": MessageLookupByLibrary.simpleMessage("Moved to trash"), "movingFilesToAlbum": MessageLookupByLibrary.simpleMessage("Moving files to album..."), @@ -1278,10 +1278,10 @@ class MessageLookup extends MessageLookupByLibrary { "noResults": MessageLookupByLibrary.simpleMessage("No results"), "noResultsFound": MessageLookupByLibrary.simpleMessage("No results found"), - "noSuggestionsForPerson": m47, + "noSuggestionsForPerson": m51, "noSystemLockFound": MessageLookupByLibrary.simpleMessage("No system lock found"), - "notPersonLabel": m48, + "notPersonLabel": m52, "nothingSharedWithYouYet": MessageLookupByLibrary.simpleMessage("Nothing shared with you yet"), "nothingToSeeHere": @@ -1291,7 +1291,7 @@ class MessageLookup extends MessageLookupByLibrary { "onDevice": MessageLookupByLibrary.simpleMessage("On device"), "onEnte": MessageLookupByLibrary.simpleMessage( "On ente"), - "onlyFamilyAdminCanChangeCode": m49, + "onlyFamilyAdminCanChangeCode": m53, "onlyThem": MessageLookupByLibrary.simpleMessage("Only them"), "oops": MessageLookupByLibrary.simpleMessage("Oops"), "oopsCouldNotSaveEdits": @@ -1339,7 +1339,7 @@ class MessageLookup extends MessageLookupByLibrary { "paymentFailed": MessageLookupByLibrary.simpleMessage("Payment failed"), "paymentFailedMessage": MessageLookupByLibrary.simpleMessage( "Unfortunately your payment failed. Please contact support and we\'ll help you out!"), - "paymentFailedTalkToProvider": m50, + "paymentFailedTalkToProvider": m54, "pendingItems": MessageLookupByLibrary.simpleMessage("Pending items"), "pendingSync": MessageLookupByLibrary.simpleMessage("Pending sync"), "people": MessageLookupByLibrary.simpleMessage("People"), @@ -1361,14 +1361,14 @@ class MessageLookup extends MessageLookupByLibrary { "photosAddedByYouWillBeRemovedFromTheAlbum": MessageLookupByLibrary.simpleMessage( "Photos added by you will be removed from the album"), - "photosCount": m51, + "photosCount": m55, "pickCenterPoint": MessageLookupByLibrary.simpleMessage("Pick center point"), "pinAlbum": MessageLookupByLibrary.simpleMessage("Pin album"), "pinLock": MessageLookupByLibrary.simpleMessage("PIN lock"), "playOnTv": MessageLookupByLibrary.simpleMessage("Play album on TV"), "playOriginal": MessageLookupByLibrary.simpleMessage("Play original"), - "playStoreFreeTrialValidTill": m52, + "playStoreFreeTrialValidTill": m56, "playStream": MessageLookupByLibrary.simpleMessage("Play stream"), "playstoreSubscription": MessageLookupByLibrary.simpleMessage("PlayStore subscription"), @@ -1381,14 +1381,14 @@ class MessageLookup extends MessageLookupByLibrary { "pleaseContactSupportIfTheProblemPersists": MessageLookupByLibrary.simpleMessage( "Please contact support if the problem persists"), - "pleaseEmailUsAt": m53, + "pleaseEmailUsAt": m57, "pleaseGrantPermissions": MessageLookupByLibrary.simpleMessage("Please grant permissions"), "pleaseLoginAgain": MessageLookupByLibrary.simpleMessage("Please login again"), "pleaseSelectQuickLinksToRemove": MessageLookupByLibrary.simpleMessage( "Please select quick links to remove"), - "pleaseSendTheLogsTo": m54, + "pleaseSendTheLogsTo": m58, "pleaseTryAgain": MessageLookupByLibrary.simpleMessage("Please try again"), "pleaseVerifyTheCodeYouHaveEntered": @@ -1400,6 +1400,8 @@ class MessageLookup extends MessageLookupByLibrary { "pleaseWaitForSometimeBeforeRetrying": MessageLookupByLibrary.simpleMessage( "Please wait for sometime before retrying"), + "pleaseWaitThisWillTakeAWhile": MessageLookupByLibrary.simpleMessage( + "Please wait, this will take a while."), "preparingLogs": MessageLookupByLibrary.simpleMessage("Preparing logs..."), "preserveMore": MessageLookupByLibrary.simpleMessage("Preserve more"), @@ -1417,7 +1419,7 @@ class MessageLookup extends MessageLookupByLibrary { "proceed": MessageLookupByLibrary.simpleMessage("Proceed"), "processed": MessageLookupByLibrary.simpleMessage("Processed"), "processing": MessageLookupByLibrary.simpleMessage("Processing"), - "processingImport": m55, + "processingImport": m59, "processingVideos": MessageLookupByLibrary.simpleMessage("Processing videos"), "publicLinkCreated": @@ -1430,9 +1432,9 @@ class MessageLookup extends MessageLookupByLibrary { "raiseTicket": MessageLookupByLibrary.simpleMessage("Raise ticket"), "rateTheApp": MessageLookupByLibrary.simpleMessage("Rate the app"), "rateUs": MessageLookupByLibrary.simpleMessage("Rate us"), - "rateUsOnStore": m56, + "rateUsOnStore": m60, "reassignMe": MessageLookupByLibrary.simpleMessage("Reassign \"Me\""), - "reassignedToName": m93, + "reassignedToName": m61, "reassigningLoading": MessageLookupByLibrary.simpleMessage("Reassigning..."), "recover": MessageLookupByLibrary.simpleMessage("Recover"), @@ -1443,7 +1445,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Recover account"), "recoveryInitiated": MessageLookupByLibrary.simpleMessage("Recovery initiated"), - "recoveryInitiatedDesc": m57, + "recoveryInitiatedDesc": m62, "recoveryKey": MessageLookupByLibrary.simpleMessage("Recovery key"), "recoveryKeyCopiedToClipboard": MessageLookupByLibrary.simpleMessage( "Recovery key copied to clipboard"), @@ -1457,12 +1459,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": m58, + "recoveryReady": m63, "recoverySuccessful": MessageLookupByLibrary.simpleMessage("Recovery successful!"), "recoveryWarning": MessageLookupByLibrary.simpleMessage( "A trusted contact is trying to access your account"), - "recoveryWarningBody": m59, + "recoveryWarningBody": m64, "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": @@ -1477,7 +1479,7 @@ class MessageLookup extends MessageLookupByLibrary { "1. Give this code to your friends"), "referralStep2": MessageLookupByLibrary.simpleMessage( "2. They sign up for a paid plan"), - "referralStep3": m60, + "referralStep3": m65, "referrals": MessageLookupByLibrary.simpleMessage("Referrals"), "referralsAreCurrentlyPaused": MessageLookupByLibrary.simpleMessage( "Referrals are currently paused"), @@ -1506,7 +1508,7 @@ class MessageLookup extends MessageLookupByLibrary { "removeLink": MessageLookupByLibrary.simpleMessage("Remove link"), "removeParticipant": MessageLookupByLibrary.simpleMessage("Remove participant"), - "removeParticipantBody": m61, + "removeParticipantBody": m66, "removePersonLabel": MessageLookupByLibrary.simpleMessage("Remove person label"), "removePublicLink": @@ -1526,7 +1528,7 @@ class MessageLookup extends MessageLookupByLibrary { "renameFile": MessageLookupByLibrary.simpleMessage("Rename file"), "renewSubscription": MessageLookupByLibrary.simpleMessage("Renew subscription"), - "renewsOn": m62, + "renewsOn": m67, "reportABug": MessageLookupByLibrary.simpleMessage("Report a bug"), "reportBug": MessageLookupByLibrary.simpleMessage("Report bug"), "resendEmail": MessageLookupByLibrary.simpleMessage("Resend email"), @@ -1604,8 +1606,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": m63, - "searchSectionsLengthMismatch": m64, + "searchResultCount": m68, + "searchSectionsLengthMismatch": m69, "security": MessageLookupByLibrary.simpleMessage("Security"), "seePublicAlbumLinksInApp": MessageLookupByLibrary.simpleMessage( "See public album links in app"), @@ -1644,7 +1646,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage( "Selected items will be deleted from all albums and moved to trash."), "selectedPhotos": m6, - "selectedPhotosWithYours": m65, + "selectedPhotosWithYours": m70, "send": MessageLookupByLibrary.simpleMessage("Send"), "sendEmail": MessageLookupByLibrary.simpleMessage("Send email"), "sendInvite": MessageLookupByLibrary.simpleMessage("Send invite"), @@ -1673,16 +1675,16 @@ class MessageLookup extends MessageLookupByLibrary { "shareAnAlbumNow": MessageLookupByLibrary.simpleMessage("Share an album now"), "shareLink": MessageLookupByLibrary.simpleMessage("Share link"), - "shareMyVerificationID": m66, + "shareMyVerificationID": m71, "shareOnlyWithThePeopleYouWant": MessageLookupByLibrary.simpleMessage( "Share only with the people you want"), "shareTextConfirmOthersVerificationID": m7, "shareTextRecommendUsingEnte": MessageLookupByLibrary.simpleMessage( "Download Ente so we can easily share original quality photos and videos\n\nhttps://ente.io"), - "shareTextReferralCode": m67, + "shareTextReferralCode": m72, "shareWithNonenteUsers": MessageLookupByLibrary.simpleMessage("Share with non-Ente users"), - "shareWithPeopleSectionTitle": m68, + "shareWithPeopleSectionTitle": m73, "shareYourFirstAlbum": MessageLookupByLibrary.simpleMessage("Share your first album"), "sharedAlbumSectionDescription": MessageLookupByLibrary.simpleMessage( @@ -1693,7 +1695,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": m69, + "sharedWith": m74, "sharedWithMe": MessageLookupByLibrary.simpleMessage("Shared with me"), "sharedWithYou": MessageLookupByLibrary.simpleMessage("Shared with you"), @@ -1708,11 +1710,11 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Sign out other devices"), "signUpTerms": MessageLookupByLibrary.simpleMessage( "I agree to the terms of service and privacy policy"), - "singleFileDeleteFromDevice": m70, + "singleFileDeleteFromDevice": m75, "singleFileDeleteHighlight": MessageLookupByLibrary.simpleMessage( "It will be deleted from all albums."), - "singleFileInBothLocalAndRemote": m71, - "singleFileInRemoteOnly": m72, + "singleFileInBothLocalAndRemote": m76, + "singleFileInRemoteOnly": m77, "skip": MessageLookupByLibrary.simpleMessage("Skip"), "social": MessageLookupByLibrary.simpleMessage("Social"), "someItemsAreInBothEnteAndYourDevice": @@ -1760,11 +1762,11 @@ class MessageLookup extends MessageLookupByLibrary { "storageInGB": m1, "storageLimitExceeded": MessageLookupByLibrary.simpleMessage("Storage limit exceeded"), - "storageUsageInfo": m73, + "storageUsageInfo": m78, "streamDetails": MessageLookupByLibrary.simpleMessage("Stream details"), "strongStrength": MessageLookupByLibrary.simpleMessage("Strong"), - "subAlreadyLinkedErrMessage": m74, - "subWillBeCancelledOn": m75, + "subAlreadyLinkedErrMessage": m79, + "subWillBeCancelledOn": m80, "subscribe": MessageLookupByLibrary.simpleMessage("Subscribe"), "subscribeToEnableSharing": MessageLookupByLibrary.simpleMessage( "You need an active paid subscription to enable sharing."), @@ -1781,7 +1783,7 @@ class MessageLookup extends MessageLookupByLibrary { "suggestFeatures": MessageLookupByLibrary.simpleMessage("Suggest features"), "support": MessageLookupByLibrary.simpleMessage("Support"), - "syncProgress": m76, + "syncProgress": m81, "syncStopped": MessageLookupByLibrary.simpleMessage("Sync stopped"), "syncing": MessageLookupByLibrary.simpleMessage("Syncing..."), "systemTheme": MessageLookupByLibrary.simpleMessage("System"), @@ -1790,7 +1792,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Tap to enter code"), "tapToUnlock": MessageLookupByLibrary.simpleMessage("Tap to unlock"), "tapToUpload": MessageLookupByLibrary.simpleMessage("Tap to upload"), - "tapToUploadIsIgnoredDue": m77, + "tapToUploadIsIgnoredDue": m82, "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"), @@ -1831,7 +1833,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("This image has no exif data"), "thisIsMeExclamation": MessageLookupByLibrary.simpleMessage("This is me!"), - "thisIsPersonVerificationId": m78, + "thisIsPersonVerificationId": m83, "thisIsYourVerificationId": MessageLookupByLibrary.simpleMessage( "This is your Verification ID"), "thisWillLogYouOutOfTheFollowingDevice": @@ -1855,11 +1857,11 @@ class MessageLookup extends MessageLookupByLibrary { "total": MessageLookupByLibrary.simpleMessage("total"), "totalSize": MessageLookupByLibrary.simpleMessage("Total size"), "trash": MessageLookupByLibrary.simpleMessage("Trash"), - "trashDaysLeft": m79, + "trashDaysLeft": m84, "trim": MessageLookupByLibrary.simpleMessage("Trim"), "trustedContacts": MessageLookupByLibrary.simpleMessage("Trusted contacts"), - "trustedInviteBody": m80, + "trustedInviteBody": m85, "tryAgain": MessageLookupByLibrary.simpleMessage("Try again"), "turnOnBackupForAutoUpload": MessageLookupByLibrary.simpleMessage( "Turn on backup to automatically upload files added to this device folder to Ente."), @@ -1877,7 +1879,7 @@ class MessageLookup extends MessageLookupByLibrary { "Two-factor authentication successfully reset"), "twofactorSetup": MessageLookupByLibrary.simpleMessage("Two-factor setup"), - "typeOfGallerGallerytypeIsNotSupportedForRename": m81, + "typeOfGallerGallerytypeIsNotSupportedForRename": m86, "unarchive": MessageLookupByLibrary.simpleMessage("Unarchive"), "unarchiveAlbum": MessageLookupByLibrary.simpleMessage("Unarchive album"), @@ -1900,10 +1902,10 @@ class MessageLookup extends MessageLookupByLibrary { "updatingFolderSelection": MessageLookupByLibrary.simpleMessage( "Updating folder selection..."), "upgrade": MessageLookupByLibrary.simpleMessage("Upgrade"), - "uploadIsIgnoredDueToIgnorereason": m82, + "uploadIsIgnoredDueToIgnorereason": m87, "uploadingFilesToAlbum": MessageLookupByLibrary.simpleMessage("Uploading files to album..."), - "uploadingMultipleMemories": m83, + "uploadingMultipleMemories": m88, "uploadingSingleMemory": MessageLookupByLibrary.simpleMessage("Preserving 1 memory..."), "upto50OffUntil4thDec": MessageLookupByLibrary.simpleMessage( @@ -1921,7 +1923,7 @@ class MessageLookup extends MessageLookupByLibrary { "useSelectedPhoto": MessageLookupByLibrary.simpleMessage("Use selected photo"), "usedSpace": MessageLookupByLibrary.simpleMessage("Used space"), - "validTill": m84, + "validTill": m89, "verificationFailedPleaseTryAgain": MessageLookupByLibrary.simpleMessage( "Verification failed, please try again"), @@ -1929,7 +1931,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Verification ID"), "verify": MessageLookupByLibrary.simpleMessage("Verify"), "verifyEmail": MessageLookupByLibrary.simpleMessage("Verify email"), - "verifyEmailID": m85, + "verifyEmailID": m90, "verifyIDLabel": MessageLookupByLibrary.simpleMessage("Verify"), "verifyPasskey": MessageLookupByLibrary.simpleMessage("Verify passkey"), "verifyPassword": @@ -1955,7 +1957,7 @@ class MessageLookup extends MessageLookupByLibrary { "viewRecoveryKey": MessageLookupByLibrary.simpleMessage("View recovery key"), "viewer": MessageLookupByLibrary.simpleMessage("Viewer"), - "viewersSuccessfullyAdded": m86, + "viewersSuccessfullyAdded": m91, "visitWebToManage": MessageLookupByLibrary.simpleMessage( "Please visit web.ente.io to manage your subscription"), "waitingForVerification": @@ -1976,7 +1978,7 @@ class MessageLookup extends MessageLookupByLibrary { "Trusted contact can help in recovering your data."), "yearShort": MessageLookupByLibrary.simpleMessage("yr"), "yearly": MessageLookupByLibrary.simpleMessage("Yearly"), - "yearsAgo": m87, + "yearsAgo": m92, "yes": MessageLookupByLibrary.simpleMessage("Yes"), "yesCancel": MessageLookupByLibrary.simpleMessage("Yes, cancel"), "yesConvertToViewer": @@ -2008,7 +2010,7 @@ class MessageLookup extends MessageLookupByLibrary { "You cannot share with yourself"), "youDontHaveAnyArchivedItems": MessageLookupByLibrary.simpleMessage( "You don\'t have any archived items."), - "youHaveSuccessfullyFreedUp": m88, + "youHaveSuccessfullyFreedUp": m93, "yourAccountHasBeenDeleted": MessageLookupByLibrary.simpleMessage( "Your account has been deleted"), "yourMap": MessageLookupByLibrary.simpleMessage("Your map"), @@ -2031,7 +2033,7 @@ class MessageLookup extends MessageLookupByLibrary { "Your verification code has expired"), "youveNoDuplicateFilesThatCanBeCleared": MessageLookupByLibrary.simpleMessage( - "You\'ve no duplicate files that can be cleared"), + "You don\'t have any duplicate files that can be cleared"), "youveNoFilesInThisAlbumThatCanBeDeleted": MessageLookupByLibrary.simpleMessage( "You\'ve no files in this album that can be deleted"), diff --git a/mobile/lib/generated/intl/messages_es.dart b/mobile/lib/generated/intl/messages_es.dart index 1d44802a78..2571b435ba 100644 --- a/mobile/lib/generated/intl/messages_es.dart +++ b/mobile/lib/generated/intl/messages_es.dart @@ -20,37 +20,39 @@ typedef String MessageIfAbsent(String messageStr, List args); class MessageLookup extends MessageLookupByLibrary { String get localeName => 'es'; - static String m9(count) => - "${Intl.plural(count, zero: 'Añadir colaborador', one: 'Añadir colaborador', other: 'Añadir colaboradores')}"; + static String m9(title) => "${title} (Yo)"; static String m10(count) => + "${Intl.plural(count, zero: 'Añadir colaborador', one: 'Añadir colaborador', other: 'Añadir colaboradores')}"; + + static String m11(count) => "${Intl.plural(count, one: 'Agregar elemento', other: 'Agregar elementos')}"; - static String m11(storageAmount, endDate) => + static String m12(storageAmount, endDate) => "Tu ${storageAmount} adicional es válido hasta ${endDate}"; - static String m12(count) => + static String m13(count) => "${Intl.plural(count, zero: 'Añadir espectador', one: 'Añadir espectador', other: 'Añadir espectadores')}"; - static String m13(emailOrName) => "Añadido por ${emailOrName}"; + static String m14(emailOrName) => "Añadido por ${emailOrName}"; - static String m14(albumName) => "Añadido exitosamente a ${albumName}"; + static String m15(albumName) => "Añadido exitosamente a ${albumName}"; - static String m15(count) => + static String m16(count) => "${Intl.plural(count, zero: 'No hay Participantes', one: '1 Participante', other: '${count} Participantes')}"; - static String m16(versionValue) => "Versión: ${versionValue}"; + static String m17(versionValue) => "Versión: ${versionValue}"; - static String m17(freeAmount, storageUnit) => + static String m18(freeAmount, storageUnit) => "${freeAmount} ${storageUnit} gratis"; - static String m18(paymentProvider) => + static String m19(paymentProvider) => "Por favor, cancela primero tu suscripción existente de ${paymentProvider}"; static String m3(user) => "${user} no podrá añadir más fotos a este álbum\n\nTodavía podrán eliminar las fotos ya añadidas por ellos"; - static String m19(isFamilyMember, storageAmountInGb) => + static String m20(isFamilyMember, storageAmountInGb) => "${Intl.select(isFamilyMember, { 'true': 'Tu familia ha obtenido ${storageAmountInGb} GB hasta el momento', @@ -59,213 +61,222 @@ class MessageLookup extends MessageLookupByLibrary { '¡Tú has obtenido ${storageAmountInGb} GB hasta el momento!', })}"; - static String m20(albumName) => + static String m21(albumName) => "Enlace colaborativo creado para ${albumName}"; - static String m21(count) => + static String m22(count) => "${Intl.plural(count, zero: '0 colaboradores añadidos', one: '1 colaborador añadido', other: '${count} colaboradores añadidos')}"; - static String m22(email, numOfDays) => + static String m23(email, numOfDays) => "Estás a punto de añadir ${email} como un contacto de confianza. Esta persona podrá recuperar tu cuenta si no estás durante ${numOfDays} días."; - static String m23(familyAdminEmail) => + static String m24(familyAdminEmail) => "Por favor contacta con ${familyAdminEmail} para administrar tu suscripción"; - static String m24(provider) => + static String m25(provider) => "Por favor, contáctanos en support@ente.io para gestionar tu suscripción a ${provider}."; - static String m25(endpoint) => "Conectado a ${endpoint}"; + static String m26(endpoint) => "Conectado a ${endpoint}"; - static String m26(count) => + static String m27(count) => "${Intl.plural(count, one: 'Elimina ${count} elemento', other: 'Elimina ${count} elementos')}"; - static String m27(currentlyDeleting, totalCount) => + static String m28(currentlyDeleting, totalCount) => "Borrando ${currentlyDeleting} / ${totalCount}"; - static String m28(albumName) => + static String m29(albumName) => "Esto eliminará el enlace público para acceder a \"${albumName}\"."; - static String m29(supportEmail) => + static String m30(supportEmail) => "Por favor, envía un correo electrónico a ${supportEmail} desde tu dirección de correo electrónico que usó para registrarse"; - static String m30(count, storageSaved) => + static String m31(count, storageSaved) => "¡Has limpiado ${Intl.plural(count, one: '${count} archivo duplicado', other: '${count} archivos duplicados')}, ahorrando (${storageSaved}!)"; - static String m31(count, formattedSize) => + static String m32(count, formattedSize) => "${count} archivos, ${formattedSize} cada uno"; - static String m32(newEmail) => "Correo cambiado a ${newEmail}"; + static String m33(newEmail) => "Correo cambiado a ${newEmail}"; - static String m33(email) => + static String m34(email) => "${email} no tiene una cuenta de Ente."; + + static String m35(email) => "${email} no tiene una cuente en Ente.\n\nEnvíale una invitación para compartir fotos."; - static String m34(text) => "Fotos adicionales encontradas para ${text}"; + static String m36(text) => "Fotos adicionales encontradas para ${text}"; - static String m35(count, formattedNumber) => + static String m37(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 m36(count, formattedNumber) => + static String m38(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 m4(storageAmountInGB) => "${storageAmountInGB} GB cada vez que alguien se registra en un plan de pago y aplica tu código"; - static String m37(endDate) => "Prueba gratuita válida hasta ${endDate}"; + static String m39(endDate) => "Prueba gratuita válida hasta ${endDate}"; - static String m38(count) => + static String m40(count) => "Aún puedes acceder ${Intl.plural(count, one: 'a él', other: 'a ellos')} en Ente mientras tengas una suscripción activa"; - static String m39(sizeInMBorGB) => "Liberar ${sizeInMBorGB}"; + static String m41(sizeInMBorGB) => "Liberar ${sizeInMBorGB}"; - static String m40(count, formattedSize) => + static String m42(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 m41(currentlyProcessing, totalCount) => + static String m43(currentlyProcessing, totalCount) => "Procesando ${currentlyProcessing} / ${totalCount}"; - static String m42(count) => + static String m44(count) => "${Intl.plural(count, one: '${count} elemento', other: '${count} elementos')}"; - static String m43(email) => + static String m45(email) => "${email} te ha invitado a ser un contacto de confianza"; - static String m44(expiryTime) => "El enlace caducará en ${expiryTime}"; + static String m46(expiryTime) => "El enlace caducará en ${expiryTime}"; + + static String m47(email) => "Enlazar persona a ${email}"; + + static String m48(personName, email) => + "Esto enlazará a ${personName} a ${email}"; static String m5(count, formattedCount) => "${Intl.plural(count, zero: 'sin recuerdos', one: '${formattedCount} recuerdo', other: '${formattedCount} recuerdos')}"; - static String m45(count) => + static String m49(count) => "${Intl.plural(count, one: 'Mover elemento', other: 'Mover elementos')}"; - static String m46(albumName) => "Movido exitosamente a ${albumName}"; + static String m50(albumName) => "Movido exitosamente a ${albumName}"; - static String m47(personName) => "No hay sugerencias para ${personName}"; + static String m51(personName) => "No hay sugerencias para ${personName}"; - static String m48(name) => "¿No es ${name}?"; + static String m52(name) => "¿No es ${name}?"; - static String m49(familyAdminEmail) => + static String m53(familyAdminEmail) => "Por favor, contacta a ${familyAdminEmail} para cambiar tu código."; static String m0(passwordStrengthValue) => "Seguridad de la contraseña: ${passwordStrengthValue}"; - static String m50(providerName) => + static String m54(providerName) => "Por favor, habla con el soporte de ${providerName} si se te cobró"; - static String m51(count) => + static String m55(count) => "${Intl.plural(count, zero: '0 fotos', one: '1 foto', other: '${count} fotos')}"; - static String m52(endDate) => + static String m56(endDate) => "Prueba gratuita válida hasta ${endDate}.\nPuedes elegir un plan de pago después."; - static String m53(toEmail) => + static String m57(toEmail) => "Por favor, envíanos un correo electrónico a ${toEmail}"; - static String m54(toEmail) => "Por favor, envía los registros a ${toEmail}"; + static String m58(toEmail) => "Por favor, envía los registros a ${toEmail}"; - static String m55(folderName) => "Procesando ${folderName}..."; + static String m59(folderName) => "Procesando ${folderName}..."; - static String m56(storeName) => "Puntúanos en ${storeName}"; + static String m60(storeName) => "Puntúanos en ${storeName}"; - static String m57(days, email) => + static String m61(name) => "Te has reasignado a ${name}"; + + static String m62(days, email) => "Puedes acceder a la cuenta después de ${days} días. Se enviará una notificación a ${email}."; - static String m58(email) => + static String m63(email) => "Ahora puedes recuperar la cuenta de ${email} estableciendo una nueva contraseña."; - static String m59(email) => "${email} está intentando recuperar tu cuenta."; + static String m64(email) => "${email} está intentando recuperar tu cuenta."; - static String m60(storageInGB) => + static String m65(storageInGB) => "3. Ambos obtienen ${storageInGB} GB* gratis"; - static String m61(userEmail) => + static String m66(userEmail) => "${userEmail} será eliminado de este álbum compartido\n\nCualquier foto añadida por ellos también será eliminada del álbum"; - static String m62(endDate) => "La suscripción se renueva el ${endDate}"; + static String m67(endDate) => "La suscripción se renueva el ${endDate}"; - static String m63(count) => + static String m68(count) => "${Intl.plural(count, one: '${count} resultado encontrado', other: '${count} resultados encontrados')}"; - static String m64(snapshotLength, searchLength) => + static String m69(snapshotLength, searchLength) => "La longitud de las secciones no coincide: ${snapshotLength} != ${searchLength}"; static String m6(count) => "${count} seleccionados"; - static String m65(count, yourCount) => + static String m70(count, yourCount) => "${count} seleccionados (${yourCount} tuyos)"; - static String m66(verificationID) => + static String m71(verificationID) => "Aquí está mi ID de verificación: ${verificationID} para ente.io."; static String m7(verificationID) => "Hola, ¿puedes confirmar que esta es tu ID de verificación ente.io: ${verificationID}?"; - static String m67(referralCode, referralStorageInGB) => + static String m72(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 m68(numberOfPeople) => + static String m73(numberOfPeople) => "${Intl.plural(numberOfPeople, zero: 'Compartir con personas específicas', one: 'Compartido con 1 persona', other: 'Compartido con ${numberOfPeople} personas')}"; - static String m69(emailIDs) => "Compartido con ${emailIDs}"; + static String m74(emailIDs) => "Compartido con ${emailIDs}"; - static String m70(fileType) => + static String m75(fileType) => "Este ${fileType} se eliminará de tu dispositivo."; - static String m71(fileType) => + static String m76(fileType) => "Este ${fileType} está tanto en Ente como en tu dispositivo."; - static String m72(fileType) => "Este ${fileType} será eliminado de Ente."; + static String m77(fileType) => "Este ${fileType} será eliminado de Ente."; static String m1(storageAmountInGB) => "${storageAmountInGB} GB"; - static String m73( + static String m78( usedAmount, usedStorageUnit, totalAmount, totalStorageUnit) => "${usedAmount} ${usedStorageUnit} de ${totalAmount} ${totalStorageUnit} usados"; - static String m74(id) => + static String m79(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 m75(endDate) => "Tu suscripción se cancelará el ${endDate}"; + static String m80(endDate) => "Tu suscripción se cancelará el ${endDate}"; - static String m76(completed, total) => + static String m81(completed, total) => "${completed}/${total} recuerdos conservados"; - static String m77(ignoreReason) => + static String m82(ignoreReason) => "Toca para subir, la subida se está ignorando debido a ${ignoreReason}"; static String m8(storageAmountInGB) => "También obtienen ${storageAmountInGB} GB"; - static String m78(email) => "Este es el ID de verificación de ${email}"; + static String m83(email) => "Este es el ID de verificación de ${email}"; - static String m79(count) => + static String m84(count) => "${Intl.plural(count, zero: 'Pronto', one: '1 día', other: '${count} días')}"; - static String m80(email) => + static String m85(email) => "Has sido invitado a ser un contacto legado por ${email}."; - static String m81(galleryType) => + static String m86(galleryType) => "El tipo de galería ${galleryType} no es compatible con el renombrado"; - static String m82(ignoreReason) => + static String m87(ignoreReason) => "La subida se ignoró debido a ${ignoreReason}"; - static String m83(count) => "Preservando ${count} memorias..."; + static String m88(count) => "Preservando ${count} memorias..."; - static String m84(endDate) => "Válido hasta ${endDate}"; + static String m89(endDate) => "Válido hasta ${endDate}"; - static String m85(email) => "Verificar ${email}"; + static String m90(email) => "Verificar ${email}"; - static String m86(count) => + static String m91(count) => "${Intl.plural(count, zero: '0 espectadores añadidos', one: '1 espectador añadido', other: '${count} espectadores añadidos')}"; static String m2(email) => "Hemos enviado un correo a ${email}"; - static String m87(count) => + static String m92(count) => "${Intl.plural(count, one: 'Hace ${count} año', other: 'Hace ${count} años')}"; - static String m88(storageSaved) => "¡Has liberado ${storageSaved} con éxito!"; + static String m93(storageSaved) => "¡Has liberado ${storageSaved} con éxito!"; final messages = _notInlinedMessages(_notInlinedMessages); static Map _notInlinedMessages(_) => { @@ -277,6 +288,7 @@ class MessageLookup extends MessageLookupByLibrary { "account": MessageLookupByLibrary.simpleMessage("Cuenta"), "accountIsAlreadyConfigured": MessageLookupByLibrary.simpleMessage( "La cuenta ya está configurada."), + "accountOwnerPersonAppbarTitle": m9, "accountWelcomeBack": MessageLookupByLibrary.simpleMessage("¡Bienvenido de nuevo!"), "ackPasswordLostWarning": MessageLookupByLibrary.simpleMessage( @@ -289,11 +301,11 @@ class MessageLookup extends MessageLookupByLibrary { "Agregar nuevo correo electrónico"), "addCollaborator": MessageLookupByLibrary.simpleMessage("Agregar colaborador"), - "addCollaborators": m9, + "addCollaborators": m10, "addFiles": MessageLookupByLibrary.simpleMessage("Añadir archivos"), "addFromDevice": MessageLookupByLibrary.simpleMessage( "Agregar desde el dispositivo"), - "addItem": m10, + "addItem": m11, "addLocation": MessageLookupByLibrary.simpleMessage("Agregar ubicación"), "addLocationButton": MessageLookupByLibrary.simpleMessage("Añadir"), @@ -306,7 +318,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Añadir nueva persona"), "addOnPageSubtitle": MessageLookupByLibrary.simpleMessage( "Detalles de los complementos"), - "addOnValidTill": m11, + "addOnValidTill": m12, "addOns": MessageLookupByLibrary.simpleMessage("Complementos"), "addPhotos": MessageLookupByLibrary.simpleMessage("Agregar fotos"), "addSelected": @@ -318,12 +330,12 @@ class MessageLookup extends MessageLookupByLibrary { "addTrustedContact": MessageLookupByLibrary.simpleMessage( "Añadir contacto de confianza"), "addViewer": MessageLookupByLibrary.simpleMessage("Añadir espectador"), - "addViewers": m12, + "addViewers": m13, "addYourPhotosNow": MessageLookupByLibrary.simpleMessage("Añade tus fotos ahora"), "addedAs": MessageLookupByLibrary.simpleMessage("Agregado como"), - "addedBy": m13, - "addedSuccessfullyTo": m14, + "addedBy": m14, + "addedSuccessfullyTo": m15, "addingToFavorites": MessageLookupByLibrary.simpleMessage("Añadiendo a favoritos..."), "advanced": MessageLookupByLibrary.simpleMessage("Avanzado"), @@ -336,7 +348,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Después de una semana"), "after1Year": MessageLookupByLibrary.simpleMessage("Después de un año"), "albumOwner": MessageLookupByLibrary.simpleMessage("Propietario"), - "albumParticipantsCount": m15, + "albumParticipantsCount": m16, "albumTitle": MessageLookupByLibrary.simpleMessage("Título del álbum"), "albumUpdated": MessageLookupByLibrary.simpleMessage("Álbum actualizado"), @@ -386,7 +398,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Bloqueo de aplicación"), "appLockDescriptions": MessageLookupByLibrary.simpleMessage( "Escoge entre la pantalla de bloqueo por defecto de tu dispositivo y una pantalla de bloqueo personalizada con un PIN o contraseña."), - "appVersion": m16, + "appVersion": m17, "appleId": MessageLookupByLibrary.simpleMessage("ID de Apple"), "apply": MessageLookupByLibrary.simpleMessage("Aplicar"), "applyCodeTitle": MessageLookupByLibrary.simpleMessage("Usar código"), @@ -469,7 +481,7 @@ class MessageLookup extends MessageLookupByLibrary { "autoPairDesc": MessageLookupByLibrary.simpleMessage( "El emparejamiento automático funciona sólo con dispositivos compatibles con Chromecast."), "available": MessageLookupByLibrary.simpleMessage("Disponible"), - "availableStorageSpace": m17, + "availableStorageSpace": m18, "backedUpFolders": MessageLookupByLibrary.simpleMessage( "Carpetas con copia de seguridad"), "backup": MessageLookupByLibrary.simpleMessage("Copia de seguridad"), @@ -511,7 +523,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Cancelar la recuperación"), "cancelAccountRecoveryBody": MessageLookupByLibrary.simpleMessage( "¿Estás seguro de que quieres cancelar la recuperación?"), - "cancelOtherSubscription": m18, + "cancelOtherSubscription": m19, "cancelSubscription": MessageLookupByLibrary.simpleMessage("Cancelar suscripción"), "cannotAddMorePhotosAfterBecomingViewer": m3, @@ -564,7 +576,7 @@ class MessageLookup extends MessageLookupByLibrary { "Obtén almacenamiento gratuito"), "claimMore": MessageLookupByLibrary.simpleMessage("¡Obtén más!"), "claimed": MessageLookupByLibrary.simpleMessage("Obtenido"), - "claimedStorageSoFar": m19, + "claimedStorageSoFar": m20, "cleanUncategorized": MessageLookupByLibrary.simpleMessage("Limpiar sin categorizar"), "cleanUncategorizedDescription": MessageLookupByLibrary.simpleMessage( @@ -593,12 +605,12 @@ class MessageLookup extends MessageLookupByLibrary { "Crea un enlace para permitir que otros pueda añadir y ver fotos en tu álbum compartido sin necesitar la aplicación Ente o una cuenta. Genial para recolectar fotos de eventos."), "collaborativeLink": MessageLookupByLibrary.simpleMessage("Enlace colaborativo"), - "collaborativeLinkCreatedFor": m20, + "collaborativeLinkCreatedFor": m21, "collaborator": MessageLookupByLibrary.simpleMessage("Colaborador"), "collaboratorsCanAddPhotosAndVideosToTheSharedAlbum": MessageLookupByLibrary.simpleMessage( "Colaboradores pueden añadir fotos y videos al álbum compartido."), - "collaboratorsSuccessfullyAdded": m21, + "collaboratorsSuccessfullyAdded": m22, "collageLayout": MessageLookupByLibrary.simpleMessage("Disposición"), "collageSaved": MessageLookupByLibrary.simpleMessage( "Collage guardado en la galería"), @@ -616,7 +628,7 @@ class MessageLookup extends MessageLookupByLibrary { "¿Estás seguro de que deseas deshabilitar la autenticación de doble factor?"), "confirmAccountDeletion": MessageLookupByLibrary.simpleMessage("Confirmar borrado de cuenta"), - "confirmAddingTrustedContact": m22, + "confirmAddingTrustedContact": m23, "confirmDeletePrompt": MessageLookupByLibrary.simpleMessage( "Sí, quiero eliminar permanentemente esta cuenta y todos sus datos en todas las aplicaciones."), "confirmPassword": @@ -629,10 +641,10 @@ class MessageLookup extends MessageLookupByLibrary { "Confirma tu clave de recuperación"), "connectToDevice": MessageLookupByLibrary.simpleMessage("Conectar a dispositivo"), - "contactFamilyAdmin": m23, + "contactFamilyAdmin": m24, "contactSupport": MessageLookupByLibrary.simpleMessage("Contactar con soporte"), - "contactToManageSubscription": m24, + "contactToManageSubscription": m25, "contacts": MessageLookupByLibrary.simpleMessage("Contactos"), "contents": MessageLookupByLibrary.simpleMessage("Contenidos"), "continueLabel": MessageLookupByLibrary.simpleMessage("Continuar"), @@ -678,7 +690,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("El uso actual es de "), "currentlyRunning": MessageLookupByLibrary.simpleMessage("ejecutando"), "custom": MessageLookupByLibrary.simpleMessage("Personalizado"), - "customEndpoint": m25, + "customEndpoint": m26, "darkTheme": MessageLookupByLibrary.simpleMessage("Oscuro"), "dayToday": MessageLookupByLibrary.simpleMessage("Hoy"), "dayYesterday": MessageLookupByLibrary.simpleMessage("Ayer"), @@ -716,12 +728,12 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Eliminar del dispositivo"), "deleteFromEnte": MessageLookupByLibrary.simpleMessage("Eliminar de Ente"), - "deleteItemCount": m26, + "deleteItemCount": m27, "deleteLocation": MessageLookupByLibrary.simpleMessage("Borrar la ubicación"), "deletePhotos": MessageLookupByLibrary.simpleMessage("Borrar las fotos"), - "deleteProgress": m27, + "deleteProgress": m28, "deleteReason1": MessageLookupByLibrary.simpleMessage( "Falta una característica clave que necesito"), "deleteReason2": MessageLookupByLibrary.simpleMessage( @@ -762,7 +774,7 @@ class MessageLookup extends MessageLookupByLibrary { "Los espectadores todavía pueden tomar capturas de pantalla o guardar una copia de tus fotos usando herramientas externas"), "disableDownloadWarningTitle": MessageLookupByLibrary.simpleMessage("Por favor, ten en cuenta"), - "disableLinkMessage": m28, + "disableLinkMessage": m29, "disableTwofactor": MessageLookupByLibrary.simpleMessage("Deshabilitar dos factores"), "disablingTwofactorAuthentication": @@ -799,15 +811,16 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage( "¿Quieres descartar las ediciones que has hecho?"), "done": MessageLookupByLibrary.simpleMessage("Hecho"), + "dontSave": MessageLookupByLibrary.simpleMessage("No guardar"), "doubleYourStorage": MessageLookupByLibrary.simpleMessage("Duplica tu almacenamiento"), "download": MessageLookupByLibrary.simpleMessage("Descargar"), "downloadFailed": MessageLookupByLibrary.simpleMessage("Descarga fallida"), "downloading": MessageLookupByLibrary.simpleMessage("Descargando..."), - "dropSupportEmail": m29, - "duplicateFileCountWithStorageSaved": m30, - "duplicateItemsGroup": m31, + "dropSupportEmail": m30, + "duplicateFileCountWithStorageSaved": m31, + "duplicateItemsGroup": m32, "edit": MessageLookupByLibrary.simpleMessage("Editar"), "editLocation": MessageLookupByLibrary.simpleMessage("Editar la ubicación"), @@ -823,8 +836,9 @@ class MessageLookup extends MessageLookupByLibrary { "email": MessageLookupByLibrary.simpleMessage("Correo electrónico"), "emailAlreadyRegistered": MessageLookupByLibrary.simpleMessage( "Correo electrónico ya registrado."), - "emailChangedTo": m32, - "emailNoEnteAccount": m33, + "emailChangedTo": m33, + "emailDoesNotHaveEnteAccount": m34, + "emailNoEnteAccount": m35, "emailNotRegistered": MessageLookupByLibrary.simpleMessage( "Correo electrónico no registrado."), "emailVerificationToggle": MessageLookupByLibrary.simpleMessage( @@ -912,12 +926,13 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Exportar tus datos"), "extraPhotosFound": MessageLookupByLibrary.simpleMessage( "Fotos adicionales encontradas"), - "extraPhotosFoundFor": m34, + "extraPhotosFoundFor": m36, "faceNotClusteredYet": MessageLookupByLibrary.simpleMessage( "Cara no agrupada todavía, por favor vuelve más tarde"), "faceRecognition": MessageLookupByLibrary.simpleMessage("Reconocimiento facial"), "faces": MessageLookupByLibrary.simpleMessage("Caras"), + "failed": MessageLookupByLibrary.simpleMessage("Fallido"), "failedToApplyCode": MessageLookupByLibrary.simpleMessage("Error al aplicar el código"), "failedToCancel": @@ -963,8 +978,8 @@ class MessageLookup extends MessageLookupByLibrary { "fileTypes": MessageLookupByLibrary.simpleMessage("Tipos de archivos"), "fileTypesAndNames": MessageLookupByLibrary.simpleMessage("Tipos de archivo y nombres"), - "filesBackedUpFromDevice": m35, - "filesBackedUpInAlbum": m36, + "filesBackedUpFromDevice": m37, + "filesBackedUpInAlbum": m38, "filesDeleted": MessageLookupByLibrary.simpleMessage("Archivos eliminados"), "filesSavedToGallery": MessageLookupByLibrary.simpleMessage( @@ -985,22 +1000,22 @@ class MessageLookup extends MessageLookupByLibrary { "freeStorageUsable": MessageLookupByLibrary.simpleMessage( "Almacenamiento libre disponible"), "freeTrial": MessageLookupByLibrary.simpleMessage("Prueba gratuita"), - "freeTrialValidTill": m37, - "freeUpAccessPostDelete": m38, - "freeUpAmount": m39, + "freeTrialValidTill": m39, + "freeUpAccessPostDelete": m40, + "freeUpAmount": m41, "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": m40, + "freeUpSpaceSaving": m42, "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": m41, + "genericProgress": m43, "goToSettings": MessageLookupByLibrary.simpleMessage("Ir a Ajustes"), "googlePlayId": MessageLookupByLibrary.simpleMessage("ID de Google Play"), @@ -1061,6 +1076,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Elementos indexados"), "indexingIsPaused": MessageLookupByLibrary.simpleMessage( "La indexación está pausada. Se reanudará automáticamente cuando el dispositivo esté listo."), + "ineligible": MessageLookupByLibrary.simpleMessage("Inelegible"), "info": MessageLookupByLibrary.simpleMessage("Info"), "insecureDevice": MessageLookupByLibrary.simpleMessage("Dispositivo inseguro"), @@ -1084,7 +1100,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": m44, "itemsShowTheNumberOfDaysRemainingBeforePermanentDeletion": MessageLookupByLibrary.simpleMessage( "Los artículos muestran el número de días restantes antes de ser borrados permanente"), @@ -1092,6 +1108,8 @@ class MessageLookup extends MessageLookupByLibrary { "Los elementos seleccionados serán eliminados de este álbum"), "join": MessageLookupByLibrary.simpleMessage("Unir"), "joinAlbum": MessageLookupByLibrary.simpleMessage("Unir álbum"), + "joinAlbumConfirmationDialogBody": MessageLookupByLibrary.simpleMessage( + "Unirse a un álbum hará visible tu correo electrónico a sus participantes."), "joinAlbumSubtext": MessageLookupByLibrary.simpleMessage("para ver y añadir tus fotos"), "joinAlbumSubtextViewer": MessageLookupByLibrary.simpleMessage( @@ -1115,7 +1133,7 @@ class MessageLookup extends MessageLookupByLibrary { "legacy": MessageLookupByLibrary.simpleMessage("Legado"), "legacyAccounts": MessageLookupByLibrary.simpleMessage("Cuentas legadas"), - "legacyInvite": m43, + "legacyInvite": m45, "legacyPageDesc": MessageLookupByLibrary.simpleMessage( "Legado permite a los contactos de confianza acceder a su cuenta en su ausencia."), "legacyPageDesc2": MessageLookupByLibrary.simpleMessage( @@ -1129,13 +1147,20 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Límite del dispositivo"), "linkEmail": MessageLookupByLibrary.simpleMessage("Vincular correo electrónico"), + "linkEmailToContactBannerCaption": + MessageLookupByLibrary.simpleMessage("para compartir más rápido"), "linkEnabled": MessageLookupByLibrary.simpleMessage("Habilitado"), "linkExpired": MessageLookupByLibrary.simpleMessage("Vencido"), - "linkExpiresOn": m44, + "linkExpiresOn": m46, "linkExpiry": MessageLookupByLibrary.simpleMessage("Enlace vence"), "linkHasExpired": MessageLookupByLibrary.simpleMessage("El enlace ha caducado"), "linkNeverExpires": MessageLookupByLibrary.simpleMessage("Nunca"), + "linkPerson": MessageLookupByLibrary.simpleMessage("Vincular persona"), + "linkPersonCaption": MessageLookupByLibrary.simpleMessage( + "para una mejor experiencia compartida"), + "linkPersonToEmail": m47, + "linkPersonToEmailConfirmation": m48, "livePhotos": MessageLookupByLibrary.simpleMessage("Foto en vivo"), "loadMessage1": MessageLookupByLibrary.simpleMessage( "Puedes compartir tu suscripción con tu familia"), @@ -1228,6 +1253,7 @@ class MessageLookup extends MessageLookupByLibrary { "maps": MessageLookupByLibrary.simpleMessage("Mapas"), "mastodon": MessageLookupByLibrary.simpleMessage("Mastodon"), "matrix": MessageLookupByLibrary.simpleMessage("Matrix"), + "me": MessageLookupByLibrary.simpleMessage("Yo"), "memoryCount": m5, "merchandise": MessageLookupByLibrary.simpleMessage("Mercancías"), "mergeWithExisting": @@ -1258,11 +1284,11 @@ class MessageLookup extends MessageLookupByLibrary { "moreDetails": MessageLookupByLibrary.simpleMessage("Más detalles"), "mostRecent": MessageLookupByLibrary.simpleMessage("Más reciente"), "mostRelevant": MessageLookupByLibrary.simpleMessage("Más relevante"), - "moveItem": m45, + "moveItem": m49, "moveToAlbum": MessageLookupByLibrary.simpleMessage("Mover al álbum"), "moveToHiddenAlbum": MessageLookupByLibrary.simpleMessage("Mover al álbum oculto"), - "movedSuccessfullyTo": m46, + "movedSuccessfullyTo": m50, "movedToTrash": MessageLookupByLibrary.simpleMessage("Movido a la papelera"), "movingFilesToAlbum": MessageLookupByLibrary.simpleMessage( @@ -1315,10 +1341,10 @@ class MessageLookup extends MessageLookupByLibrary { "noResults": MessageLookupByLibrary.simpleMessage("Sin resultados"), "noResultsFound": MessageLookupByLibrary.simpleMessage( "No se han encontrado resultados"), - "noSuggestionsForPerson": m47, + "noSuggestionsForPerson": m51, "noSystemLockFound": MessageLookupByLibrary.simpleMessage( "Bloqueo de sistema no encontrado"), - "notPersonLabel": m48, + "notPersonLabel": m52, "nothingSharedWithYouYet": MessageLookupByLibrary.simpleMessage( "Aún no hay nada compartido contigo"), "nothingToSeeHere": MessageLookupByLibrary.simpleMessage( @@ -1328,7 +1354,7 @@ class MessageLookup extends MessageLookupByLibrary { "onDevice": MessageLookupByLibrary.simpleMessage("En el dispositivo"), "onEnte": MessageLookupByLibrary.simpleMessage( "En ente"), - "onlyFamilyAdminCanChangeCode": m49, + "onlyFamilyAdminCanChangeCode": m53, "onlyThem": MessageLookupByLibrary.simpleMessage("Solo ellos"), "oops": MessageLookupByLibrary.simpleMessage("Ups"), "oopsCouldNotSaveEdits": MessageLookupByLibrary.simpleMessage( @@ -1351,8 +1377,8 @@ class MessageLookup extends MessageLookupByLibrary { "O combinar con persona existente"), "orPickAnExistingOne": MessageLookupByLibrary.simpleMessage("O elige uno existente"), - "orPickFromYourContacts": - MessageLookupByLibrary.simpleMessage("or pick from your contacts"), + "orPickFromYourContacts": MessageLookupByLibrary.simpleMessage( + "o elige de entre tus contactos"), "pair": MessageLookupByLibrary.simpleMessage("Emparejar"), "pairWithPin": MessageLookupByLibrary.simpleMessage("Emparejar con PIN"), @@ -1379,7 +1405,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": m50, + "paymentFailedTalkToProvider": m54, "pendingItems": MessageLookupByLibrary.simpleMessage("Elementos pendientes"), "pendingSync": @@ -1404,14 +1430,18 @@ class MessageLookup extends MessageLookupByLibrary { "photosAddedByYouWillBeRemovedFromTheAlbum": MessageLookupByLibrary.simpleMessage( "Las fotos añadidas por ti serán removidas del álbum"), - "photosCount": m51, + "photosCount": m55, "pickCenterPoint": MessageLookupByLibrary.simpleMessage("Elegir punto central"), "pinAlbum": MessageLookupByLibrary.simpleMessage("Fijar álbum"), "pinLock": MessageLookupByLibrary.simpleMessage("Bloqueo con Pin"), "playOnTv": MessageLookupByLibrary.simpleMessage("Reproducir álbum en TV"), - "playStoreFreeTrialValidTill": m52, + "playOriginal": + MessageLookupByLibrary.simpleMessage("Reproducir original"), + "playStoreFreeTrialValidTill": m56, + "playStream": + MessageLookupByLibrary.simpleMessage("Reproducir transmisión"), "playstoreSubscription": MessageLookupByLibrary.simpleMessage("Suscripción en la PlayStore"), "pleaseCheckYourInternetConnectionAndTryAgain": @@ -1423,14 +1453,14 @@ class MessageLookup extends MessageLookupByLibrary { "pleaseContactSupportIfTheProblemPersists": MessageLookupByLibrary.simpleMessage( "Por favor, contacta a soporte técnico si el problema persiste"), - "pleaseEmailUsAt": m53, + "pleaseEmailUsAt": m57, "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": m54, + "pleaseSendTheLogsTo": m58, "pleaseTryAgain": MessageLookupByLibrary.simpleMessage( "Por favor, inténtalo nuevamente"), "pleaseVerifyTheCodeYouHaveEntered": @@ -1459,18 +1489,26 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Compartir en privado"), "proceed": MessageLookupByLibrary.simpleMessage("Continuar"), "processed": MessageLookupByLibrary.simpleMessage("Procesado"), - "processingImport": m55, + "processing": MessageLookupByLibrary.simpleMessage("Procesando"), + "processingImport": m59, + "processingVideos": + MessageLookupByLibrary.simpleMessage("Procesando vídeos"), "publicLinkCreated": MessageLookupByLibrary.simpleMessage("Enlace público creado"), "publicLinkEnabled": MessageLookupByLibrary.simpleMessage("Enlace público habilitado"), + "queued": MessageLookupByLibrary.simpleMessage("En cola"), "quickLinks": MessageLookupByLibrary.simpleMessage("Acceso rápido"), "radius": MessageLookupByLibrary.simpleMessage("Radio"), "raiseTicket": MessageLookupByLibrary.simpleMessage("Generar ticket"), "rateTheApp": MessageLookupByLibrary.simpleMessage("Evalúa la aplicación"), "rateUs": MessageLookupByLibrary.simpleMessage("Califícanos"), - "rateUsOnStore": m56, + "rateUsOnStore": m60, + "reassignMe": MessageLookupByLibrary.simpleMessage("Reasignar \"Yo\""), + "reassignedToName": m61, + "reassigningLoading": + MessageLookupByLibrary.simpleMessage("Reasignando..."), "recover": MessageLookupByLibrary.simpleMessage("Recuperar"), "recoverAccount": MessageLookupByLibrary.simpleMessage("Recuperar cuenta"), @@ -1479,7 +1517,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Recuperar cuenta"), "recoveryInitiated": MessageLookupByLibrary.simpleMessage("Recuperación iniciada"), - "recoveryInitiatedDesc": m57, + "recoveryInitiatedDesc": m62, "recoveryKey": MessageLookupByLibrary.simpleMessage("Clave de recuperación"), "recoveryKeyCopiedToClipboard": MessageLookupByLibrary.simpleMessage( @@ -1494,12 +1532,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": m58, + "recoveryReady": m63, "recoverySuccessful": MessageLookupByLibrary.simpleMessage("¡Recuperación exitosa!"), "recoveryWarning": MessageLookupByLibrary.simpleMessage( "Un contacto de confianza está intentando acceder a tu cuenta"), - "recoveryWarningBody": m59, + "recoveryWarningBody": m64, "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": @@ -1514,7 +1552,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": m60, + "referralStep3": m65, "referrals": MessageLookupByLibrary.simpleMessage("Referidos"), "referralsAreCurrentlyPaused": MessageLookupByLibrary.simpleMessage( "Las referencias están actualmente en pausa"), @@ -1545,7 +1583,7 @@ class MessageLookup extends MessageLookupByLibrary { "removeLink": MessageLookupByLibrary.simpleMessage("Eliminar enlace"), "removeParticipant": MessageLookupByLibrary.simpleMessage("Quitar participante"), - "removeParticipantBody": m61, + "removeParticipantBody": m66, "removePersonLabel": MessageLookupByLibrary.simpleMessage( "Eliminar etiqueta de persona"), "removePublicLink": @@ -1565,7 +1603,7 @@ class MessageLookup extends MessageLookupByLibrary { "renameFile": MessageLookupByLibrary.simpleMessage("Renombrar archivo"), "renewSubscription": MessageLookupByLibrary.simpleMessage("Renovar suscripción"), - "renewsOn": m62, + "renewsOn": m67, "reportABug": MessageLookupByLibrary.simpleMessage("Reportar un error"), "reportBug": MessageLookupByLibrary.simpleMessage("Reportar error"), "resendEmail": @@ -1599,6 +1637,9 @@ class MessageLookup extends MessageLookupByLibrary { "safelyStored": MessageLookupByLibrary.simpleMessage("Almacenado con seguridad"), "save": MessageLookupByLibrary.simpleMessage("Guardar"), + "saveChangesBeforeLeavingQuestion": + MessageLookupByLibrary.simpleMessage( + "¿Guardar cambios antes de salir?"), "saveCollage": MessageLookupByLibrary.simpleMessage("Guardar collage"), "saveCopy": MessageLookupByLibrary.simpleMessage("Guardar copia"), "saveKey": MessageLookupByLibrary.simpleMessage("Guardar Clave"), @@ -1645,8 +1686,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": m63, - "searchSectionsLengthMismatch": m64, + "searchResultCount": m68, + "searchSectionsLengthMismatch": m69, "security": MessageLookupByLibrary.simpleMessage("Seguridad"), "seePublicAlbumLinksInApp": MessageLookupByLibrary.simpleMessage( "Ver enlaces del álbum público en la aplicación"), @@ -1670,8 +1711,12 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Seleccionar app de correo"), "selectMorePhotos": MessageLookupByLibrary.simpleMessage("Seleccionar más fotos"), + "selectPersonToLink": MessageLookupByLibrary.simpleMessage( + "Selecciona persona a vincular"), "selectReason": MessageLookupByLibrary.simpleMessage("Seleccionar motivo"), + "selectYourFace": + MessageLookupByLibrary.simpleMessage("Selecciona tu cara"), "selectYourPlan": MessageLookupByLibrary.simpleMessage("Elegir tu suscripción"), "selectedFilesAreNotOnEnte": MessageLookupByLibrary.simpleMessage( @@ -1683,7 +1728,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage( "Los archivos seleccionados serán eliminados de todos los álbumes y movidos a la papelera."), "selectedPhotos": m6, - "selectedPhotosWithYours": m65, + "selectedPhotosWithYours": m70, "send": MessageLookupByLibrary.simpleMessage("Enviar"), "sendEmail": MessageLookupByLibrary.simpleMessage("Enviar correo electrónico"), @@ -1717,16 +1762,16 @@ class MessageLookup extends MessageLookupByLibrary { "shareAnAlbumNow": MessageLookupByLibrary.simpleMessage("Compartir un álbum ahora"), "shareLink": MessageLookupByLibrary.simpleMessage("Compartir enlace"), - "shareMyVerificationID": m66, + "shareMyVerificationID": m71, "shareOnlyWithThePeopleYouWant": MessageLookupByLibrary.simpleMessage( "Comparte sólo con la gente que quieres"), "shareTextConfirmOthersVerificationID": m7, "shareTextRecommendUsingEnte": MessageLookupByLibrary.simpleMessage( "Descarga Ente para que podamos compartir fácilmente fotos y videos en calidad original.\n\nhttps://ente.io"), - "shareTextReferralCode": m67, + "shareTextReferralCode": m72, "shareWithNonenteUsers": MessageLookupByLibrary.simpleMessage( "Compartir con usuarios fuera de Ente"), - "shareWithPeopleSectionTitle": m68, + "shareWithPeopleSectionTitle": m73, "shareYourFirstAlbum": MessageLookupByLibrary.simpleMessage("Comparte tu primer álbum"), "sharedAlbumSectionDescription": MessageLookupByLibrary.simpleMessage( @@ -1738,7 +1783,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": m69, + "sharedWith": m74, "sharedWithMe": MessageLookupByLibrary.simpleMessage("Compartido conmigo"), "sharedWithYou": @@ -1755,11 +1800,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": m70, + "singleFileDeleteFromDevice": m75, "singleFileDeleteHighlight": MessageLookupByLibrary.simpleMessage( "Se borrará de todos los álbumes."), - "singleFileInBothLocalAndRemote": m71, - "singleFileInRemoteOnly": m72, + "singleFileInBothLocalAndRemote": m76, + "singleFileInRemoteOnly": m77, "skip": MessageLookupByLibrary.simpleMessage("Omitir"), "social": MessageLookupByLibrary.simpleMessage("Social"), "someItemsAreInBothEnteAndYourDevice": @@ -1810,10 +1855,12 @@ class MessageLookup extends MessageLookupByLibrary { "storageInGB": m1, "storageLimitExceeded": MessageLookupByLibrary.simpleMessage("Límite de datos excedido"), - "storageUsageInfo": m73, + "storageUsageInfo": m78, + "streamDetails": + MessageLookupByLibrary.simpleMessage("Detalles de la transmisión"), "strongStrength": MessageLookupByLibrary.simpleMessage("Segura"), - "subAlreadyLinkedErrMessage": m74, - "subWillBeCancelledOn": m75, + "subAlreadyLinkedErrMessage": m79, + "subWillBeCancelledOn": m80, "subscribe": MessageLookupByLibrary.simpleMessage("Suscribirse"), "subscribeToEnableSharing": MessageLookupByLibrary.simpleMessage( "Necesitas una suscripción activa de pago para habilitar el compartir."), @@ -1830,7 +1877,7 @@ class MessageLookup extends MessageLookupByLibrary { "suggestFeatures": MessageLookupByLibrary.simpleMessage("Sugerir una característica"), "support": MessageLookupByLibrary.simpleMessage("Soporte"), - "syncProgress": m76, + "syncProgress": m81, "syncStopped": MessageLookupByLibrary.simpleMessage("Sincronización detenida"), "syncing": MessageLookupByLibrary.simpleMessage("Sincronizando..."), @@ -1841,7 +1888,7 @@ class MessageLookup extends MessageLookupByLibrary { "tapToUnlock": MessageLookupByLibrary.simpleMessage("Toca para desbloquear"), "tapToUpload": MessageLookupByLibrary.simpleMessage("Toca para subir"), - "tapToUploadIsIgnoredDue": m77, + "tapToUploadIsIgnoredDue": m82, "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"), @@ -1881,7 +1928,9 @@ class MessageLookup extends MessageLookupByLibrary { "Este correo electrónico ya está en uso"), "thisImageHasNoExifData": MessageLookupByLibrary.simpleMessage( "Esta imagen no tiene datos exif"), - "thisIsPersonVerificationId": m78, + "thisIsMeExclamation": + MessageLookupByLibrary.simpleMessage("¡Este soy yo!"), + "thisIsPersonVerificationId": m83, "thisIsYourVerificationId": MessageLookupByLibrary.simpleMessage( "Esta es tu ID de verificación"), "thisWillLogYouOutOfTheFollowingDevice": @@ -1905,11 +1954,11 @@ class MessageLookup extends MessageLookupByLibrary { "total": MessageLookupByLibrary.simpleMessage("total"), "totalSize": MessageLookupByLibrary.simpleMessage("Tamaño total"), "trash": MessageLookupByLibrary.simpleMessage("Papelera"), - "trashDaysLeft": m79, + "trashDaysLeft": m84, "trim": MessageLookupByLibrary.simpleMessage("Ajustar duración"), "trustedContacts": MessageLookupByLibrary.simpleMessage("Contactos de confianza"), - "trustedInviteBody": m80, + "trustedInviteBody": m85, "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."), @@ -1927,7 +1976,7 @@ class MessageLookup extends MessageLookupByLibrary { "Autenticación de doble factor restablecida con éxito"), "twofactorSetup": MessageLookupByLibrary.simpleMessage("Configuración de dos pasos"), - "typeOfGallerGallerytypeIsNotSupportedForRename": m81, + "typeOfGallerGallerytypeIsNotSupportedForRename": m86, "unarchive": MessageLookupByLibrary.simpleMessage("Desarchivar"), "unarchiveAlbum": MessageLookupByLibrary.simpleMessage("Desarchivar álbum"), @@ -1952,10 +2001,10 @@ class MessageLookup extends MessageLookupByLibrary { "updatingFolderSelection": MessageLookupByLibrary.simpleMessage( "Actualizando la selección de carpeta..."), "upgrade": MessageLookupByLibrary.simpleMessage("Mejorar"), - "uploadIsIgnoredDueToIgnorereason": m82, + "uploadIsIgnoredDueToIgnorereason": m87, "uploadingFilesToAlbum": MessageLookupByLibrary.simpleMessage( "Subiendo archivos al álbum..."), - "uploadingMultipleMemories": m83, + "uploadingMultipleMemories": m88, "uploadingSingleMemory": MessageLookupByLibrary.simpleMessage("Preservando 1 memoria..."), "upto50OffUntil4thDec": MessageLookupByLibrary.simpleMessage( @@ -1974,7 +2023,7 @@ class MessageLookup extends MessageLookupByLibrary { "useSelectedPhoto": MessageLookupByLibrary.simpleMessage("Usar foto seleccionada"), "usedSpace": MessageLookupByLibrary.simpleMessage("Espacio usado"), - "validTill": m84, + "validTill": m89, "verificationFailedPleaseTryAgain": MessageLookupByLibrary.simpleMessage( "Verificación fallida, por favor inténtalo de nuevo"), @@ -1983,7 +2032,7 @@ class MessageLookup extends MessageLookupByLibrary { "verify": MessageLookupByLibrary.simpleMessage("Verificar"), "verifyEmail": MessageLookupByLibrary.simpleMessage( "Verificar correo electrónico"), - "verifyEmailID": m85, + "verifyEmailID": m90, "verifyIDLabel": MessageLookupByLibrary.simpleMessage("Verificar"), "verifyPasskey": MessageLookupByLibrary.simpleMessage("Verificar clave de acceso"), @@ -1995,6 +2044,8 @@ 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"), @@ -2011,7 +2062,7 @@ class MessageLookup extends MessageLookupByLibrary { "viewRecoveryKey": MessageLookupByLibrary.simpleMessage("Ver código de recuperación"), "viewer": MessageLookupByLibrary.simpleMessage("Espectador"), - "viewersSuccessfullyAdded": m86, + "viewersSuccessfullyAdded": m91, "visitWebToManage": MessageLookupByLibrary.simpleMessage( "Por favor, visita web.ente.io para administrar tu suscripción"), "waitingForVerification": @@ -2033,7 +2084,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": m87, + "yearsAgo": m92, "yes": MessageLookupByLibrary.simpleMessage("Sí"), "yesCancel": MessageLookupByLibrary.simpleMessage("Sí, cancelar"), "yesConvertToViewer": @@ -2065,7 +2116,7 @@ class MessageLookup extends MessageLookupByLibrary { "No puedes compartir contigo mismo"), "youDontHaveAnyArchivedItems": MessageLookupByLibrary.simpleMessage( "No tienes ningún elemento archivado."), - "youHaveSuccessfullyFreedUp": m88, + "youHaveSuccessfullyFreedUp": m93, "yourAccountHasBeenDeleted": MessageLookupByLibrary.simpleMessage("Tu cuenta ha sido eliminada"), "yourMap": MessageLookupByLibrary.simpleMessage("Tu mapa"), @@ -2086,9 +2137,6 @@ class MessageLookup extends MessageLookupByLibrary { "Tu suscripción se ha actualizado con éxito"), "yourVerificationCodeHasExpired": MessageLookupByLibrary.simpleMessage( "Tu código de verificación ha expirado"), - "youveNoDuplicateFilesThatCanBeCleared": - MessageLookupByLibrary.simpleMessage( - "No tienes archivos duplicados que puedan ser borrados"), "youveNoFilesInThisAlbumThatCanBeDeleted": MessageLookupByLibrary.simpleMessage( "No tienes archivos en este álbum que puedan ser borrados"), diff --git a/mobile/lib/generated/intl/messages_et.dart b/mobile/lib/generated/intl/messages_et.dart index 20fb528795..dc3a61a6ff 100644 --- a/mobile/lib/generated/intl/messages_et.dart +++ b/mobile/lib/generated/intl/messages_et.dart @@ -169,8 +169,6 @@ class MessageLookup extends MessageLookupByLibrary { "oops": MessageLookupByLibrary.simpleMessage("Oih"), "oopsSomethingWentWrong": MessageLookupByLibrary.simpleMessage("Oih, midagi läks valesti"), - "orPickFromYourContacts": - MessageLookupByLibrary.simpleMessage("or pick from your contacts"), "password": MessageLookupByLibrary.simpleMessage("Parool"), "photoSmallCase": MessageLookupByLibrary.simpleMessage("foto"), "pleaseTryAgain": diff --git a/mobile/lib/generated/intl/messages_fa.dart b/mobile/lib/generated/intl/messages_fa.dart index 3beb2c2f51..c705ca6393 100644 --- a/mobile/lib/generated/intl/messages_fa.dart +++ b/mobile/lib/generated/intl/messages_fa.dart @@ -20,24 +20,24 @@ typedef String MessageIfAbsent(String messageStr, List args); class MessageLookup extends MessageLookupByLibrary { String get localeName => 'fa'; - static String m16(versionValue) => "نسخه: ${versionValue}"; + static String m17(versionValue) => "نسخه: ${versionValue}"; - static String m17(freeAmount, storageUnit) => + static String m18(freeAmount, storageUnit) => "${freeAmount} ${storageUnit} رایگان"; - static String m29(supportEmail) => + static String m30(supportEmail) => "لطفا یک ایمیل از آدرس ایمیلی که ثبت نام کردید به ${supportEmail} ارسال کنید"; static String m0(passwordStrengthValue) => "قدرت رمز عبور: ${passwordStrengthValue}"; - static String m56(storeName) => "به ما در ${storeName} امتیاز دهید"; + static String m60(storeName) => "به ما در ${storeName} امتیاز دهید"; - static String m73( + static String m78( usedAmount, usedStorageUnit, totalAmount, totalStorageUnit) => "${usedAmount} ${usedStorageUnit} از ${totalAmount} ${totalStorageUnit} استفاده شده"; - static String m85(email) => "تایید ${email}"; + static String m90(email) => "تایید ${email}"; static String m2(email) => "ما یک ایمیل به ${email} ارسال کرده‌ایم"; @@ -75,7 +75,7 @@ class MessageLookup extends MessageLookupByLibrary { "androidCancelButton": MessageLookupByLibrary.simpleMessage("لغو"), "androidIosWebDesktop": MessageLookupByLibrary.simpleMessage( "اندروید، آی‌اواس، وب، رایانه رومیزی"), - "appVersion": m16, + "appVersion": m17, "archive": MessageLookupByLibrary.simpleMessage("بایگانی"), "areYouSureYouWantToLogout": MessageLookupByLibrary.simpleMessage( "آیا برای خارج شدن مطمئن هستید؟"), @@ -86,7 +86,7 @@ class MessageLookup extends MessageLookupByLibrary { "authToViewYourActiveSessions": MessageLookupByLibrary.simpleMessage( "لطفاً برای مشاهده دستگاه‌های فعال خود احراز هویت کنید"), "available": MessageLookupByLibrary.simpleMessage("در دسترس"), - "availableStorageSpace": m17, + "availableStorageSpace": m18, "backedUpFolders": MessageLookupByLibrary.simpleMessage("پوشه‌های پشتیبان گیری شده"), "backup": MessageLookupByLibrary.simpleMessage("پشتیبان گیری"), @@ -166,7 +166,7 @@ class MessageLookup extends MessageLookupByLibrary { "discord": MessageLookupByLibrary.simpleMessage("دیسکورد"), "doThisLater": MessageLookupByLibrary.simpleMessage("بعداً انجام شود"), "downloading": MessageLookupByLibrary.simpleMessage("در حال دانلود..."), - "dropSupportEmail": m29, + "dropSupportEmail": m30, "editLocationTagTitle": MessageLookupByLibrary.simpleMessage("ویرایش مکان"), "email": MessageLookupByLibrary.simpleMessage("ایمیل"), @@ -269,8 +269,6 @@ class MessageLookup extends MessageLookupByLibrary { "notifications": MessageLookupByLibrary.simpleMessage("آگاه‌سازی‌ها"), "ok": MessageLookupByLibrary.simpleMessage("تایید"), "oops": MessageLookupByLibrary.simpleMessage("اوه"), - "orPickFromYourContacts": - MessageLookupByLibrary.simpleMessage("or pick from your contacts"), "password": MessageLookupByLibrary.simpleMessage("رمز عبور"), "passwordChangedSuccessfully": MessageLookupByLibrary.simpleMessage( "رمز عبور با موفقیت تغییر کرد"), @@ -294,7 +292,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("پشتیبان گیری خصوصی"), "privateSharing": MessageLookupByLibrary.simpleMessage("اشتراک گذاری خصوصی"), - "rateUsOnStore": m56, + "rateUsOnStore": m60, "recover": MessageLookupByLibrary.simpleMessage("بازیابی"), "recoverAccount": MessageLookupByLibrary.simpleMessage("بازیابی حساب کاربری"), @@ -370,7 +368,7 @@ class MessageLookup extends MessageLookupByLibrary { "storageBreakupFamily": MessageLookupByLibrary.simpleMessage("خانوادگی"), "storageBreakupYou": MessageLookupByLibrary.simpleMessage("شما"), - "storageUsageInfo": m73, + "storageUsageInfo": m78, "strongStrength": MessageLookupByLibrary.simpleMessage("قوی"), "support": MessageLookupByLibrary.simpleMessage("پشتیبانی"), "systemTheme": MessageLookupByLibrary.simpleMessage("سیستم"), @@ -411,7 +409,7 @@ class MessageLookup extends MessageLookupByLibrary { "از کلید بازیابی استفاده کنید"), "verify": MessageLookupByLibrary.simpleMessage("تایید"), "verifyEmail": MessageLookupByLibrary.simpleMessage("تایید ایمیل"), - "verifyEmailID": m85, + "verifyEmailID": m90, "verifyIDLabel": MessageLookupByLibrary.simpleMessage("تایید"), "verifyPassword": MessageLookupByLibrary.simpleMessage("تایید رمز عبور"), diff --git a/mobile/lib/generated/intl/messages_fr.dart b/mobile/lib/generated/intl/messages_fr.dart index f90ca9f1f6..db251563cb 100644 --- a/mobile/lib/generated/intl/messages_fr.dart +++ b/mobile/lib/generated/intl/messages_fr.dart @@ -20,263 +20,275 @@ typedef String MessageIfAbsent(String messageStr, List args); class MessageLookup extends MessageLookupByLibrary { String get localeName => 'fr'; - static String m9(count) => - "${Intl.plural(count, zero: 'Ajouter un collaborateur', one: 'Ajouter un collaborateur', other: 'Ajouter des collaborateurs')}"; + static String m9(title) => "${title} (Moi)"; static String m10(count) => + "${Intl.plural(count, zero: 'Ajouter un collaborateur', one: 'Ajouter un collaborateur', other: 'Ajouter des collaborateurs')}"; + + static String m11(count) => "${Intl.plural(count, one: 'Ajoutez un objet', other: 'Ajoutez des objets')}"; - static String m11(storageAmount, endDate) => + static String m12(storageAmount, endDate) => "Votre extension de ${storageAmount} est valable jusqu\'au ${endDate}"; - static String m12(count) => + static String m13(count) => "${Intl.plural(count, zero: 'Ajouter un observateur', one: 'Ajouter un observateur', other: 'Ajouter des observateurs')}"; - static String m13(emailOrName) => "Ajouté par ${emailOrName}"; + static String m14(emailOrName) => "Ajouté par ${emailOrName}"; - static String m14(albumName) => "Ajouté avec succès à ${albumName}"; + static String m15(albumName) => "Ajouté avec succès à ${albumName}"; - static String m15(count) => + static String m16(count) => "${Intl.plural(count, zero: 'Aucun Participant', one: '1 Participant', other: '${count} Participants')}"; - static String m16(versionValue) => "Version : ${versionValue}"; + static String m17(versionValue) => "Version : ${versionValue}"; - static String m17(freeAmount, storageUnit) => + static String m18(freeAmount, storageUnit) => "${freeAmount} ${storageUnit} libre"; - static String m18(paymentProvider) => + static String m19(paymentProvider) => "Veuillez d\'abord annuler votre abonnement existant de ${paymentProvider}"; static String m3(user) => "${user} ne pourra pas ajouter plus de photos à cet album\n\nIl pourra toujours supprimer les photos existantes ajoutées par eux"; - static String m19(isFamilyMember, storageAmountInGb) => + static String m20(isFamilyMember, storageAmountInGb) => "${Intl.select(isFamilyMember, { 'true': - 'Votre famille a demandé ${storageAmountInGb} GB jusqu\'à présent', + 'Votre famille a obtenu ${storageAmountInGb} Go jusqu\'à présent', 'false': - 'Vous avez réclamé ${storageAmountInGb} GB jusqu\'à présent', + 'Vous avez obtenu ${storageAmountInGb} Go jusqu\'à présent', 'other': - 'Vous avez réclamé ${storageAmountInGb} GB jusqu\'à présent!', + 'Vous avez obtenu ${storageAmountInGb} Go jusqu\'à présent !', })}"; - static String m20(albumName) => "Lien collaboratif créé pour ${albumName}"; + static String m21(albumName) => "Lien collaboratif créé pour ${albumName}"; - static String m21(count) => + static String m22(count) => "${Intl.plural(count, zero: '0 collaborateur ajouté', one: '1 collaborateur ajouté', other: '${count} collaborateurs ajoutés')}"; - static String m22(email, numOfDays) => + static String m23(email, numOfDays) => "Vous êtes sur le point d\'ajouter ${email} en tant que contact sûr. Il pourra récupérer votre compte si vous êtes absent pendant ${numOfDays} jours."; - static String m23(familyAdminEmail) => + static String m24(familyAdminEmail) => "Veuillez contacter ${familyAdminEmail} pour gérer votre abonnement"; - static String m24(provider) => + static String m25(provider) => "Veuillez nous contacter à support@ente.io pour gérer votre abonnement ${provider}."; - static String m25(endpoint) => "Connecté à ${endpoint}"; + static String m26(endpoint) => "Connecté à ${endpoint}"; - static String m26(count) => + static String m27(count) => "${Intl.plural(count, one: 'Supprimer le fichier', other: 'Supprimer ${count} fichiers')}"; - static String m27(currentlyDeleting, totalCount) => + static String m28(currentlyDeleting, totalCount) => "Suppression de ${currentlyDeleting} / ${totalCount}"; - static String m28(albumName) => + static String m29(albumName) => "Cela supprimera le lien public pour accéder à \"${albumName}\"."; - static String m29(supportEmail) => + static String m30(supportEmail) => "Veuillez envoyer un e-mail à ${supportEmail} depuis votre adresse enregistrée"; - static String m30(count, storageSaved) => + static String m31(count, storageSaved) => "Vous avez nettoyé ${Intl.plural(count, one: '${count} fichier dupliqué', other: '${count} fichiers dupliqués')}, en libérant (${storageSaved}!)"; - static String m31(count, formattedSize) => + static String m32(count, formattedSize) => "${count} fichiers, ${formattedSize} chacun"; - static String m32(newEmail) => "L\'e-mail a été changé en ${newEmail}"; + static String m33(newEmail) => "L\'email a été changé par ${newEmail}"; - static String m33(email) => + static String m34(email) => "${email} n\'a pas de compte Ente."; + + static String m35(email) => "${email} n\'a pas de compte Ente.\n\nEnvoyez une invitation pour partager des photos."; - static String m34(text) => "Photos supplémentaires trouvées pour ${text}"; + static String m36(text) => "Photos supplémentaires trouvées pour ${text}"; - static String m35(count, formattedNumber) => + static String m37(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 m36(count, formattedNumber) => + static String m38(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 m4(storageAmountInGB) => "${storageAmountInGB} Go chaque fois que quelqu\'un s\'inscrit à une offre payante et applique votre code"; - static String m37(endDate) => "Essai gratuit valide jusqu’au ${endDate}"; + static String m39(endDate) => "Essai gratuit valide jusqu’au ${endDate}"; - static String m38(count) => + static String m40(count) => "Vous pouvez toujours ${Intl.plural(count, one: 'y', other: 'y')} accéder sur Ente tant que vous avez un abonnement actif"; - static String m39(sizeInMBorGB) => "Libérer ${sizeInMBorGB}"; + static String m41(sizeInMBorGB) => "Libérer ${sizeInMBorGB}"; - static String m40(count, formattedSize) => + static String m42(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) => + static String m43(currentlyProcessing, totalCount) => "Traitement en cours ${currentlyProcessing} / ${totalCount}"; - static String m42(count) => + static String m44(count) => "${Intl.plural(count, one: '${count} objet', other: '${count} objets')}"; - static String m43(email) => + static String m45(email) => "${email} vous a invité à être un contact de confiance"; - static String m44(expiryTime) => "Le lien expirera le ${expiryTime}"; + static String m46(expiryTime) => "Le lien expirera le ${expiryTime}"; + + static String m47(email) => "Associer la personne à ${email}"; + + static String m48(personName, email) => + "Cela va associer ${personName} à ${email}"; static String m5(count, formattedCount) => "${Intl.plural(count, one: '${formattedCount} souvenir', other: '${formattedCount} souvenirs')}"; - static String m45(count) => + static String m49(count) => "${Intl.plural(count, one: 'Déplacez l\'objet', other: 'Déplacez des objets')}"; - static String m46(albumName) => "Déplacé avec succès vers ${albumName}"; + static String m50(albumName) => "Déplacé avec succès vers ${albumName}"; - static String m47(personName) => "Aucune suggestion pour ${personName}"; + static String m51(personName) => "Aucune suggestion pour ${personName}"; - static String m48(name) => "Pas ${name}?"; + static String m52(name) => "Pas ${name}?"; - static String m49(familyAdminEmail) => + static String m53(familyAdminEmail) => "Veuillez contacter ${familyAdminEmail} pour modifier votre code."; static String m0(passwordStrengthValue) => "Sécurité du mot de passe : ${passwordStrengthValue}"; - static String m50(providerName) => + static String m54(providerName) => "Veuillez contacter le support ${providerName} si vous avez été facturé"; - static String m51(count) => + static String m55(count) => "${Intl.plural(count, zero: '0 photo', one: '1 photo', other: '${count} photos')}"; - static String m52(endDate) => + static String m56(endDate) => "Essai gratuit valable jusqu\'à ${endDate}.\nVous pouvez choisir un plan payant par la suite."; - static String m53(toEmail) => "Merci de nous envoyer un e-mail à ${toEmail}"; + static String m57(toEmail) => "Merci de nous envoyer un email à ${toEmail}"; - static String m54(toEmail) => "Envoyez les logs à ${toEmail}"; + static String m58(toEmail) => "Envoyez les logs à ${toEmail}"; - static String m55(folderName) => "Traitement de ${folderName}..."; + static String m59(folderName) => "Traitement de ${folderName}..."; - static String m56(storeName) => "Notez-nous sur ${storeName}"; + static String m60(storeName) => "Laissez une note sur ${storeName}"; - static String m57(days, email) => + static String m61(name) => "Vous a réassigné à ${name}"; + + static String m62(days, email) => "Vous pourrez accéder au compte d\'ici ${days} jours. Une notification sera envoyée à ${email}."; - static String m58(email) => + static String m63(email) => "Vous pouvez maintenant récupérer le compte de ${email} en définissant un nouveau mot de passe."; - static String m59(email) => "${email} tente de récupérer votre compte."; + static String m64(email) => "${email} tente de récupérer votre compte."; - static String m60(storageInGB) => - "3. Vous recevez tous les deux ${storageInGB} GB* gratuits"; + static String m65(storageInGB) => + "3. Vous recevez tous les deux ${storageInGB} Go* gratuits"; - static String m61(userEmail) => + static String m66(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 m62(endDate) => "Renouvellement le ${endDate}"; + static String m67(endDate) => "Renouvellement le ${endDate}"; - static String m63(count) => + static String m68(count) => "${Intl.plural(count, one: '${count} résultat trouvé', other: '${count} résultats trouvés')}"; - static String m64(snapshotLength, searchLength) => + static String m69(snapshotLength, searchLength) => "Incompatibilité de longueur des sections : ${snapshotLength} != ${searchLength}"; static String m6(count) => "${count} sélectionné(s)"; - static String m65(count, yourCount) => + static String m70(count, yourCount) => "${count} sélectionné(s) (${yourCount} à vous)"; - static String m66(verificationID) => + static String m71(verificationID) => "Voici mon ID de vérification : ${verificationID} pour ente.io."; static String m7(verificationID) => "Hé, pouvez-vous confirmer qu\'il s\'agit de votre ID de vérification ente.io : ${verificationID}"; - static String m67(referralCode, referralStorageInGB) => + static String m72(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 m68(numberOfPeople) => + static String m73(numberOfPeople) => "${Intl.plural(numberOfPeople, zero: 'Partagez avec des personnes spécifiques', one: 'Partagé avec 1 personne', other: 'Partagé avec ${numberOfPeople} personnes')}"; - static String m69(emailIDs) => "Partagé avec ${emailIDs}"; + static String m74(emailIDs) => "Partagé avec ${emailIDs}"; - static String m70(fileType) => + static String m75(fileType) => "Elle ${fileType} sera supprimée de votre appareil."; - static String m71(fileType) => + static String m76(fileType) => "Cette ${fileType} est à la fois sur ente et sur votre appareil."; - static String m72(fileType) => "Cette ${fileType} sera supprimée de l\'Ente."; + static String m77(fileType) => "Cette ${fileType} sera supprimée de l\'Ente."; static String m1(storageAmountInGB) => "${storageAmountInGB} Go"; - static String m73( + static String m78( usedAmount, usedStorageUnit, totalAmount, totalStorageUnit) => "${usedAmount} ${usedStorageUnit} sur ${totalAmount} ${totalStorageUnit} utilisés"; - static String m74(id) => + static String m79(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 m75(endDate) => "Votre abonnement sera annulé le ${endDate}"; + static String m80(endDate) => "Votre abonnement sera annulé le ${endDate}"; - static String m76(completed, total) => - "${completed}/${total} souvenirs conservés"; + static String m81(completed, total) => + "${completed}/${total} souvenirs sauvegardés"; - static String m77(ignoreReason) => + static String m82(ignoreReason) => "Appuyer pour envoyer, l\'envoi est actuellement ignoré en raison de ${ignoreReason}"; static String m8(storageAmountInGB) => "Ils obtiennent aussi ${storageAmountInGB} Go"; - static String m78(email) => "Ceci est l\'ID de vérification de ${email}"; + static String m83(email) => "Ceci est l\'ID de vérification de ${email}"; - static String m79(count) => + static String m84(count) => "${Intl.plural(count, zero: 'Bientôt', one: '1 jour', other: '${count} jours')}"; - static String m80(email) => + static String m85(email) => "Vous avez été invité(e) à être un(e) héritier(e) par ${email}."; - static String m81(galleryType) => + static String m86(galleryType) => "Les galeries de type \'${galleryType}\' ne peuvent être renommées"; - static String m82(ignoreReason) => + static String m87(ignoreReason) => "L\'envoi est ignoré en raison de ${ignoreReason}"; - static String m83(count) => "Sauvegarde ${count} souvenirs..."; + static String m88(count) => "Sauvegarde de ${count} souvenirs..."; - static String m84(endDate) => "Valable jusqu\'au ${endDate}"; + static String m89(endDate) => "Valable jusqu\'au ${endDate}"; - static String m85(email) => "Vérifier ${email}"; + static String m90(email) => "Vérifier ${email}"; - static String m86(count) => + static String m91(count) => "${Intl.plural(count, zero: '0 observateur ajouté', one: '1 observateur ajouté', other: '${count} observateurs ajoutés')}"; static String m2(email) => - "Nous avons envoyé un e-mail à ${email}"; + "Nous avons envoyé un email à ${email}"; - static String m87(count) => + static String m92(count) => "${Intl.plural(count, one: 'il y a ${count} an', other: 'il y a ${count} ans')}"; - static String m88(storageSaved) => + static String m93(storageSaved) => "Vous avez libéré ${storageSaved} avec succès !"; final messages = _notInlinedMessages(_notInlinedMessages); static Map _notInlinedMessages(_) => { "aNewVersionOfEnteIsAvailable": MessageLookupByLibrary.simpleMessage( "Une nouvelle version de Ente est disponible."), - "about": MessageLookupByLibrary.simpleMessage("À propos"), + "about": MessageLookupByLibrary.simpleMessage("À propos d\'Ente"), "acceptTrustInvite": MessageLookupByLibrary.simpleMessage("Accepter l\'invitation"), "account": MessageLookupByLibrary.simpleMessage("Compte"), "accountIsAlreadyConfigured": MessageLookupByLibrary.simpleMessage( "Le compte est déjà configuré."), + "accountOwnerPersonAppbarTitle": m9, "accountWelcomeBack": MessageLookupByLibrary.simpleMessage("Bon retour parmi nous !"), "ackPasswordLostWarning": MessageLookupByLibrary.simpleMessage( @@ -289,12 +301,12 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Ajouter un nouvel email"), "addCollaborator": MessageLookupByLibrary.simpleMessage("Ajouter un collaborateur"), - "addCollaborators": m9, + "addCollaborators": m10, "addFiles": MessageLookupByLibrary.simpleMessage("Ajouter des fichiers"), "addFromDevice": MessageLookupByLibrary.simpleMessage("Ajouter depuis l\'appareil"), - "addItem": m10, + "addItem": m11, "addLocation": MessageLookupByLibrary.simpleMessage("Ajouter la localisation"), "addLocationButton": MessageLookupByLibrary.simpleMessage("Ajouter"), @@ -307,7 +319,7 @@ class MessageLookup extends MessageLookupByLibrary { "Ajouter une nouvelle personne"), "addOnPageSubtitle": MessageLookupByLibrary.simpleMessage( "Détails des modules complémentaires"), - "addOnValidTill": m11, + "addOnValidTill": m12, "addOns": MessageLookupByLibrary.simpleMessage("Modules complémentaires"), "addPhotos": MessageLookupByLibrary.simpleMessage("Ajouter des photos"), @@ -322,12 +334,12 @@ class MessageLookup extends MessageLookupByLibrary { "Ajouter un contact de confiance"), "addViewer": MessageLookupByLibrary.simpleMessage("Ajouter un observateur"), - "addViewers": m12, + "addViewers": m13, "addYourPhotosNow": MessageLookupByLibrary.simpleMessage( "Ajoutez vos photos maintenant"), "addedAs": MessageLookupByLibrary.simpleMessage("Ajouté comme"), - "addedBy": m13, - "addedSuccessfullyTo": m14, + "addedBy": m14, + "addedSuccessfullyTo": m15, "addingToFavorites": MessageLookupByLibrary.simpleMessage("Ajout aux favoris..."), "advanced": MessageLookupByLibrary.simpleMessage("Avancé"), @@ -338,14 +350,14 @@ class MessageLookup extends MessageLookupByLibrary { "after1Week": MessageLookupByLibrary.simpleMessage("Après 1 semaine"), "after1Year": MessageLookupByLibrary.simpleMessage("Après 1 an"), "albumOwner": MessageLookupByLibrary.simpleMessage("Propriétaire"), - "albumParticipantsCount": m15, + "albumParticipantsCount": m16, "albumTitle": MessageLookupByLibrary.simpleMessage("Titre de l\'album"), "albumUpdated": MessageLookupByLibrary.simpleMessage("Album mis à jour"), "albums": MessageLookupByLibrary.simpleMessage("Albums"), "allClear": MessageLookupByLibrary.simpleMessage("✨ Tout est effacé"), "allMemoriesPreserved": MessageLookupByLibrary.simpleMessage( - "Tous les souvenirs sont conservés"), + "Tous les souvenirs sont sauvegardés"), "allPersonGroupingWillReset": MessageLookupByLibrary.simpleMessage( "Tous les groupements pour cette personne seront réinitialisés, et vous perdrez toutes les suggestions faites pour cette personne"), "allow": MessageLookupByLibrary.simpleMessage("Autoriser"), @@ -386,7 +398,7 @@ class MessageLookup extends MessageLookupByLibrary { "Verrouillage de l\'application"), "appLockDescriptions": MessageLookupByLibrary.simpleMessage( "Choisissez entre l\'écran de verrouillage par défaut de votre appareil et un écran de verrouillage personnalisé avec un code PIN ou un mot de passe."), - "appVersion": m16, + "appVersion": m17, "appleId": MessageLookupByLibrary.simpleMessage("Apple ID"), "apply": MessageLookupByLibrary.simpleMessage("Appliquer"), "applyCodeTitle": @@ -425,11 +437,11 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("dans un abri antiatomique"), "authToChangeEmailVerificationSetting": MessageLookupByLibrary.simpleMessage( - "Veuillez vous authentifier pour modifier votre adresse e-mail"), + "Authentifiez-vous pour modifier l\'authentification à deux facteurs par email"), "authToChangeLockscreenSetting": MessageLookupByLibrary.simpleMessage( "Veuillez vous authentifier pour modifier les paramètres de l\'écran de verrouillage"), "authToChangeYourEmail": MessageLookupByLibrary.simpleMessage( - "Veuillez vous authentifier pour modifier votre adresse e-mail"), + "Authentifiez-vous pour modifier votre adresse email"), "authToChangeYourPassword": MessageLookupByLibrary.simpleMessage( "Veuillez vous authentifier pour modifier votre mot de passe"), "authToConfigureTwofactorAuthentication": @@ -444,11 +456,11 @@ class MessageLookup extends MessageLookupByLibrary { "authToViewTrashedFiles": MessageLookupByLibrary.simpleMessage( "Veuillez vous authentifier pour voir vos fichiers mis à la corbeille"), "authToViewYourActiveSessions": MessageLookupByLibrary.simpleMessage( - "Veuillez vous authentifier pour voir vos sessions actives"), + "Authentifiez-vous pour voir les connexions actives"), "authToViewYourHiddenFiles": MessageLookupByLibrary.simpleMessage( "Veuillez vous authentifier pour voir vos fichiers cachés"), "authToViewYourMemories": MessageLookupByLibrary.simpleMessage( - "Veuillez vous authentifier pour voir vos souvenirs"), + "Authentifiez-vous pour voir vos souvenirs"), "authToViewYourRecoveryKey": MessageLookupByLibrary.simpleMessage( "Veuillez vous authentifier pour afficher votre clé de récupération"), "authenticating": @@ -465,7 +477,7 @@ class MessageLookup extends MessageLookupByLibrary { "autoLock": MessageLookupByLibrary.simpleMessage("Verrouillage automatique"), "autoLockFeatureDescription": MessageLookupByLibrary.simpleMessage( - "Délai après lequel l\'application se verrouille une fois qu\'elle a été mise en arrière-plan"), + "Délai après lequel l\'application se verrouille une fois qu\'elle est en arrière-plan"), "autoLogoutMessage": MessageLookupByLibrary.simpleMessage( "En raison d\'un problème technique, vous avez été déconnecté. Veuillez nous excuser pour le désagrément."), "autoPair": @@ -473,7 +485,7 @@ class MessageLookup extends MessageLookupByLibrary { "autoPairDesc": MessageLookupByLibrary.simpleMessage( "L\'appairage automatique ne fonctionne qu\'avec les appareils qui prennent en charge Chromecast."), "available": MessageLookupByLibrary.simpleMessage("Disponible"), - "availableStorageSpace": m17, + "availableStorageSpace": m18, "backedUpFolders": MessageLookupByLibrary.simpleMessage("Dossiers sauvegardés"), "backup": MessageLookupByLibrary.simpleMessage("Sauvegarde"), @@ -515,7 +527,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Annuler la récupération"), "cancelAccountRecoveryBody": MessageLookupByLibrary.simpleMessage( "Êtes-vous sûr de vouloir annuler la récupération ?"), - "cancelOtherSubscription": m18, + "cancelOtherSubscription": m19, "cancelSubscription": MessageLookupByLibrary.simpleMessage("Annuler l\'abonnement"), "cannotAddMorePhotosAfterBecomingViewer": m3, @@ -539,7 +551,7 @@ class MessageLookup extends MessageLookupByLibrary { "changeLogBackupStatusTitle": MessageLookupByLibrary.simpleMessage("Statut de la Sauvegarde"), "changeLogDiscoverContent": MessageLookupByLibrary.simpleMessage( - "Vous cherchez des photos de vos cartes d\'identité, des notes ou même des memes? Allez dans l\'onglet de recherche et découvrez Découverte. Sur la base de notre recherche sémantique, vous trouverez des photos qui pourraient être importantes pour vous.\\n\\nUniquement disponible si vous avez activé l\'apprentissage automatique."), + "Vous cherchez des photos de vos papiers d\'identité, des notes ou même des \"memes\" ? Rendez-vous dans l\'onglet de recherche et explorez la fonction Découverte. Grâce à notre recherche sémantique vous trouverez des photos qui pourraient être importantes pour vous.\\n\\nDisponible uniquement si vous avez activé l\'apprentissage automatique."), "changeLogDiscoverTitle": MessageLookupByLibrary.simpleMessage("Découverte"), "changeLogMagicSearchImprovementContent": @@ -559,17 +571,17 @@ class MessageLookup extends MessageLookupByLibrary { "checkForUpdates": MessageLookupByLibrary.simpleMessage("Vérifier les mises à jour"), "checkInboxAndSpamFolder": MessageLookupByLibrary.simpleMessage( - "Veuillez consulter votre boîte de réception (ainsi que les indésirables) pour compléter la vérification"), + "Consultez votre boîte de réception (et les indésirables) pour finaliser la vérification"), "checkStatus": MessageLookupByLibrary.simpleMessage("Vérifier le statut"), "checking": MessageLookupByLibrary.simpleMessage("Vérification..."), "checkingModels": MessageLookupByLibrary.simpleMessage("Vérification des modèles..."), "claimFreeStorage": - MessageLookupByLibrary.simpleMessage("Stockage gratuit obtenu"), + MessageLookupByLibrary.simpleMessage("Obtenez du stockage gratuit"), "claimMore": MessageLookupByLibrary.simpleMessage("Réclamez plus !"), "claimed": MessageLookupByLibrary.simpleMessage("Obtenu"), - "claimedStorageSoFar": m19, + "claimedStorageSoFar": m20, "cleanUncategorized": MessageLookupByLibrary.simpleMessage( "Effacer les éléments non classés"), "cleanUncategorizedDescription": MessageLookupByLibrary.simpleMessage( @@ -600,12 +612,12 @@ class MessageLookup extends MessageLookupByLibrary { "Créez un lien pour permettre aux personnes d\'ajouter et de voir des photos dans votre album partagé sans avoir besoin d\'une application Ente ou d\'un compte. Idéal pour récupérer des photos d\'événement."), "collaborativeLink": MessageLookupByLibrary.simpleMessage("Lien collaboratif"), - "collaborativeLinkCreatedFor": m20, + "collaborativeLinkCreatedFor": m21, "collaborator": MessageLookupByLibrary.simpleMessage("Collaborateur"), "collaboratorsCanAddPhotosAndVideosToTheSharedAlbum": MessageLookupByLibrary.simpleMessage( "Les collaborateurs peuvent ajouter des photos et des vidéos à l\'album partagé."), - "collaboratorsSuccessfullyAdded": m21, + "collaboratorsSuccessfullyAdded": m22, "collageLayout": MessageLookupByLibrary.simpleMessage("Disposition"), "collageSaved": MessageLookupByLibrary.simpleMessage( "Collage sauvegardé dans la galerie"), @@ -623,7 +635,7 @@ class MessageLookup extends MessageLookupByLibrary { "Voulez-vous vraiment désactiver l\'authentification à deux facteurs ?"), "confirmAccountDeletion": MessageLookupByLibrary.simpleMessage( "Confirmer la suppression du compte"), - "confirmAddingTrustedContact": m22, + "confirmAddingTrustedContact": m23, "confirmDeletePrompt": MessageLookupByLibrary.simpleMessage( "Oui, je veux supprimer définitivement ce compte et ses données dans toutes les applications."), "confirmPassword": @@ -636,10 +648,10 @@ class MessageLookup extends MessageLookupByLibrary { "Confirmer la clé de récupération"), "connectToDevice": MessageLookupByLibrary.simpleMessage("Connexion à l\'appareil"), - "contactFamilyAdmin": m23, + "contactFamilyAdmin": m24, "contactSupport": MessageLookupByLibrary.simpleMessage("Contacter l\'assistance"), - "contactToManageSubscription": m24, + "contactToManageSubscription": m25, "contacts": MessageLookupByLibrary.simpleMessage("Contacts"), "contents": MessageLookupByLibrary.simpleMessage("Contenus"), "continueLabel": MessageLookupByLibrary.simpleMessage("Continuer"), @@ -648,7 +660,7 @@ class MessageLookup extends MessageLookupByLibrary { "convertToAlbum": MessageLookupByLibrary.simpleMessage("Convertir en album"), "copyEmailAddress": - MessageLookupByLibrary.simpleMessage("Copier l’adresse e-mail"), + MessageLookupByLibrary.simpleMessage("Copier l’adresse email"), "copyLink": MessageLookupByLibrary.simpleMessage("Copier le lien"), "copypasteThisCodentoYourAuthenticatorApp": MessageLookupByLibrary.simpleMessage( @@ -661,7 +673,7 @@ class MessageLookup extends MessageLookupByLibrary { "Impossible de mettre à jour l’abonnement"), "count": MessageLookupByLibrary.simpleMessage("Total"), "crashReporting": - MessageLookupByLibrary.simpleMessage("Rapports d\'erreurs"), + MessageLookupByLibrary.simpleMessage("Rapport d\'erreur"), "create": MessageLookupByLibrary.simpleMessage("Créer"), "createAccount": MessageLookupByLibrary.simpleMessage("Créer un compte"), @@ -687,7 +699,7 @@ class MessageLookup extends MessageLookupByLibrary { "currentlyRunning": MessageLookupByLibrary.simpleMessage("en cours d\'exécution"), "custom": MessageLookupByLibrary.simpleMessage("Personnaliser"), - "customEndpoint": m25, + "customEndpoint": m26, "darkTheme": MessageLookupByLibrary.simpleMessage("Sombre"), "dayToday": MessageLookupByLibrary.simpleMessage("Aujourd\'hui"), "dayYesterday": MessageLookupByLibrary.simpleMessage("Hier"), @@ -728,12 +740,12 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Supprimer de l\'appareil"), "deleteFromEnte": MessageLookupByLibrary.simpleMessage("Supprimer de Ente"), - "deleteItemCount": m26, + "deleteItemCount": m27, "deleteLocation": MessageLookupByLibrary.simpleMessage("Supprimer la localisation"), "deletePhotos": MessageLookupByLibrary.simpleMessage("Supprimer des photos"), - "deleteProgress": m27, + "deleteProgress": m28, "deleteReason1": MessageLookupByLibrary.simpleMessage( "Il manque une fonction clé dont j\'ai besoin"), "deleteReason2": MessageLookupByLibrary.simpleMessage( @@ -761,10 +773,10 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Saisissez le code"), "deviceFilesAutoUploading": MessageLookupByLibrary.simpleMessage( "Les fichiers ajoutés à cet album seront automatiquement téléchargés sur Ente."), - "deviceLock": - MessageLookupByLibrary.simpleMessage("Verrouillage de l\'appareil"), + "deviceLock": MessageLookupByLibrary.simpleMessage( + "Verrouillage par défaut de l\'appareil"), "deviceLockExplanation": MessageLookupByLibrary.simpleMessage( - "Désactiver le verrouillage de l\'écran de l\'appareil lorsque ente est au premier plan et il y a une sauvegarde en cours. Ce n\'est normalement pas nécessaire, mais peut aider les gros téléchargements et les premières importations de grandes bibliothèques plus rapidement."), + "Désactiver le verrouillage de l\'écran lorsque Ente est au premier plan et qu\'une sauvegarde est en cours. Ce n\'est normalement pas nécessaire mais cela peut faciliter les gros téléchargements et les premières importations de grandes bibliothèques."), "deviceNotFound": MessageLookupByLibrary.simpleMessage("Appareil non trouvé"), "didYouKnow": MessageLookupByLibrary.simpleMessage("Le savais-tu ?"), @@ -774,12 +786,12 @@ class MessageLookup extends MessageLookupByLibrary { "Les observateurs peuvent toujours prendre des captures d\'écran ou enregistrer une copie de vos photos en utilisant des outils externes"), "disableDownloadWarningTitle": MessageLookupByLibrary.simpleMessage("Veuillez remarquer"), - "disableLinkMessage": m28, + "disableLinkMessage": m29, "disableTwofactor": MessageLookupByLibrary.simpleMessage( - "Désactiver la double-authentification"), + "Désactiver l\'authentification à deux facteurs"), "disablingTwofactorAuthentication": MessageLookupByLibrary.simpleMessage( - "Désactiver la double-authentification..."), + "Désactivation de l\'authentification à deux facteurs..."), "discord": MessageLookupByLibrary.simpleMessage("Discord"), "discover": MessageLookupByLibrary.simpleMessage("Découverte"), "discover_babies": MessageLookupByLibrary.simpleMessage("Bébés"), @@ -811,6 +823,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage( "Voulez-vous annuler les modifications que vous avez faites ?"), "done": MessageLookupByLibrary.simpleMessage("Terminé"), + "dontSave": MessageLookupByLibrary.simpleMessage("Ne pas enregistrer"), "doubleYourStorage": MessageLookupByLibrary.simpleMessage( "Doublez votre espace de stockage"), "download": MessageLookupByLibrary.simpleMessage("Télécharger"), @@ -818,9 +831,9 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Échec du téléchargement"), "downloading": MessageLookupByLibrary.simpleMessage("Téléchargement en cours..."), - "dropSupportEmail": m29, - "duplicateFileCountWithStorageSaved": m30, - "duplicateItemsGroup": m31, + "dropSupportEmail": m30, + "duplicateFileCountWithStorageSaved": m31, + "duplicateItemsGroup": m32, "edit": MessageLookupByLibrary.simpleMessage("Éditer"), "editLocation": MessageLookupByLibrary.simpleMessage("Modifier l’emplacement"), @@ -836,15 +849,16 @@ class MessageLookup extends MessageLookupByLibrary { "eligible": MessageLookupByLibrary.simpleMessage("éligible"), "email": MessageLookupByLibrary.simpleMessage("E-mail"), "emailAlreadyRegistered": - MessageLookupByLibrary.simpleMessage("E-mail déjà enregistré."), - "emailChangedTo": m32, - "emailNoEnteAccount": m33, + MessageLookupByLibrary.simpleMessage("Email déjà enregistré."), + "emailChangedTo": m33, + "emailDoesNotHaveEnteAccount": m34, + "emailNoEnteAccount": m35, "emailNotRegistered": - MessageLookupByLibrary.simpleMessage("E-mail non enregistré."), + MessageLookupByLibrary.simpleMessage("Email inconnu."), "emailVerificationToggle": MessageLookupByLibrary.simpleMessage( - "Vérification de l\'adresse e-mail"), - "emailYourLogs": - MessageLookupByLibrary.simpleMessage("Envoyez vos logs par e-mail"), + "Authentification à deux facteurs par email"), + "emailYourLogs": MessageLookupByLibrary.simpleMessage( + "Envoyez vos journaux par email"), "emergencyContacts": MessageLookupByLibrary.simpleMessage("Contacts d\'urgence"), "empty": MessageLookupByLibrary.simpleMessage("Vider"), @@ -852,9 +866,9 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Vider la corbeille ?"), "enable": MessageLookupByLibrary.simpleMessage("Activer"), "enableMLIndexingDesc": MessageLookupByLibrary.simpleMessage( - "Ente prend en charge l\'apprentissage automatique sur l\'appareil pour la reconnaissance faciale, la recherche magique et d\'autres fonctionnalités de recherche avancée"), + "Ente prend en charge l\'apprentissage automatique sur l\'appareil pour la reconnaissance des visages, la recherche magique et d\'autres fonctionnalités de recherche avancée"), "enableMachineLearningBanner": MessageLookupByLibrary.simpleMessage( - "Activer l\'apprentissage automatique pour la recherche magique et la reconnaissance faciale"), + "Activer l\'apprentissage automatique pour la reconnaissance des visages et la recherche magique"), "enableMaps": MessageLookupByLibrary.simpleMessage("Activer la carte"), "enableMapsDesc": MessageLookupByLibrary.simpleMessage( "Vos photos seront affichées sur une carte du monde.\n\nCette carte est hébergée par Open Street Map, et les emplacements exacts de vos photos ne sont jamais partagés.\n\nVous pouvez désactiver cette fonction à tout moment dans les Paramètres."), @@ -874,22 +888,22 @@ class MessageLookup extends MessageLookupByLibrary { "entePhotosPerm": MessageLookupByLibrary.simpleMessage( "Ente a besoin d\'une autorisation pour préserver vos photos"), "enteSubscriptionPitch": MessageLookupByLibrary.simpleMessage( - "Ente conserve vos souvenirs, ils sont donc toujours disponibles pour vous, même si vous perdez votre appareil."), + "Ente conserve vos souvenirs pour qu\'ils soient toujours disponible, même si vous perdez cet appareil."), "enteSubscriptionShareWithFamily": MessageLookupByLibrary.simpleMessage( "Vous pouvez également ajouter votre famille à votre forfait."), "enterAlbumName": MessageLookupByLibrary.simpleMessage("Saisir un nom d\'album"), "enterCode": MessageLookupByLibrary.simpleMessage("Entrer le code"), "enterCodeDescription": MessageLookupByLibrary.simpleMessage( - "Entrez le code fourni par votre ami pour réclamer de l\'espace de stockage gratuit pour vous deux"), + "Entrez le code fourni par votre ami·e pour débloquer l\'espace de stockage gratuit"), "enterDateOfBirth": MessageLookupByLibrary.simpleMessage("Anniversaire (facultatif)"), - "enterEmail": MessageLookupByLibrary.simpleMessage("Entrer e-mail"), + "enterEmail": MessageLookupByLibrary.simpleMessage("Entrer un email"), "enterFileName": MessageLookupByLibrary.simpleMessage("Entrez le nom du fichier"), "enterName": MessageLookupByLibrary.simpleMessage("Saisir un nom"), "enterNewPasswordToEncrypt": MessageLookupByLibrary.simpleMessage( - "Saisir un nouveau mot de passe pour l\'utiliser pour chiffrer vos données"), + "Saisissez votre nouveau mot de passe qui sera utilisé pour chiffrer vos données"), "enterPassword": MessageLookupByLibrary.simpleMessage("Saisissez le mot de passe"), "enterPasswordToEncrypt": MessageLookupByLibrary.simpleMessage( @@ -897,8 +911,8 @@ class MessageLookup extends MessageLookupByLibrary { "enterPersonName": MessageLookupByLibrary.simpleMessage( "Entrez le nom d\'une personne"), "enterPin": MessageLookupByLibrary.simpleMessage("Saisir le code PIN"), - "enterReferralCode": MessageLookupByLibrary.simpleMessage( - "Entrez le code de parrainage"), + "enterReferralCode": + MessageLookupByLibrary.simpleMessage("Code de parrainage"), "enterThe6digitCodeFromnyourAuthenticatorApp": MessageLookupByLibrary.simpleMessage( "Entrez le code à 6 chiffres de\nvotre application d\'authentification"), @@ -922,12 +936,13 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Exportez vos données"), "extraPhotosFound": MessageLookupByLibrary.simpleMessage( "Photos supplémentaires trouvées"), - "extraPhotosFoundFor": m34, + "extraPhotosFoundFor": m36, "faceNotClusteredYet": MessageLookupByLibrary.simpleMessage( "Ce visage n\'a pas encore été regroupé, veuillez revenir plus tard"), "faceRecognition": MessageLookupByLibrary.simpleMessage("Reconnaissance faciale"), "faces": MessageLookupByLibrary.simpleMessage("Visages"), + "failed": MessageLookupByLibrary.simpleMessage("Échec"), "failedToApplyCode": MessageLookupByLibrary.simpleMessage( "Impossible d\'appliquer le code"), "failedToCancel": @@ -935,7 +950,7 @@ class MessageLookup extends MessageLookupByLibrary { "failedToDownloadVideo": MessageLookupByLibrary.simpleMessage( "Échec du téléchargement de la vidéo"), "failedToFetchActiveSessions": MessageLookupByLibrary.simpleMessage( - "Impossible de récupérer les sessions actives"), + "Impossible de récupérer les connexions actives"), "failedToFetchOriginalForEdit": MessageLookupByLibrary.simpleMessage( "Impossible de récupérer l\'original pour l\'édition"), "failedToFetchReferralDetails": MessageLookupByLibrary.simpleMessage( @@ -973,8 +988,8 @@ class MessageLookup extends MessageLookupByLibrary { "fileTypes": MessageLookupByLibrary.simpleMessage("Types de fichiers"), "fileTypesAndNames": MessageLookupByLibrary.simpleMessage("Types et noms de fichiers"), - "filesBackedUpFromDevice": m35, - "filesBackedUpInAlbum": m36, + "filesBackedUpFromDevice": m37, + "filesBackedUpInAlbum": m38, "filesDeleted": MessageLookupByLibrary.simpleMessage("Fichiers supprimés"), "filesSavedToGallery": MessageLookupByLibrary.simpleMessage( @@ -993,25 +1008,25 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Stockage gratuit obtenu"), "freeStorageOnReferralSuccess": m4, "freeStorageUsable": - MessageLookupByLibrary.simpleMessage("Stockage gratuit utilisable"), + MessageLookupByLibrary.simpleMessage("Stockage gratuit disponible"), "freeTrial": MessageLookupByLibrary.simpleMessage("Essai gratuit"), - "freeTrialValidTill": m37, - "freeUpAccessPostDelete": m38, - "freeUpAmount": m39, + "freeTrialValidTill": m39, + "freeUpAccessPostDelete": m40, + "freeUpAmount": m41, "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, + "freeUpSpaceSaving": m42, "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": m41, + "genericProgress": m43, "goToSettings": MessageLookupByLibrary.simpleMessage("Allez aux réglages"), "googlePlayId": @@ -1029,7 +1044,7 @@ class MessageLookup extends MessageLookupByLibrary { "Nous ne suivons pas les installations d\'applications. Il serait utile que vous nous disiez comment vous nous avez trouvés !"), "hearUsWhereTitle": MessageLookupByLibrary.simpleMessage( "Comment avez-vous entendu parler de Ente? (facultatif)"), - "help": MessageLookupByLibrary.simpleMessage("Aide"), + "help": MessageLookupByLibrary.simpleMessage("Documentation"), "hidden": MessageLookupByLibrary.simpleMessage("Masqué"), "hide": MessageLookupByLibrary.simpleMessage("Masquer"), "hideContent": @@ -1039,14 +1054,14 @@ class MessageLookup extends MessageLookupByLibrary { "hideContentDescriptionIos": MessageLookupByLibrary.simpleMessage( "Masque le contenu de l\'application dans le sélecteur d\'application"), "hideSharedItemsFromHomeGallery": MessageLookupByLibrary.simpleMessage( - "Masquer les éléments partagés de la galerie d\'accueil"), + "Masquer les éléments partagés avec vous dans la galerie"), "hiding": MessageLookupByLibrary.simpleMessage("Masquage en cours..."), "hostedAtOsmFrance": MessageLookupByLibrary.simpleMessage("Hébergé chez OSM France"), "howItWorks": MessageLookupByLibrary.simpleMessage("Comment cela fonctionne"), "howToViewShareeVerificationID": MessageLookupByLibrary.simpleMessage( - "Demandez-leur d\'appuyer longuement sur leur adresse e-mail sur l\'écran des paramètres et de vérifier que les identifiants des deux appareils correspondent."), + "Demandez-leur d\'appuyer longuement sur leur adresse email dans l\'écran des paramètres pour vérifier que les identifiants des deux appareils correspondent."), "iOSGoToSettingsDescription": MessageLookupByLibrary.simpleMessage( "L\'authentification biométrique n\'est pas configurée sur votre appareil. Veuillez activer Touch ID ou Face ID sur votre téléphone."), "iOSLockOut": MessageLookupByLibrary.simpleMessage( @@ -1075,6 +1090,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Éléments indexés"), "indexingIsPaused": MessageLookupByLibrary.simpleMessage( "L\'indexation est en pause. Elle reprendra automatiquement lorsque l\'appareil sera prêt."), + "ineligible": MessageLookupByLibrary.simpleMessage("Non compatible"), "info": MessageLookupByLibrary.simpleMessage("Info"), "insecureDevice": MessageLookupByLibrary.simpleMessage("Appareil non sécurisé"), @@ -1093,13 +1109,13 @@ class MessageLookup extends MessageLookupByLibrary { "inviteToEnte": MessageLookupByLibrary.simpleMessage("Inviter à rejoindre Ente"), "inviteYourFriends": - MessageLookupByLibrary.simpleMessage("Invitez vos ami(e)s"), - "inviteYourFriendsToEnte": - MessageLookupByLibrary.simpleMessage("Invitez vos amis sur Ente"), + MessageLookupByLibrary.simpleMessage("Parrainez vos ami·e·s"), + "inviteYourFriendsToEnte": MessageLookupByLibrary.simpleMessage( + "Invitez vos ami·e·s sur Ente"), "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": m44, "itemsShowTheNumberOfDaysRemainingBeforePermanentDeletion": MessageLookupByLibrary.simpleMessage( "Les éléments montrent le nombre de jours restants avant la suppression définitive"), @@ -1107,6 +1123,8 @@ class MessageLookup extends MessageLookupByLibrary { "Les éléments sélectionnés seront supprimés de cet album"), "join": MessageLookupByLibrary.simpleMessage("Rejoindre"), "joinAlbum": MessageLookupByLibrary.simpleMessage("Rejoindre l\'album"), + "joinAlbumConfirmationDialogBody": MessageLookupByLibrary.simpleMessage( + "Rejoindre un album rendra votre e-mail visible à ses participants."), "joinAlbumSubtext": MessageLookupByLibrary.simpleMessage( "pour afficher et ajouter vos photos"), "joinAlbumSubtextViewer": MessageLookupByLibrary.simpleMessage( @@ -1131,25 +1149,34 @@ class MessageLookup extends MessageLookupByLibrary { "legacy": MessageLookupByLibrary.simpleMessage("Héritage"), "legacyAccounts": MessageLookupByLibrary.simpleMessage("Comptes hérités"), - "legacyInvite": m43, + "legacyInvite": m45, "legacyPageDesc": MessageLookupByLibrary.simpleMessage( "L\'héritage permet aux contacts de confiance d\'accéder à votre compte en votre absence."), "legacyPageDesc2": MessageLookupByLibrary.simpleMessage( - "Les contacts de confiance peuvent initier la récupération du compte et, s\'ils ne sont pas bloqués dans les 30 jours qui suivent, peuvent réinitialiser votre mot de passe et accéder à votre compte."), + "Ces contacts peuvent initier la récupération du compte et, s\'ils ne sont pas bloqués dans les 30 jours qui suivent, peuvent réinitialiser votre mot de passe et accéder à votre compte."), "light": MessageLookupByLibrary.simpleMessage("Clair"), "lightTheme": MessageLookupByLibrary.simpleMessage("Clair"), + "link": MessageLookupByLibrary.simpleMessage("Lier"), "linkCopiedToClipboard": MessageLookupByLibrary.simpleMessage( "Lien copié dans le presse-papiers"), "linkDeviceLimit": MessageLookupByLibrary.simpleMessage("Limite d\'appareil"), + "linkEmail": MessageLookupByLibrary.simpleMessage("Lier l\'email"), + "linkEmailToContactBannerCaption": + MessageLookupByLibrary.simpleMessage("pour un partage plus rapide"), "linkEnabled": MessageLookupByLibrary.simpleMessage("Activé"), "linkExpired": MessageLookupByLibrary.simpleMessage("Expiré"), - "linkExpiresOn": m44, + "linkExpiresOn": m46, "linkExpiry": MessageLookupByLibrary.simpleMessage("Expiration du lien"), "linkHasExpired": MessageLookupByLibrary.simpleMessage("Le lien a expiré"), "linkNeverExpires": MessageLookupByLibrary.simpleMessage("Jamais"), + "linkPerson": MessageLookupByLibrary.simpleMessage("Lier la personne"), + "linkPersonCaption": MessageLookupByLibrary.simpleMessage( + "pour une meilleure expérience de partage"), + "linkPersonToEmail": m47, + "linkPersonToEmailConfirmation": m48, "livePhotos": MessageLookupByLibrary.simpleMessage("Photos en direct"), "loadMessage1": MessageLookupByLibrary.simpleMessage( "Vous pouvez partager votre abonnement avec votre famille"), @@ -1204,10 +1231,10 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Se connecter avec TOTP"), "logout": MessageLookupByLibrary.simpleMessage("Déconnexion"), "logsDialogBody": MessageLookupByLibrary.simpleMessage( - "Cela enverra des logs pour nous aider à déboguer votre problème. Veuillez noter que les noms de fichiers seront inclus pour aider à suivre les problèmes avec des fichiers spécifiques."), + "Les journaux seront envoyés pour nous aider à déboguer votre problème. Les noms de fichiers seront inclus pour aider à identifier les problèmes."), "longPressAnEmailToVerifyEndToEndEncryption": MessageLookupByLibrary.simpleMessage( - "Appuyez longuement sur un e-mail pour vérifier le chiffrement de bout en bout."), + "Appuyez longuement sur un email pour vérifier le chiffrement de bout en bout."), "longpressOnAnItemToViewInFullscreen": MessageLookupByLibrary.simpleMessage( "Appuyez longuement sur un élément pour le voir en plein écran"), @@ -1216,8 +1243,8 @@ class MessageLookup extends MessageLookupByLibrary { "loopVideoOn": MessageLookupByLibrary.simpleMessage("Vidéo en boucle activée"), "lostDevice": MessageLookupByLibrary.simpleMessage("Appareil perdu ?"), - "machineLearning": - MessageLookupByLibrary.simpleMessage("Apprentissage automatique"), + "machineLearning": MessageLookupByLibrary.simpleMessage( + "Apprentissage automatique (IA locale)"), "magicSearch": MessageLookupByLibrary.simpleMessage("Recherche magique"), "magicSearchHint": MessageLookupByLibrary.simpleMessage( @@ -1236,9 +1263,10 @@ class MessageLookup extends MessageLookupByLibrary { "manualPairDesc": MessageLookupByLibrary.simpleMessage( "L\'appairage avec le code PIN fonctionne avec n\'importe quel écran sur lequel vous souhaitez voir votre album."), "map": MessageLookupByLibrary.simpleMessage("Carte"), - "maps": MessageLookupByLibrary.simpleMessage("Cartes"), + "maps": MessageLookupByLibrary.simpleMessage("Carte"), "mastodon": MessageLookupByLibrary.simpleMessage("Mastodon"), "matrix": MessageLookupByLibrary.simpleMessage("Matrix"), + "me": MessageLookupByLibrary.simpleMessage("Moi"), "memoryCount": m5, "merchandise": MessageLookupByLibrary.simpleMessage("Boutique"), "mergeWithExisting": @@ -1248,15 +1276,15 @@ class MessageLookup extends MessageLookupByLibrary { "mlConsent": MessageLookupByLibrary.simpleMessage( "Activer l\'apprentissage automatique"), "mlConsentConfirmation": MessageLookupByLibrary.simpleMessage( - "Je comprends, et souhaite activer l\'apprentissage automatique"), + "Je comprends et je souhaite activer l\'apprentissage automatique"), "mlConsentDescription": MessageLookupByLibrary.simpleMessage( - "Si vous activez l\'apprentissage automatique, Ente extraira des informations comme la géométrie des visages, incluant les photos partagées avec vous. \nCela se fera sur votre appareil, avec un cryptage de bout-en-bout de toutes les données biométriques générées."), + "Si vous activez l\'apprentissage automatique Ente extraira des informations comme la géométrie des visages, y compris dans les photos partagées avec vous. \nCela se fera localement sur votre appareil et avec un chiffrement bout-en-bout de toutes les données biométriques générées."), "mlConsentPrivacy": MessageLookupByLibrary.simpleMessage( "Veuillez cliquer ici pour plus de détails sur cette fonctionnalité dans notre politique de confidentialité"), "mlConsentTitle": MessageLookupByLibrary.simpleMessage( "Activer l\'apprentissage automatique ?"), "mlIndexingDescription": MessageLookupByLibrary.simpleMessage( - "Veuillez noter que l\'apprentissage automatique entraînera une augmentation de l\'utilisation de la bande passante et de la batterie, jusqu\'à ce que tous les éléments soient indexés. \nEnvisagez d\'utiliser l\'application de bureau pour une indexation plus rapide, tous les résultats seront automatiquement synchronisés."), + "Veuillez noter que l\'apprentissage automatique entraînera une augmentation de l\'utilisation de la connexion Internet et de la batterie jusqu\'à ce que tous les souvenirs soient indexés. \nVous pouvez utiliser l\'application de bureau Ente pour accélérer cette étape, tous les résultats seront synchronisés."), "mobileWebDesktop": MessageLookupByLibrary.simpleMessage("Mobile, Web, Ordinateur"), "moderateStrength": MessageLookupByLibrary.simpleMessage("Moyen"), @@ -1270,12 +1298,12 @@ class MessageLookup extends MessageLookupByLibrary { "mostRecent": MessageLookupByLibrary.simpleMessage("Les plus récents"), "mostRelevant": MessageLookupByLibrary.simpleMessage("Les plus pertinents"), - "moveItem": m45, + "moveItem": m49, "moveToAlbum": MessageLookupByLibrary.simpleMessage("Déplacer vers l\'album"), "moveToHiddenAlbum": MessageLookupByLibrary.simpleMessage( "Déplacer vers un album masqué"), - "movedSuccessfullyTo": m46, + "movedSuccessfullyTo": m50, "movedToTrash": MessageLookupByLibrary.simpleMessage("Déplacé dans la corbeille"), "movingFilesToAlbum": MessageLookupByLibrary.simpleMessage( @@ -1302,6 +1330,8 @@ class MessageLookup extends MessageLookupByLibrary { "noDeviceThatCanBeDeleted": MessageLookupByLibrary.simpleMessage( "Vous n\'avez pas de fichiers sur cet appareil qui peuvent être supprimés"), "noDuplicates": MessageLookupByLibrary.simpleMessage("✨ Aucun doublon"), + "noEnteAccountExclamation": + MessageLookupByLibrary.simpleMessage("Aucun compte Ente !"), "noExifData": MessageLookupByLibrary.simpleMessage("Aucune donnée EXIF"), "noFacesFound": @@ -1326,10 +1356,10 @@ class MessageLookup extends MessageLookupByLibrary { "noResults": MessageLookupByLibrary.simpleMessage("Aucun résultat"), "noResultsFound": MessageLookupByLibrary.simpleMessage("Aucun résultat trouvé"), - "noSuggestionsForPerson": m47, + "noSuggestionsForPerson": m51, "noSystemLockFound": MessageLookupByLibrary.simpleMessage("Aucun verrou système trouvé"), - "notPersonLabel": m48, + "notPersonLabel": m52, "nothingSharedWithYouYet": MessageLookupByLibrary.simpleMessage( "Rien n\'a encore été partagé avec vous"), "nothingToSeeHere": MessageLookupByLibrary.simpleMessage( @@ -1339,7 +1369,7 @@ class MessageLookup extends MessageLookupByLibrary { "onDevice": MessageLookupByLibrary.simpleMessage("Sur votre appareil"), "onEnte": MessageLookupByLibrary.simpleMessage( "Sur Ente"), - "onlyFamilyAdminCanChangeCode": m49, + "onlyFamilyAdminCanChangeCode": m53, "onlyThem": MessageLookupByLibrary.simpleMessage("Seulement eux"), "oops": MessageLookupByLibrary.simpleMessage("Oups"), "oopsCouldNotSaveEdits": MessageLookupByLibrary.simpleMessage( @@ -1363,8 +1393,8 @@ class MessageLookup extends MessageLookupByLibrary { "Ou fusionner avec une personne existante"), "orPickAnExistingOne": MessageLookupByLibrary.simpleMessage( "Ou sélectionner un email existant"), - "orPickFromYourContacts": - MessageLookupByLibrary.simpleMessage("or pick from your contacts"), + "orPickFromYourContacts": MessageLookupByLibrary.simpleMessage( + "ou choisissez parmi vos contacts"), "pair": MessageLookupByLibrary.simpleMessage("Associer"), "pairWithPin": MessageLookupByLibrary.simpleMessage("Appairer avec le code PIN"), @@ -1373,14 +1403,15 @@ class MessageLookup extends MessageLookupByLibrary { "panorama": MessageLookupByLibrary.simpleMessage("Panorama"), "passKeyPendingVerification": MessageLookupByLibrary.simpleMessage( "La vérification est toujours en attente"), - "passkey": MessageLookupByLibrary.simpleMessage("Code d\'accès"), + "passkey": MessageLookupByLibrary.simpleMessage( + "Authentification à deux facteurs avec une clé de sécurité"), "passkeyAuthTitle": MessageLookupByLibrary.simpleMessage( - "Vérification du code d\'accès"), + "Vérification de la clé de sécurité"), "password": MessageLookupByLibrary.simpleMessage("Mot de passe"), "passwordChangedSuccessfully": MessageLookupByLibrary.simpleMessage( "Le mot de passe a été modifié"), - "passwordLock": - MessageLookupByLibrary.simpleMessage("Mot de passe verrou"), + "passwordLock": MessageLookupByLibrary.simpleMessage( + "Verrouillage par mot de passe"), "passwordStrength": m0, "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"), @@ -1392,14 +1423,14 @@ 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": m50, + "paymentFailedTalkToProvider": m54, "pendingItems": MessageLookupByLibrary.simpleMessage("Éléments en attente"), "pendingSync": MessageLookupByLibrary.simpleMessage("Synchronisation en attente"), "people": MessageLookupByLibrary.simpleMessage("Personnes"), "peopleUsingYourCode": MessageLookupByLibrary.simpleMessage( - "Personnes utilisant votre code"), + "Filleul·e·s utilisant votre code"), "permDeleteWarning": MessageLookupByLibrary.simpleMessage( "Tous les éléments de la corbeille seront définitivement supprimés\n\nCette action ne peut pas être annulée"), "permanentlyDelete": @@ -1417,15 +1448,18 @@ class MessageLookup extends MessageLookupByLibrary { "photosAddedByYouWillBeRemovedFromTheAlbum": MessageLookupByLibrary.simpleMessage( "Les photos ajoutées par vous seront retirées de l\'album"), - "photosCount": m51, + "photosCount": m55, "pickCenterPoint": MessageLookupByLibrary.simpleMessage( "Sélectionner le point central"), "pinAlbum": MessageLookupByLibrary.simpleMessage("Épingler l\'album"), "pinLock": - MessageLookupByLibrary.simpleMessage("Verrouillage du code PIN"), + MessageLookupByLibrary.simpleMessage("Verrouillage par code PIN"), "playOnTv": MessageLookupByLibrary.simpleMessage("Lire l\'album sur la TV"), - "playStoreFreeTrialValidTill": m52, + "playOriginal": + MessageLookupByLibrary.simpleMessage("Lire l\'original"), + "playStoreFreeTrialValidTill": m56, + "playStream": MessageLookupByLibrary.simpleMessage("Lire le stream"), "playstoreSubscription": MessageLookupByLibrary.simpleMessage("Abonnement au PlayStore"), "pleaseCheckYourInternetConnectionAndTryAgain": @@ -1437,14 +1471,14 @@ class MessageLookup extends MessageLookupByLibrary { "pleaseContactSupportIfTheProblemPersists": MessageLookupByLibrary.simpleMessage( "Merci de contacter l\'assistance si cette erreur persiste"), - "pleaseEmailUsAt": m53, + "pleaseEmailUsAt": m57, "pleaseGrantPermissions": MessageLookupByLibrary.simpleMessage( "Veuillez accorder la permission"), "pleaseLoginAgain": MessageLookupByLibrary.simpleMessage("Veuillez vous reconnecter"), "pleaseSelectQuickLinksToRemove": MessageLookupByLibrary.simpleMessage( "Veuillez sélectionner les liens rapides à supprimer"), - "pleaseSendTheLogsTo": m54, + "pleaseSendTheLogsTo": m58, "pleaseTryAgain": MessageLookupByLibrary.simpleMessage("Veuillez réessayer"), "pleaseVerifyTheCodeYouHaveEntered": @@ -1464,26 +1498,37 @@ class MessageLookup extends MessageLookupByLibrary { "Appuyez et maintenez enfoncé pour lire la vidéo"), "pressAndHoldToPlayVideoDetailed": MessageLookupByLibrary.simpleMessage( "Maintenez appuyé sur l\'image pour lire la vidéo"), - "privacy": MessageLookupByLibrary.simpleMessage("Confidentialité"), + "privacy": MessageLookupByLibrary.simpleMessage( + "Politique de confidentialité"), "privacyPolicyTitle": MessageLookupByLibrary.simpleMessage( "Politique de Confidentialité"), "privateBackups": MessageLookupByLibrary.simpleMessage("Sauvegardes privées"), "privateSharing": MessageLookupByLibrary.simpleMessage("Partage privé"), "proceed": MessageLookupByLibrary.simpleMessage("Procéder"), - "processed": MessageLookupByLibrary.simpleMessage("Traité"), - "processingImport": m55, + "processed": MessageLookupByLibrary.simpleMessage("Appris"), + "processing": + MessageLookupByLibrary.simpleMessage("Traitement en cours"), + "processingImport": m59, + "processingVideos": + MessageLookupByLibrary.simpleMessage("Traitement des vidéos"), "publicLinkCreated": MessageLookupByLibrary.simpleMessage("Lien public créé"), "publicLinkEnabled": MessageLookupByLibrary.simpleMessage("Lien public activé"), + "queued": MessageLookupByLibrary.simpleMessage("En file d\'attente"), "quickLinks": MessageLookupByLibrary.simpleMessage("Liens rapides"), "radius": MessageLookupByLibrary.simpleMessage("Rayon"), "raiseTicket": MessageLookupByLibrary.simpleMessage("Créer un ticket"), "rateTheApp": MessageLookupByLibrary.simpleMessage("Évaluer l\'application"), "rateUs": MessageLookupByLibrary.simpleMessage("Évaluez-nous"), - "rateUsOnStore": m56, + "rateUsOnStore": m60, + "reassignMe": + MessageLookupByLibrary.simpleMessage("Réassigner \"Moi\""), + "reassignedToName": m61, + "reassigningLoading": + MessageLookupByLibrary.simpleMessage("Réassignation..."), "recover": MessageLookupByLibrary.simpleMessage("Récupérer"), "recoverAccount": MessageLookupByLibrary.simpleMessage("Récupérer un compte"), @@ -1492,26 +1537,26 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Récupérer un compte"), "recoveryInitiated": MessageLookupByLibrary.simpleMessage("Récupération initiée"), - "recoveryInitiatedDesc": m57, + "recoveryInitiatedDesc": m62, "recoveryKey": MessageLookupByLibrary.simpleMessage("Clé de secours"), "recoveryKeyCopiedToClipboard": MessageLookupByLibrary.simpleMessage( "Clé de secours copiée dans le presse-papiers"), "recoveryKeyOnForgotPassword": MessageLookupByLibrary.simpleMessage( "Si vous oubliez votre mot de passe, la seule façon de récupérer vos données sera grâce à cette clé."), "recoveryKeySaveDescription": MessageLookupByLibrary.simpleMessage( - "Nous ne stockons pas cette clé, veuillez garder cette clé de 24 mots dans un endroit sûr."), + "Nous ne la stockons pas, veuillez la conserver en lieu endroit sûr."), "recoveryKeySuccessBody": MessageLookupByLibrary.simpleMessage( "Génial ! Votre clé de récupération est valide. Merci de votre vérification.\n\nN\'oubliez pas de garder votre clé de récupération sauvegardée."), "recoveryKeyVerified": MessageLookupByLibrary.simpleMessage( "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": m58, + "recoveryReady": m63, "recoverySuccessful": MessageLookupByLibrary.simpleMessage("Restauration réussie !"), "recoveryWarning": MessageLookupByLibrary.simpleMessage( "Un contact de confiance tente d\'accéder à votre compte"), - "recoveryWarningBody": m59, + "recoveryWarningBody": m64, "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": @@ -1522,12 +1567,12 @@ class MessageLookup extends MessageLookupByLibrary { "reenterPin": MessageLookupByLibrary.simpleMessage("Ressaisir le code PIN"), "referFriendsAnd2xYourPlan": MessageLookupByLibrary.simpleMessage( - "Parrainez des amis et doublez votre abonnement"), + "Parrainez vos ami·e·s et doublez votre stockage"), "referralStep1": MessageLookupByLibrary.simpleMessage( - "1. Donnez ce code à vos amis"), + "1. Donnez ce code à vos ami·e·s"), "referralStep2": MessageLookupByLibrary.simpleMessage( - "2. Ils s\'inscrivent à une offre payante"), - "referralStep3": m60, + "2. Ils souscrivent à une offre payante"), + "referralStep3": m65, "referrals": MessageLookupByLibrary.simpleMessage("Parrainages"), "referralsAreCurrentlyPaused": MessageLookupByLibrary.simpleMessage( "Les recommandations sont actuellement en pause"), @@ -1559,7 +1604,7 @@ class MessageLookup extends MessageLookupByLibrary { "removeLink": MessageLookupByLibrary.simpleMessage("Supprimer le lien"), "removeParticipant": MessageLookupByLibrary.simpleMessage("Supprimer le participant"), - "removeParticipantBody": m61, + "removeParticipantBody": m66, "removePersonLabel": MessageLookupByLibrary.simpleMessage( "Supprimer le libellé d\'une personne"), "removePublicLink": @@ -1581,11 +1626,11 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Renommer le fichier"), "renewSubscription": MessageLookupByLibrary.simpleMessage("Renouveler l’abonnement"), - "renewsOn": m62, - "reportABug": MessageLookupByLibrary.simpleMessage("Signaler un bug"), - "reportBug": MessageLookupByLibrary.simpleMessage("Signaler un bug"), + "renewsOn": m67, + "reportABug": MessageLookupByLibrary.simpleMessage("Signaler un bogue"), + "reportBug": MessageLookupByLibrary.simpleMessage("Signaler un bogue"), "resendEmail": - MessageLookupByLibrary.simpleMessage("Renvoyer l\'e-mail"), + MessageLookupByLibrary.simpleMessage("Renvoyer l\'email"), "resetIgnoredFiles": MessageLookupByLibrary.simpleMessage( "Réinitialiser les fichiers ignorés"), "resetPasswordTitle": MessageLookupByLibrary.simpleMessage( @@ -1598,8 +1643,8 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Restaurer vers l\'album"), "restoringFiles": MessageLookupByLibrary.simpleMessage( "Restauration des fichiers..."), - "resumableUploads": - MessageLookupByLibrary.simpleMessage("Reprise des chargements"), + "resumableUploads": MessageLookupByLibrary.simpleMessage( + "Reprise automatique des transferts"), "retry": MessageLookupByLibrary.simpleMessage("Réessayer"), "review": MessageLookupByLibrary.simpleMessage("Suggestions"), "reviewDeduplicateItems": MessageLookupByLibrary.simpleMessage( @@ -1614,6 +1659,9 @@ class MessageLookup extends MessageLookupByLibrary { "safelyStored": MessageLookupByLibrary.simpleMessage("Stockage sécurisé"), "save": MessageLookupByLibrary.simpleMessage("Sauvegarder"), + "saveChangesBeforeLeavingQuestion": + MessageLookupByLibrary.simpleMessage( + "Enregistrer les modifications avant de quitter ?"), "saveCollage": MessageLookupByLibrary.simpleMessage("Enregistrer le collage"), "saveCopy": @@ -1660,11 +1708,11 @@ class MessageLookup extends MessageLookupByLibrary { "searchLocationEmptySection": MessageLookupByLibrary.simpleMessage( "Grouper les photos qui sont prises dans un certain angle d\'une photo"), "searchPeopleEmptySection": MessageLookupByLibrary.simpleMessage( - "Invitez des personnes, et vous verrez ici toutes les photos qu\'elles partagent"), + "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": m63, - "searchSectionsLengthMismatch": m64, + "searchResultCount": m68, + "searchSectionsLengthMismatch": m69, "security": MessageLookupByLibrary.simpleMessage("Sécurité"), "seePublicAlbumLinksInApp": MessageLookupByLibrary.simpleMessage( "Ouvrir les liens des albums publics dans l\'application"), @@ -1678,8 +1726,8 @@ class MessageLookup extends MessageLookupByLibrary { "selectAllShort": MessageLookupByLibrary.simpleMessage("Tout"), "selectCoverPhoto": MessageLookupByLibrary.simpleMessage( "Sélectionnez la photo de couverture"), - "selectFoldersForBackup": MessageLookupByLibrary.simpleMessage( - "Sélectionnez les dossiers à sauvegarder"), + "selectFoldersForBackup": + MessageLookupByLibrary.simpleMessage("Dossiers à sauvegarder"), "selectItemsToAdd": MessageLookupByLibrary.simpleMessage( "Sélectionner les éléments à ajouter"), "selectLanguage": @@ -1688,20 +1736,24 @@ class MessageLookup extends MessageLookupByLibrary { "Sélectionnez l\'application mail"), "selectMorePhotos": MessageLookupByLibrary.simpleMessage("Sélectionner plus de photos"), + "selectPersonToLink": MessageLookupByLibrary.simpleMessage( + "Sélectionnez la personne à associer"), "selectReason": MessageLookupByLibrary.simpleMessage("Sélectionnez une raison"), + "selectYourFace": + MessageLookupByLibrary.simpleMessage("Sélectionnez votre visage"), "selectYourPlan": MessageLookupByLibrary.simpleMessage("Sélectionner votre offre"), "selectedFilesAreNotOnEnte": MessageLookupByLibrary.simpleMessage( "Les fichiers sélectionnés ne sont pas sur Ente"), "selectedFoldersWillBeEncryptedAndBackedUp": MessageLookupByLibrary.simpleMessage( - "Les dossiers sélectionnés seront cryptés et sauvegardés"), + "Les dossiers sélectionnés seront chiffrés et sauvegardés"), "selectedItemsWillBeDeletedFromAllAlbumsAndMoved": MessageLookupByLibrary.simpleMessage( "Les éléments sélectionnés seront supprimés de tous les albums et déplacés dans la corbeille."), "selectedPhotos": m6, - "selectedPhotosWithYours": m65, + "selectedPhotosWithYours": m70, "send": MessageLookupByLibrary.simpleMessage("Envoyer"), "sendEmail": MessageLookupByLibrary.simpleMessage("Envoyer un e-mail"), "sendInvite": @@ -1735,16 +1787,16 @@ class MessageLookup extends MessageLookupByLibrary { "shareAnAlbumNow": MessageLookupByLibrary.simpleMessage( "Partagez un album maintenant"), "shareLink": MessageLookupByLibrary.simpleMessage("Partager le lien"), - "shareMyVerificationID": m66, + "shareMyVerificationID": m71, "shareOnlyWithThePeopleYouWant": MessageLookupByLibrary.simpleMessage( "Partagez uniquement avec les personnes que vous souhaitez"), "shareTextConfirmOthersVerificationID": m7, "shareTextRecommendUsingEnte": MessageLookupByLibrary.simpleMessage( "Téléchargez Ente pour pouvoir facilement partager des photos et vidéos en qualité originale\n\nhttps://ente.io"), - "shareTextReferralCode": m67, + "shareTextReferralCode": m72, "shareWithNonenteUsers": MessageLookupByLibrary.simpleMessage( "Partager avec des utilisateurs non-Ente"), - "shareWithPeopleSectionTitle": m68, + "shareWithPeopleSectionTitle": m73, "shareYourFirstAlbum": MessageLookupByLibrary.simpleMessage( "Partagez votre premier album"), "sharedAlbumSectionDescription": MessageLookupByLibrary.simpleMessage( @@ -1754,8 +1806,8 @@ class MessageLookup extends MessageLookupByLibrary { "sharedPhotoNotifications": MessageLookupByLibrary.simpleMessage("Nouvelles photos partagées"), "sharedPhotoNotificationsExplanation": MessageLookupByLibrary.simpleMessage( - "Recevoir des notifications quand quelqu\'un ajoute une photo à un album partagé dont vous faites partie"), - "sharedWith": m69, + "Recevoir des notifications quand quelqu\'un·e ajoute une photo à un album partagé dont vous faites partie"), + "sharedWith": m74, "sharedWithMe": MessageLookupByLibrary.simpleMessage("Partagés avec moi"), "sharedWithYou": @@ -1773,13 +1825,13 @@ class MessageLookup extends MessageLookupByLibrary { "Déconnecter les autres appareils"), "signUpTerms": MessageLookupByLibrary.simpleMessage( "J\'accepte les conditions d\'utilisation et la politique de confidentialité"), - "singleFileDeleteFromDevice": m70, + "singleFileDeleteFromDevice": m75, "singleFileDeleteHighlight": MessageLookupByLibrary.simpleMessage( "Elle sera supprimée de tous les albums."), - "singleFileInBothLocalAndRemote": m71, - "singleFileInRemoteOnly": m72, + "singleFileInBothLocalAndRemote": m76, + "singleFileInRemoteOnly": m77, "skip": MessageLookupByLibrary.simpleMessage("Ignorer"), - "social": MessageLookupByLibrary.simpleMessage("Réseaux sociaux"), + "social": MessageLookupByLibrary.simpleMessage("Retrouvez nous"), "someItemsAreInBothEnteAndYourDevice": MessageLookupByLibrary.simpleMessage( "Certains éléments sont à la fois sur Ente et votre appareil."), @@ -1828,10 +1880,12 @@ class MessageLookup extends MessageLookupByLibrary { "storageInGB": m1, "storageLimitExceeded": MessageLookupByLibrary.simpleMessage("Limite de stockage atteinte"), - "storageUsageInfo": m73, + "storageUsageInfo": m78, + "streamDetails": + MessageLookupByLibrary.simpleMessage("Détails du stream"), "strongStrength": MessageLookupByLibrary.simpleMessage("Forte"), - "subAlreadyLinkedErrMessage": m74, - "subWillBeCancelledOn": m75, + "subAlreadyLinkedErrMessage": m79, + "subWillBeCancelledOn": m80, "subscribe": MessageLookupByLibrary.simpleMessage("S\'abonner"), "subscribeToEnableSharing": MessageLookupByLibrary.simpleMessage( "Vous avez besoin d\'un abonnement payant actif pour activer le partage."), @@ -1845,10 +1899,10 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Désarchivé avec succès"), "successfullyUnhid": MessageLookupByLibrary.simpleMessage("Masquage réussi"), - "suggestFeatures": MessageLookupByLibrary.simpleMessage( - "Suggérer des fonctionnalités"), + "suggestFeatures": + MessageLookupByLibrary.simpleMessage("Suggérer une fonctionnalité"), "support": MessageLookupByLibrary.simpleMessage("Support"), - "syncProgress": m76, + "syncProgress": m81, "syncStopped": MessageLookupByLibrary.simpleMessage("Synchronisation arrêtée ?"), "syncing": MessageLookupByLibrary.simpleMessage( @@ -1861,7 +1915,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Appuyer pour déverrouiller"), "tapToUpload": MessageLookupByLibrary.simpleMessage("Appuyer pour envoyer"), - "tapToUploadIsIgnoredDue": m77, + "tapToUploadIsIgnoredDue": m82, "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"), @@ -1901,7 +1955,9 @@ class MessageLookup extends MessageLookupByLibrary { "Cette adresse mail est déjà utilisé"), "thisImageHasNoExifData": MessageLookupByLibrary.simpleMessage( "Cette image n\'a pas de données exif"), - "thisIsPersonVerificationId": m78, + "thisIsMeExclamation": + MessageLookupByLibrary.simpleMessage("C\'est moi !"), + "thisIsPersonVerificationId": m83, "thisIsYourVerificationId": MessageLookupByLibrary.simpleMessage( "Ceci est votre ID de vérification"), "thisWillLogYouOutOfTheFollowingDevice": @@ -1914,42 +1970,42 @@ class MessageLookup extends MessageLookupByLibrary { "Ceci supprimera les liens publics de tous les liens rapides sélectionnés."), "toEnableAppLockPleaseSetupDevicePasscodeOrScreen": MessageLookupByLibrary.simpleMessage( - "Pour activer le verrouillage d\'application, veuillez configurer le code d\'accès de l\'appareil ou le verrouillage de l\'écran dans les paramètres de votre système."), + "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."), "toHideAPhotoOrVideo": MessageLookupByLibrary.simpleMessage( "Pour masquer une photo ou une vidéo:"), "toResetVerifyEmail": MessageLookupByLibrary.simpleMessage( - "Pour réinitialiser votre mot de passe, veuillez d\'abord vérifier votre e-mail."), + "Pour réinitialiser votre mot de passe, vérifiez d\'abord votre email."), "todaysLogs": MessageLookupByLibrary.simpleMessage("Journaux du jour"), "tooManyIncorrectAttempts": MessageLookupByLibrary.simpleMessage( "Trop de tentatives incorrectes"), "total": MessageLookupByLibrary.simpleMessage("total"), "totalSize": MessageLookupByLibrary.simpleMessage("Taille totale"), "trash": MessageLookupByLibrary.simpleMessage("Corbeille"), - "trashDaysLeft": m79, + "trashDaysLeft": m84, "trim": MessageLookupByLibrary.simpleMessage("Recadrer"), "trustedContacts": MessageLookupByLibrary.simpleMessage("Contacts de confiance"), - "trustedInviteBody": m80, + "trustedInviteBody": m85, "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."), "twitter": MessageLookupByLibrary.simpleMessage("Twitter"), "twoMonthsFreeOnYearlyPlans": MessageLookupByLibrary.simpleMessage( "2 mois gratuits sur les forfaits annuels"), - "twofactor": - MessageLookupByLibrary.simpleMessage("Double authentification"), + "twofactor": MessageLookupByLibrary.simpleMessage( + "Authentification à deux facteurs (A2F)"), "twofactorAuthenticationHasBeenDisabled": MessageLookupByLibrary.simpleMessage( "L\'authentification à deux facteurs a été désactivée"), "twofactorAuthenticationPageTitle": MessageLookupByLibrary.simpleMessage( - "Authentification à deux facteurs"), + "Authentification à deux facteurs (A2F)"), "twofactorAuthenticationSuccessfullyReset": MessageLookupByLibrary.simpleMessage( "L\'authentification à deux facteurs a été réinitialisée avec succès "), "twofactorSetup": MessageLookupByLibrary.simpleMessage( "Configuration de l\'authentification à deux facteurs"), - "typeOfGallerGallerytypeIsNotSupportedForRename": m81, + "typeOfGallerGallerytypeIsNotSupportedForRename": m86, "unarchive": MessageLookupByLibrary.simpleMessage("Désarchiver"), "unarchiveAlbum": MessageLookupByLibrary.simpleMessage("Désarchiver l\'album"), @@ -1977,16 +2033,16 @@ class MessageLookup extends MessageLookupByLibrary { "updatingFolderSelection": MessageLookupByLibrary.simpleMessage( "Mise à jour de la sélection du dossier..."), "upgrade": MessageLookupByLibrary.simpleMessage("Améliorer"), - "uploadIsIgnoredDueToIgnorereason": m82, + "uploadIsIgnoredDueToIgnorereason": m87, "uploadingFilesToAlbum": MessageLookupByLibrary.simpleMessage( "Envoi des fichiers vers l\'album..."), - "uploadingMultipleMemories": m83, - "uploadingSingleMemory": - MessageLookupByLibrary.simpleMessage("Sauvegarde 1 souvenir..."), + "uploadingMultipleMemories": m88, + "uploadingSingleMemory": MessageLookupByLibrary.simpleMessage( + "Sauvegarde d\'un souvenir..."), "upto50OffUntil4thDec": MessageLookupByLibrary.simpleMessage( "Jusqu\'à 50% de réduction, jusqu\'au 4ème déc."), "usableReferralStorageInfo": MessageLookupByLibrary.simpleMessage( - "Le stockage utilisable est limité par votre offre actuelle. Le stockage excédentaire deviendra automatiquement utilisable lorsque vous mettez à niveau votre offre."), + "Le stockage gratuit possible est limité par votre offre actuelle. Vous pouvez au maximum doubler votre espace de stockage gratuitement, le stockage supplémentaire deviendra donc automatiquement utilisable lorsque vous mettrez à niveau votre offre."), "useAsCover": MessageLookupByLibrary.simpleMessage("Utiliser comme couverture"), "useDifferentPlayerInfo": MessageLookupByLibrary.simpleMessage( @@ -1998,7 +2054,7 @@ class MessageLookup extends MessageLookupByLibrary { "useSelectedPhoto": MessageLookupByLibrary.simpleMessage( "Utiliser la photo sélectionnée"), "usedSpace": MessageLookupByLibrary.simpleMessage("Stockage utilisé"), - "validTill": m84, + "validTill": m89, "verificationFailedPleaseTryAgain": MessageLookupByLibrary.simpleMessage( "La vérification a échouée, veuillez réessayer"), @@ -2006,11 +2062,11 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("ID de vérification"), "verify": MessageLookupByLibrary.simpleMessage("Vérifier"), "verifyEmail": - MessageLookupByLibrary.simpleMessage("Vérifier l\'e-mail"), - "verifyEmailID": m85, + MessageLookupByLibrary.simpleMessage("Vérifier l\'email"), + "verifyEmailID": m90, "verifyIDLabel": MessageLookupByLibrary.simpleMessage("Vérifier"), "verifyPasskey": - MessageLookupByLibrary.simpleMessage("Vérifier le code d\'accès"), + MessageLookupByLibrary.simpleMessage("Vérifier la clé de sécurité"), "verifyPassword": MessageLookupByLibrary.simpleMessage("Vérifier le mot de passe"), "verifying": @@ -2019,9 +2075,11 @@ class MessageLookup extends MessageLookupByLibrary { "Vérification de la clé de récupération..."), "videoInfo": MessageLookupByLibrary.simpleMessage("Informations vidéo"), "videoSmallCase": MessageLookupByLibrary.simpleMessage("vidéo"), + "videoStreaming": + MessageLookupByLibrary.simpleMessage("Streaming vidéo"), "videos": MessageLookupByLibrary.simpleMessage("Vidéos"), "viewActiveSessions": MessageLookupByLibrary.simpleMessage( - "Afficher les sessions actives"), + "Afficher les connexions actives"), "viewAddOnButton": MessageLookupByLibrary.simpleMessage( "Afficher les modules complémentaires"), "viewAll": MessageLookupByLibrary.simpleMessage("Tout afficher"), @@ -2036,9 +2094,9 @@ class MessageLookup extends MessageLookupByLibrary { "viewRecoveryKey": MessageLookupByLibrary.simpleMessage("Voir la clé de récupération"), "viewer": MessageLookupByLibrary.simpleMessage("Observateur"), - "viewersSuccessfullyAdded": m86, + "viewersSuccessfullyAdded": m91, "visitWebToManage": MessageLookupByLibrary.simpleMessage( - "Veuillez visiter web.ente.io pour gérer votre abonnement"), + "Vous pouvez gérer votre abonnement sur web.ente.io"), "waitingForVerification": MessageLookupByLibrary.simpleMessage( "En attente de vérification..."), "waitingForWifi": MessageLookupByLibrary.simpleMessage( @@ -2057,7 +2115,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": m87, + "yearsAgo": m92, "yes": MessageLookupByLibrary.simpleMessage("Oui"), "yesCancel": MessageLookupByLibrary.simpleMessage("Oui, annuler"), "yesConvertToViewer": MessageLookupByLibrary.simpleMessage( @@ -2090,7 +2148,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": m88, + "youHaveSuccessfullyFreedUp": m93, "yourAccountHasBeenDeleted": MessageLookupByLibrary.simpleMessage("Votre compte a été supprimé"), "yourMap": MessageLookupByLibrary.simpleMessage("Votre carte"), @@ -2111,9 +2169,6 @@ class MessageLookup extends MessageLookupByLibrary { "Votre abonnement a été mis à jour avec succès"), "yourVerificationCodeHasExpired": MessageLookupByLibrary.simpleMessage( "Votre code de vérification a expiré"), - "youveNoDuplicateFilesThatCanBeCleared": - MessageLookupByLibrary.simpleMessage( - "Vous n\'avez aucun fichier dédupliqué pouvant être nettoyé"), "youveNoFilesInThisAlbumThatCanBeDeleted": MessageLookupByLibrary.simpleMessage( "Vous n\'avez pas de fichiers dans cet album qui peuvent être supprimés"), diff --git a/mobile/lib/generated/intl/messages_gu.dart b/mobile/lib/generated/intl/messages_gu.dart index 5d87accc44..6c1d7e4d90 100644 --- a/mobile/lib/generated/intl/messages_gu.dart +++ b/mobile/lib/generated/intl/messages_gu.dart @@ -21,8 +21,5 @@ class MessageLookup extends MessageLookupByLibrary { String get localeName => 'gu'; final messages = _notInlinedMessages(_notInlinedMessages); - static Map _notInlinedMessages(_) => { - "orPickFromYourContacts": - MessageLookupByLibrary.simpleMessage("or pick from your contacts") - }; + static Map _notInlinedMessages(_) => {}; } diff --git a/mobile/lib/generated/intl/messages_he.dart b/mobile/lib/generated/intl/messages_he.dart index e65eb92134..c2371b1ec4 100644 --- a/mobile/lib/generated/intl/messages_he.dart +++ b/mobile/lib/generated/intl/messages_he.dart @@ -20,113 +20,113 @@ typedef String MessageIfAbsent(String messageStr, List args); class MessageLookup extends MessageLookupByLibrary { String get localeName => 'he'; - static String m10(count) => + static String m11(count) => "${Intl.plural(count, one: 'הוסף פריט', two: 'הוסף פריטים', many: 'הוסף פריטים', other: 'הוסף פריטים')}"; - static String m15(count) => + static String m16(count) => "${Intl.plural(count, zero: 'אין משתתפים', one: '1 משתתף', two: '2 משתתפים', other: '${count} משתתפים')}"; - static String m18(paymentProvider) => + static String m19(paymentProvider) => "אנא בטל את המנוי הקיים מ-${paymentProvider} קודם"; static String m3(user) => "${user} לא יוכל להוסיף עוד תמונות לאלבום זה\n\nהם עדיין יכולו להסיר תמונות קיימות שנוספו על ידיהם"; - static String m19(isFamilyMember, storageAmountInGb) => + static String m20(isFamilyMember, storageAmountInGb) => "${Intl.select(isFamilyMember, { 'true': 'קיבלת ${storageAmountInGb} GB עד כה', 'false': 'קיבלת ${storageAmountInGb} GB עד כה', 'other': 'קיבלת ${storageAmountInGb} GB עד כה!', })}"; - static String m23(familyAdminEmail) => + static String m24(familyAdminEmail) => "אנא צור קשר עם ${familyAdminEmail} על מנת לנהל את המנוי שלך"; - static String m24(provider) => + static String m25(provider) => "אנא צור איתנו קשר ב-support@ente.io על מנת לנהל את המנוי ${provider}."; - static String m26(count) => + static String m27(count) => "${Intl.plural(count, one: 'מחק ${count} פריט', two: 'מחק ${count} פריטים', other: 'מחק ${count} פריטים')}"; - static String m27(currentlyDeleting, totalCount) => + static String m28(currentlyDeleting, totalCount) => "מוחק ${currentlyDeleting} / ${totalCount}"; - static String m28(albumName) => + static String m29(albumName) => "זה יסיר את הלינק הפומבי שדרכו ניתן לגשת ל\"${albumName}\"."; - static String m29(supportEmail) => + static String m30(supportEmail) => "אנא תשלח דוא\"ל ל${supportEmail} מהכתובת דוא\"ל שנרשמת איתה"; - static String m31(count, formattedSize) => + static String m32(count, formattedSize) => "${count} קבצים, כל אחד ${formattedSize}"; - static String m33(email) => + static String m35(email) => "לא נמצא חשבון ente ל-${email}.\n\nשלח להם הזמנה על מנת לשתף תמונות."; static String m4(storageAmountInGB) => "${storageAmountInGB} GB כל פעם שמישהו נרשם עבור תוכנית בתשלום ומחיל את הקוד שלך"; - static String m37(endDate) => "ניסיון חינם בתוקף עד ל-${endDate}"; + static String m39(endDate) => "ניסיון חינם בתוקף עד ל-${endDate}"; - static String m42(count) => + static String m44(count) => "${Intl.plural(count, one: '${count} פריט', two: '${count} פריטים', many: '${count} פריטים', other: '${count} פריטים')}"; - static String m44(expiryTime) => "תוקף הקישור יפוג ב-${expiryTime}"; + static String m46(expiryTime) => "תוקף הקישור יפוג ב-${expiryTime}"; static String m5(count, formattedCount) => "${Intl.plural(count, one: '${formattedCount} זכרון', two: '${formattedCount} זכרונות', many: '${formattedCount} זכרונות', other: '${formattedCount} זכרונות')}"; - static String m45(count) => + static String m49(count) => "${Intl.plural(count, one: 'הזז פריט', two: 'הזז פריטים', many: 'הזז פריטים', other: 'הזז פריטים')}"; static String m0(passwordStrengthValue) => "חוזק הסיסמא: ${passwordStrengthValue}"; - static String m50(providerName) => + static String m54(providerName) => "אנא דבר עם התמיכה של ${providerName} אם אתה חוייבת"; - static String m56(storeName) => "דרג אותנו ב-${storeName}"; + static String m60(storeName) => "דרג אותנו ב-${storeName}"; - static String m60(storageInGB) => "3. שניכים מקבלים ${storageInGB} GB* בחינם"; + static String m65(storageInGB) => "3. שניכים מקבלים ${storageInGB} GB* בחינם"; - static String m61(userEmail) => + static String m66(userEmail) => "${userEmail} יוסר מהאלבום המשותף הזה\n\nגם תמונות שנוספו על ידיהם יוסרו מהאלבום"; static String m6(count) => "${count} נבחרו"; - static String m65(count, yourCount) => "${count} נבחרו (${yourCount} שלך)"; + static String m70(count, yourCount) => "${count} נבחרו (${yourCount} שלך)"; - static String m66(verificationID) => + static String m71(verificationID) => "הנה מזהה האימות שלי: ${verificationID} עבור ente.io."; static String m7(verificationID) => "היי, תוכל לוודא שזה מזהה האימות שלך של ente.io: ${verificationID}"; - static String m68(numberOfPeople) => + static String m73(numberOfPeople) => "${Intl.plural(numberOfPeople, zero: 'שתף עם אנשים ספציפיים', one: 'שותף עם איש 1', two: 'שותף עם 2 אנשים', other: 'שותף עם ${numberOfPeople} אנשים')}"; - static String m69(emailIDs) => "הושתף ע\"י ${emailIDs}"; + static String m74(emailIDs) => "הושתף ע\"י ${emailIDs}"; - static String m70(fileType) => "${fileType} יימחק מהמכשיר שלך."; + static String m75(fileType) => "${fileType} יימחק מהמכשיר שלך."; static String m1(storageAmountInGB) => "${storageAmountInGB} GB"; - static String m75(endDate) => "המנוי שלך יבוטל ב-${endDate}"; + static String m80(endDate) => "המנוי שלך יבוטל ב-${endDate}"; - static String m76(completed, total) => "${completed}/${total} זכרונות נשמרו"; + static String m81(completed, total) => "${completed}/${total} זכרונות נשמרו"; static String m8(storageAmountInGB) => "הם גם יקבלו ${storageAmountInGB} GB"; - static String m78(email) => "זה מזהה האימות של ${email}"; + static String m83(email) => "זה מזהה האימות של ${email}"; - static String m85(email) => "אמת ${email}"; + static String m90(email) => "אמת ${email}"; static String m2(email) => "שלחנו דוא\"ל ל${email}"; - static String m87(count) => + static String m92(count) => "${Intl.plural(count, one: 'לפני ${count} שנה', two: 'לפני ${count} שנים', many: 'לפני ${count} שנים', other: 'לפני ${count} שנים')}"; - static String m88(storageSaved) => "הצלחת לפנות ${storageSaved}!"; + static String m93(storageSaved) => "הצלחת לפנות ${storageSaved}!"; final messages = _notInlinedMessages(_notInlinedMessages); static Map _notInlinedMessages(_) => { @@ -141,7 +141,7 @@ class MessageLookup extends MessageLookupByLibrary { "addANewEmail": MessageLookupByLibrary.simpleMessage("הוסף דוא\"ל חדש"), "addCollaborator": MessageLookupByLibrary.simpleMessage("הוסף משתף פעולה"), - "addItem": m10, + "addItem": m11, "addLocationButton": MessageLookupByLibrary.simpleMessage("הוסף"), "addMore": MessageLookupByLibrary.simpleMessage("הוסף עוד"), "addPhotos": MessageLookupByLibrary.simpleMessage("הוסף תמונות"), @@ -158,7 +158,7 @@ class MessageLookup extends MessageLookupByLibrary { "after1Week": MessageLookupByLibrary.simpleMessage("אחרי שבוע 1"), "after1Year": MessageLookupByLibrary.simpleMessage("אחרי שנה 1"), "albumOwner": MessageLookupByLibrary.simpleMessage("בעלים"), - "albumParticipantsCount": m15, + "albumParticipantsCount": m16, "albumTitle": MessageLookupByLibrary.simpleMessage("כותרת האלבום"), "albumUpdated": MessageLookupByLibrary.simpleMessage("האלבום עודכן"), "albums": MessageLookupByLibrary.simpleMessage("אלבומים"), @@ -244,7 +244,7 @@ class MessageLookup extends MessageLookupByLibrary { "canOnlyRemoveFilesOwnedByYou": MessageLookupByLibrary.simpleMessage( "יכול להסיר רק קבצים שבבעלותך"), "cancel": MessageLookupByLibrary.simpleMessage("בטל"), - "cancelOtherSubscription": m18, + "cancelOtherSubscription": m19, "cancelSubscription": MessageLookupByLibrary.simpleMessage("בטל מנוי"), "cannotAddMorePhotosAfterBecomingViewer": m3, "cannotDeleteSharedFiles": MessageLookupByLibrary.simpleMessage( @@ -262,7 +262,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("תבע מקום אחסון בחינם"), "claimMore": MessageLookupByLibrary.simpleMessage("תבע עוד!"), "claimed": MessageLookupByLibrary.simpleMessage("נתבע"), - "claimedStorageSoFar": m19, + "claimedStorageSoFar": m20, "click": MessageLookupByLibrary.simpleMessage("• לחץ"), "close": MessageLookupByLibrary.simpleMessage("סגור"), "clubByCaptureTime": @@ -302,10 +302,10 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("אמת את מפתח השחזור"), "confirmYourRecoveryKey": MessageLookupByLibrary.simpleMessage("אמת את מפתח השחזור"), - "contactFamilyAdmin": m23, + "contactFamilyAdmin": m24, "contactSupport": MessageLookupByLibrary.simpleMessage("צור קשר עם התמיכה"), - "contactToManageSubscription": m24, + "contactToManageSubscription": m25, "continueLabel": MessageLookupByLibrary.simpleMessage("המשך"), "continueOnFreeTrial": MessageLookupByLibrary.simpleMessage("המשך עם ניסיון חינמי"), @@ -363,9 +363,9 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("למחוק אלבומים ריקים?"), "deleteFromBoth": MessageLookupByLibrary.simpleMessage("מחק משניהם"), "deleteFromDevice": MessageLookupByLibrary.simpleMessage("מחק מהמכשיר"), - "deleteItemCount": m26, + "deleteItemCount": m27, "deletePhotos": MessageLookupByLibrary.simpleMessage("מחק תמונות"), - "deleteProgress": m27, + "deleteProgress": m28, "deleteReason1": MessageLookupByLibrary.simpleMessage("חסר מאפיין מרכזי שאני צריך"), "deleteReason2": MessageLookupByLibrary.simpleMessage( @@ -390,7 +390,7 @@ class MessageLookup extends MessageLookupByLibrary { "צופים יכולים עדיין לקחת צילומי מסך או לשמור עותק של התמונות שלך בעזרת כלים חיצוניים"), "disableDownloadWarningTitle": MessageLookupByLibrary.simpleMessage("שים לב"), - "disableLinkMessage": m28, + "disableLinkMessage": m29, "disableTwofactor": MessageLookupByLibrary.simpleMessage("השבת דו-גורמי"), "discord": MessageLookupByLibrary.simpleMessage("Discord"), @@ -401,12 +401,12 @@ class MessageLookup extends MessageLookupByLibrary { "download": MessageLookupByLibrary.simpleMessage("הורד"), "downloadFailed": MessageLookupByLibrary.simpleMessage("ההורדה נכשלה"), "downloading": MessageLookupByLibrary.simpleMessage("מוריד..."), - "dropSupportEmail": m29, - "duplicateItemsGroup": m31, + "dropSupportEmail": m30, + "duplicateItemsGroup": m32, "edit": MessageLookupByLibrary.simpleMessage("ערוך"), "eligible": MessageLookupByLibrary.simpleMessage("זכאי"), "email": MessageLookupByLibrary.simpleMessage("דוא\"ל"), - "emailNoEnteAccount": m33, + "emailNoEnteAccount": m35, "emailVerificationToggle": MessageLookupByLibrary.simpleMessage("אימות מייל"), "empty": MessageLookupByLibrary.simpleMessage("ריק"), @@ -479,7 +479,7 @@ class MessageLookup extends MessageLookupByLibrary { "freeStorageUsable": MessageLookupByLibrary.simpleMessage("מקום אחסון שמיש"), "freeTrial": MessageLookupByLibrary.simpleMessage("ניסיון חינמי"), - "freeTrialValidTill": m37, + "freeTrialValidTill": m39, "freeUpDeviceSpace": MessageLookupByLibrary.simpleMessage("פנה אחסון במכשיר"), "freeUpSpace": MessageLookupByLibrary.simpleMessage("פנה מקום"), @@ -517,7 +517,7 @@ class MessageLookup extends MessageLookupByLibrary { "invite": MessageLookupByLibrary.simpleMessage("הזמן"), "inviteYourFriends": MessageLookupByLibrary.simpleMessage("הזמן את חברייך"), - "itemCount": m42, + "itemCount": m44, "itemsWillBeRemovedFromAlbum": MessageLookupByLibrary.simpleMessage( "הפריטים שנבחרו יוסרו מהאלבום הזה"), "keepPhotos": MessageLookupByLibrary.simpleMessage("השאר תמונות"), @@ -539,7 +539,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("מגבלת כמות מכשירים"), "linkEnabled": MessageLookupByLibrary.simpleMessage("מאופשר"), "linkExpired": MessageLookupByLibrary.simpleMessage("פג תוקף"), - "linkExpiresOn": m44, + "linkExpiresOn": m46, "linkExpiry": MessageLookupByLibrary.simpleMessage("תאריך תפוגה ללינק"), "linkHasExpired": MessageLookupByLibrary.simpleMessage("הקישור פג תוקף"), @@ -571,7 +571,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("פלאפון, דפדפן, שולחן עבודה"), "moderateStrength": MessageLookupByLibrary.simpleMessage("מתונה"), "monthly": MessageLookupByLibrary.simpleMessage("חודשי"), - "moveItem": m45, + "moveItem": m49, "moveToAlbum": MessageLookupByLibrary.simpleMessage("הזז לאלבום"), "movedToTrash": MessageLookupByLibrary.simpleMessage("הועבר לאשפה"), "movingFilesToAlbum": @@ -606,8 +606,6 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("אופציונלי, קצר ככל שתרצה..."), "orPickAnExistingOne": MessageLookupByLibrary.simpleMessage("או בחר באחד קיים"), - "orPickFromYourContacts": - MessageLookupByLibrary.simpleMessage("or pick from your contacts"), "password": MessageLookupByLibrary.simpleMessage("סיסמא"), "passwordChangedSuccessfully": MessageLookupByLibrary.simpleMessage("הססמה הוחלפה בהצלחה"), @@ -617,7 +615,7 @@ class MessageLookup extends MessageLookupByLibrary { "אנחנו לא שומרים את הסיסמא הזו, לכן אם אתה שוכח אותה, אנחנו לא יכולים לפענח את המידע שלך"), "paymentDetails": MessageLookupByLibrary.simpleMessage("פרטי תשלום"), "paymentFailed": MessageLookupByLibrary.simpleMessage("התשלום נכשל"), - "paymentFailedTalkToProvider": m50, + "paymentFailedTalkToProvider": m54, "peopleUsingYourCode": MessageLookupByLibrary.simpleMessage("אנשים משתמשים בקוד שלך"), "permanentlyDelete": @@ -659,7 +657,7 @@ class MessageLookup extends MessageLookupByLibrary { "raiseTicket": MessageLookupByLibrary.simpleMessage("צור ticket"), "rateTheApp": MessageLookupByLibrary.simpleMessage("דרג את האפליקציה"), "rateUs": MessageLookupByLibrary.simpleMessage("דרג אותנו"), - "rateUsOnStore": m56, + "rateUsOnStore": m60, "recover": MessageLookupByLibrary.simpleMessage("שחזר"), "recoverAccount": MessageLookupByLibrary.simpleMessage("שחזר חשבון"), "recoverButton": MessageLookupByLibrary.simpleMessage("שחזר"), @@ -685,7 +683,7 @@ class MessageLookup extends MessageLookupByLibrary { "1. תמסור את הקוד הזה לחברייך"), "referralStep2": MessageLookupByLibrary.simpleMessage( "2. הם נרשמים עבור תוכנית בתשלום"), - "referralStep3": m60, + "referralStep3": m65, "referrals": MessageLookupByLibrary.simpleMessage("הפניות"), "referralsAreCurrentlyPaused": MessageLookupByLibrary.simpleMessage("הפניות כרגע מושהות"), @@ -701,7 +699,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("הסר מהאלבום?"), "removeLink": MessageLookupByLibrary.simpleMessage("הסרת קישור"), "removeParticipant": MessageLookupByLibrary.simpleMessage("הסר משתתף"), - "removeParticipantBody": m61, + "removeParticipantBody": m66, "removePublicLink": MessageLookupByLibrary.simpleMessage("הסר לינק ציבורי"), "removeShareItemsWarning": MessageLookupByLibrary.simpleMessage( @@ -753,7 +751,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage( "התיקיות שנבחרו יוצפנו ויגובו"), "selectedPhotos": m6, - "selectedPhotosWithYours": m65, + "selectedPhotosWithYours": m70, "send": MessageLookupByLibrary.simpleMessage("שלח"), "sendEmail": MessageLookupByLibrary.simpleMessage("שלח דוא\"ל"), "sendInvite": MessageLookupByLibrary.simpleMessage("שלח הזמנה"), @@ -772,7 +770,7 @@ class MessageLookup extends MessageLookupByLibrary { "shareAnAlbumNow": MessageLookupByLibrary.simpleMessage("שתף אלבום עכשיו"), "shareLink": MessageLookupByLibrary.simpleMessage("שתף קישור"), - "shareMyVerificationID": m66, + "shareMyVerificationID": m71, "shareOnlyWithThePeopleYouWant": MessageLookupByLibrary.simpleMessage("שתף רק אם אנשים שאתה בוחר"), "shareTextConfirmOthersVerificationID": m7, @@ -780,7 +778,7 @@ class MessageLookup extends MessageLookupByLibrary { "הורד את ente על מנת שנוכל לשתף תמונות וסרטונים באיכות המקור באופן קל\n\nhttps://ente.io"), "shareWithNonenteUsers": MessageLookupByLibrary.simpleMessage( "שתף עם משתמשים שהם לא של ente"), - "shareWithPeopleSectionTitle": m68, + "shareWithPeopleSectionTitle": m73, "shareYourFirstAlbum": MessageLookupByLibrary.simpleMessage("שתף את האלבום הראשון שלך"), "sharedAlbumSectionDescription": MessageLookupByLibrary.simpleMessage( @@ -791,13 +789,13 @@ class MessageLookup extends MessageLookupByLibrary { "sharedPhotoNotificationsExplanation": MessageLookupByLibrary.simpleMessage( "קבל התראות כשמישהו מוסיף תמונה לאלבום משותף שאתה חלק ממנו"), - "sharedWith": m69, + "sharedWith": m74, "sharedWithMe": MessageLookupByLibrary.simpleMessage("שותף איתי"), "sharing": MessageLookupByLibrary.simpleMessage("משתף..."), "showMemories": MessageLookupByLibrary.simpleMessage("הצג זכרונות"), "signUpTerms": MessageLookupByLibrary.simpleMessage( "אני מסכים לתנאי שירות ולמדיניות הפרטיות"), - "singleFileDeleteFromDevice": m70, + "singleFileDeleteFromDevice": m75, "singleFileDeleteHighlight": MessageLookupByLibrary.simpleMessage("זה יימחק מכל האלבומים."), "skip": MessageLookupByLibrary.simpleMessage("דלג"), @@ -830,14 +828,14 @@ class MessageLookup extends MessageLookupByLibrary { "storageLimitExceeded": MessageLookupByLibrary.simpleMessage("גבול מקום האחסון נחרג"), "strongStrength": MessageLookupByLibrary.simpleMessage("חזקה"), - "subWillBeCancelledOn": m75, + "subWillBeCancelledOn": m80, "subscribe": MessageLookupByLibrary.simpleMessage("הרשם"), "subscription": MessageLookupByLibrary.simpleMessage("מנוי"), "success": MessageLookupByLibrary.simpleMessage("הצלחה"), "suggestFeatures": MessageLookupByLibrary.simpleMessage("הציעו מאפיינים"), "support": MessageLookupByLibrary.simpleMessage("תמיכה"), - "syncProgress": m76, + "syncProgress": m81, "syncing": MessageLookupByLibrary.simpleMessage("מסנכרן..."), "systemTheme": MessageLookupByLibrary.simpleMessage("מערכת"), "tapToCopy": MessageLookupByLibrary.simpleMessage("הקש כדי להעתיק"), @@ -858,7 +856,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage( "זה יכול לשמש לשחזור החשבון שלך במקרה ותאבד את הגורם השני"), "thisDevice": MessageLookupByLibrary.simpleMessage("מכשיר זה"), - "thisIsPersonVerificationId": m78, + "thisIsPersonVerificationId": m83, "thisIsYourVerificationId": MessageLookupByLibrary.simpleMessage("זה מזהה האימות שלך"), "thisWillLogYouOutOfTheFollowingDevice": @@ -902,7 +900,7 @@ class MessageLookup extends MessageLookupByLibrary { "verificationId": MessageLookupByLibrary.simpleMessage("מזהה אימות"), "verify": MessageLookupByLibrary.simpleMessage("אמת"), "verifyEmail": MessageLookupByLibrary.simpleMessage("אימות דוא\"ל"), - "verifyEmailID": m85, + "verifyEmailID": m90, "verifyIDLabel": MessageLookupByLibrary.simpleMessage("אמת"), "verifyPassword": MessageLookupByLibrary.simpleMessage("אמת סיסמא"), "verifyingRecoveryKey": @@ -923,7 +921,7 @@ class MessageLookup extends MessageLookupByLibrary { "weakStrength": MessageLookupByLibrary.simpleMessage("חלשה"), "welcomeBack": MessageLookupByLibrary.simpleMessage("ברוך שובך!"), "yearly": MessageLookupByLibrary.simpleMessage("שנתי"), - "yearsAgo": m87, + "yearsAgo": m92, "yes": MessageLookupByLibrary.simpleMessage("כן"), "yesCancel": MessageLookupByLibrary.simpleMessage("כן, בטל"), "yesConvertToViewer": @@ -946,7 +944,7 @@ class MessageLookup extends MessageLookupByLibrary { "אתה לא יכול לשנמך לתוכנית הזו"), "youCannotShareWithYourself": MessageLookupByLibrary.simpleMessage("אתה לא יכול לשתף עם עצמך"), - "youHaveSuccessfullyFreedUp": m88, + "youHaveSuccessfullyFreedUp": m93, "yourAccountHasBeenDeleted": MessageLookupByLibrary.simpleMessage("החשבון שלך נמחק"), "yourPlanWasSuccessfullyDowngraded": @@ -961,9 +959,6 @@ class MessageLookup extends MessageLookupByLibrary { "yourSubscriptionHasExpired": MessageLookupByLibrary.simpleMessage("פג תוקף המנוי שלך"), "yourSubscriptionWasUpdatedSuccessfully": - MessageLookupByLibrary.simpleMessage("המנוי שלך עודכן בהצלחה"), - "youveNoDuplicateFilesThatCanBeCleared": - MessageLookupByLibrary.simpleMessage( - "אין לך קבצים כפולים שניתן לנקות אותם") + MessageLookupByLibrary.simpleMessage("המנוי שלך עודכן בהצלחה") }; } diff --git a/mobile/lib/generated/intl/messages_hi.dart b/mobile/lib/generated/intl/messages_hi.dart index f018f5bac2..ff4756d8d4 100644 --- a/mobile/lib/generated/intl/messages_hi.dart +++ b/mobile/lib/generated/intl/messages_hi.dart @@ -81,8 +81,6 @@ class MessageLookup extends MessageLookupByLibrary { "हमारे एंड-टू-एंड एन्क्रिप्शन प्रोटोकॉल की प्रकृति के कारण, आपके डेटा को आपके पासवर्ड या रिकवरी कुंजी के बिना डिक्रिप्ट नहीं किया जा सकता है"), "ok": MessageLookupByLibrary.simpleMessage("ठीक है"), "oops": MessageLookupByLibrary.simpleMessage("ओह!"), - "orPickFromYourContacts": - MessageLookupByLibrary.simpleMessage("or pick from your contacts"), "password": MessageLookupByLibrary.simpleMessage("पासवर्ड"), "recoverButton": MessageLookupByLibrary.simpleMessage("पुनः प्राप्त"), "recoverySuccessful": diff --git a/mobile/lib/generated/intl/messages_hu.dart b/mobile/lib/generated/intl/messages_hu.dart index 81c4eadaa4..fbe4a79ba1 100644 --- a/mobile/lib/generated/intl/messages_hu.dart +++ b/mobile/lib/generated/intl/messages_hu.dart @@ -38,8 +38,6 @@ class MessageLookup extends MessageLookupByLibrary { "feedback": MessageLookupByLibrary.simpleMessage("Visszajelzés"), "invalidEmailAddress": MessageLookupByLibrary.simpleMessage("Érvénytelen e-mail cím"), - "orPickFromYourContacts": - MessageLookupByLibrary.simpleMessage("or pick from your contacts"), "verify": MessageLookupByLibrary.simpleMessage("Hitelesítés") }; } diff --git a/mobile/lib/generated/intl/messages_id.dart b/mobile/lib/generated/intl/messages_id.dart index bc3b312155..b7a0203345 100644 --- a/mobile/lib/generated/intl/messages_id.dart +++ b/mobile/lib/generated/intl/messages_id.dart @@ -20,33 +20,33 @@ typedef String MessageIfAbsent(String messageStr, List args); class MessageLookup extends MessageLookupByLibrary { String get localeName => 'id'; - static String m9(count) => + static String m10(count) => "${Intl.plural(count, other: 'Tambahkan kolaborator')}"; - static String m10(count) => "${Intl.plural(count, other: 'Tambahkan item')}"; + static String m11(count) => "${Intl.plural(count, other: 'Tambahkan item')}"; - static String m11(storageAmount, endDate) => + static String m12(storageAmount, endDate) => "Add-on ${storageAmount} kamu berlaku sampai ${endDate}"; - static String m13(emailOrName) => "Ditambahkan oleh ${emailOrName}"; + static String m14(emailOrName) => "Ditambahkan oleh ${emailOrName}"; - static String m14(albumName) => "Berhasil ditambahkan ke ${albumName}"; + static String m15(albumName) => "Berhasil ditambahkan ke ${albumName}"; - static String m15(count) => + static String m16(count) => "${Intl.plural(count, zero: '0 Peserta', one: '1 Peserta', other: '${count} Peserta')}"; - static String m16(versionValue) => "Versi: ${versionValue}"; + static String m17(versionValue) => "Versi: ${versionValue}"; - static String m17(freeAmount, storageUnit) => + static String m18(freeAmount, storageUnit) => "${freeAmount} ${storageUnit} tersedia"; - static String m18(paymentProvider) => + static String m19(paymentProvider) => "Harap batalkan langganan kamu di ${paymentProvider} terlebih dahulu"; static String m3(user) => "${user} tidak akan dapat menambahkan foto lagi ke album ini\n\nIa masih dapat menghapus foto yang ditambahkan olehnya sendiri"; - static String m19(isFamilyMember, storageAmountInGb) => + static String m20(isFamilyMember, storageAmountInGb) => "${Intl.select(isFamilyMember, { 'true': 'Keluargamu saat ini telah memperoleh ${storageAmountInGb} GB', @@ -54,153 +54,153 @@ class MessageLookup extends MessageLookupByLibrary { 'other': 'Kamu saat ini telah memperoleh ${storageAmountInGb} GB!', })}"; - static String m20(albumName) => "Link kolaborasi terbuat untuk ${albumName}"; + static String m21(albumName) => "Link kolaborasi terbuat untuk ${albumName}"; - static String m23(familyAdminEmail) => + static String m24(familyAdminEmail) => "Silakan hubungi ${familyAdminEmail} untuk mengatur langgananmu"; - static String m24(provider) => + static String m25(provider) => "Silakan hubungi kami di support@ente.io untuk mengatur langganan ${provider} kamu."; - static String m25(endpoint) => "Terhubung ke ${endpoint}"; + static String m26(endpoint) => "Terhubung ke ${endpoint}"; - static String m26(count) => + static String m27(count) => "${Intl.plural(count, one: 'Hapus ${count} item', other: 'Hapus ${count} item')}"; - static String m27(currentlyDeleting, totalCount) => + static String m28(currentlyDeleting, totalCount) => "Menghapus ${currentlyDeleting} / ${totalCount}"; - static String m28(albumName) => + static String m29(albumName) => "Ini akan menghapus link publik yang digunakan untuk mengakses \"${albumName}\"."; - static String m29(supportEmail) => + static String m30(supportEmail) => "Silakan kirimkan email ke ${supportEmail} dari alamat email terdaftar kamu"; - static String m30(count, storageSaved) => + static String m31(count, storageSaved) => "Kamu telah menghapus ${Intl.plural(count, other: '${count} file duplikat')} dan membersihkan (${storageSaved}!)"; - static String m32(newEmail) => "Email diubah menjadi ${newEmail}"; + static String m33(newEmail) => "Email diubah menjadi ${newEmail}"; - static String m33(email) => + static String m35(email) => "${email} tidak punya akun Ente.\n\nUndang dia untuk berbagi foto."; - static String m35(count, formattedNumber) => + static String m37(count, formattedNumber) => "${Intl.plural(count, other: '${formattedNumber} file')} di perangkat ini telah berhasil dicadangkan"; - static String m36(count, formattedNumber) => + static String m38(count, formattedNumber) => "${Intl.plural(count, other: '${formattedNumber} file')} dalam album ini telah berhasil dicadangkan"; static String m4(storageAmountInGB) => "${storageAmountInGB} GB setiap kali orang mendaftar dengan paket berbayar lalu menerapkan kode milikmu"; - static String m37(endDate) => "Percobaan gratis berlaku hingga ${endDate}"; + static String m39(endDate) => "Percobaan gratis berlaku hingga ${endDate}"; - static String m38(count) => + static String m40(count) => "Kamu masih bisa mengakses ${Intl.plural(count, other: 'filenya')} di Ente selama kamu masih berlangganan"; - static String m39(sizeInMBorGB) => "Bersihkan ${sizeInMBorGB}"; + static String m41(sizeInMBorGB) => "Bersihkan ${sizeInMBorGB}"; - static String m40(count, formattedSize) => + static String m42(count, formattedSize) => "${Intl.plural(count, other: 'File tersebut bisa dihapus dari perangkat ini untuk membersihkan ${formattedSize}')}"; - static String m41(currentlyProcessing, totalCount) => + static String m43(currentlyProcessing, totalCount) => "Memproses ${currentlyProcessing} / ${totalCount}"; - static String m42(count) => "${Intl.plural(count, other: '${count} item')}"; + static String m44(count) => "${Intl.plural(count, other: '${count} item')}"; - static String m44(expiryTime) => "Link akan kedaluwarsa pada ${expiryTime}"; + static String m46(expiryTime) => "Link akan kedaluwarsa pada ${expiryTime}"; static String m5(count, formattedCount) => "${Intl.plural(count, zero: 'tiada kenangan', one: '${formattedCount} kenangan', other: '${formattedCount} kenangan')}"; - static String m45(count) => "${Intl.plural(count, other: 'Pindahkan item')}"; + static String m49(count) => "${Intl.plural(count, other: 'Pindahkan item')}"; - static String m46(albumName) => "Berhasil dipindahkan ke ${albumName}"; + static String m50(albumName) => "Berhasil dipindahkan ke ${albumName}"; - static String m49(familyAdminEmail) => + static String m53(familyAdminEmail) => "Harap hubungi ${familyAdminEmail} untuk mengubah kode kamu."; static String m0(passwordStrengthValue) => "Keamanan sandi: ${passwordStrengthValue}"; - static String m50(providerName) => + static String m54(providerName) => "Harap hubungi dukungan ${providerName} jika kamu dikenai biaya"; - static String m52(endDate) => + static String m56(endDate) => "Percobaan gratis berlaku hingga ${endDate}.\nKamu dapat memilih paket berbayar setelahnya."; - static String m53(toEmail) => "Silakan kirimi kami email di ${toEmail}"; + static String m57(toEmail) => "Silakan kirimi kami email di ${toEmail}"; - static String m54(toEmail) => "Silakan kirim log-nya ke \n${toEmail}"; + static String m58(toEmail) => "Silakan kirim log-nya ke \n${toEmail}"; - static String m56(storeName) => "Beri nilai di ${storeName}"; + static String m60(storeName) => "Beri nilai di ${storeName}"; - static String m60(storageInGB) => + static String m65(storageInGB) => "3. Kalian berdua mendapat ${storageInGB} GB* gratis"; - static String m61(userEmail) => + static String m66(userEmail) => "${userEmail} akan dikeluarkan dari album berbagi ini\n\nSemua foto yang ia tambahkan juga akan dihapus dari album ini"; - static String m62(endDate) => "Langganan akan diperpanjang pada ${endDate}"; + static String m67(endDate) => "Langganan akan diperpanjang pada ${endDate}"; - static String m63(count) => + static String m68(count) => "${Intl.plural(count, other: '${count} hasil ditemukan')}"; static String m6(count) => "${count} terpilih"; - static String m65(count, yourCount) => + static String m70(count, yourCount) => "${count} dipilih (${yourCount} milikmu)"; - static String m66(verificationID) => + static String m71(verificationID) => "Ini ID Verifikasi saya di ente.io: ${verificationID}."; static String m7(verificationID) => "Halo, bisakah kamu pastikan bahwa ini adalah ID Verifikasi ente.io milikmu: ${verificationID}"; - static String m67(referralCode, referralStorageInGB) => + static String m72(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 m68(numberOfPeople) => + static String m73(numberOfPeople) => "${Intl.plural(numberOfPeople, zero: 'Bagikan dengan orang tertentu', one: 'Berbagi dengan 1 orang', other: 'Berbagi dengan ${numberOfPeople} orang')}"; - static String m69(emailIDs) => "Dibagikan dengan ${emailIDs}"; + static String m74(emailIDs) => "Dibagikan dengan ${emailIDs}"; - static String m70(fileType) => + static String m75(fileType) => "${fileType} ini akan dihapus dari perangkat ini."; - static String m71(fileType) => + static String m76(fileType) => "${fileType} ini tersimpan di Ente dan juga di perangkat ini."; - static String m72(fileType) => "${fileType} ini akan dihapus dari Ente."; + static String m77(fileType) => "${fileType} ini akan dihapus dari Ente."; static String m1(storageAmountInGB) => "${storageAmountInGB} GB"; - static String m73( + static String m78( usedAmount, usedStorageUnit, totalAmount, totalStorageUnit) => "${usedAmount} ${usedStorageUnit} dari ${totalAmount} ${totalStorageUnit} terpakai"; - static String m74(id) => + static String m79(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 m75(endDate) => + static String m80(endDate) => "Langganan kamu akan dibatalkan pada ${endDate}"; static String m8(storageAmountInGB) => "Ia juga mendapat ${storageAmountInGB} GB"; - static String m78(email) => "Ini adalah ID Verifikasi milik ${email}"; + static String m83(email) => "Ini adalah ID Verifikasi milik ${email}"; - static String m84(endDate) => "Berlaku hingga ${endDate}"; + static String m89(endDate) => "Berlaku hingga ${endDate}"; - static String m85(email) => "Verifikasi ${email}"; + static String m90(email) => "Verifikasi ${email}"; static String m2(email) => "Kami telah mengirimkan email ke ${email}"; - static String m87(count) => + static String m92(count) => "${Intl.plural(count, other: '${count} tahun lalu')}"; - static String m88(storageSaved) => + static String m93(storageSaved) => "Kamu telah berhasil membersihkan ${storageSaved}!"; final messages = _notInlinedMessages(_notInlinedMessages); @@ -219,14 +219,14 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Tambah email baru"), "addCollaborator": MessageLookupByLibrary.simpleMessage("Tambah kolaborator"), - "addCollaborators": m9, + "addCollaborators": m10, "addFromDevice": MessageLookupByLibrary.simpleMessage("Tambahkan dari perangkat"), - "addItem": m10, + "addItem": m11, "addLocation": MessageLookupByLibrary.simpleMessage("Tambah tempat"), "addLocationButton": MessageLookupByLibrary.simpleMessage("Tambah"), "addMore": MessageLookupByLibrary.simpleMessage("Tambah lagi"), - "addOnValidTill": m11, + "addOnValidTill": m12, "addPhotos": MessageLookupByLibrary.simpleMessage("Tambah foto"), "addSelected": MessageLookupByLibrary.simpleMessage("Tambahkan yang dipilih"), @@ -236,8 +236,8 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Tambah ke album tersembunyi"), "addViewer": MessageLookupByLibrary.simpleMessage("Tambahkan pemirsa"), "addedAs": MessageLookupByLibrary.simpleMessage("Ditambahkan sebagai"), - "addedBy": m13, - "addedSuccessfullyTo": m14, + "addedBy": m14, + "addedSuccessfullyTo": m15, "addingToFavorites": MessageLookupByLibrary.simpleMessage("Menambahkan ke favorit..."), "advanced": MessageLookupByLibrary.simpleMessage("Lanjutan"), @@ -248,7 +248,7 @@ class MessageLookup extends MessageLookupByLibrary { "after1Week": MessageLookupByLibrary.simpleMessage("Setelah 1 minggu"), "after1Year": MessageLookupByLibrary.simpleMessage("Setelah 1 tahun"), "albumOwner": MessageLookupByLibrary.simpleMessage("Pemilik"), - "albumParticipantsCount": m15, + "albumParticipantsCount": m16, "albumTitle": MessageLookupByLibrary.simpleMessage("Judul album"), "albumUpdated": MessageLookupByLibrary.simpleMessage("Album diperbarui"), @@ -279,7 +279,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Android, iOS, Web, Desktop"), "androidSignInTitle": MessageLookupByLibrary.simpleMessage("Autentikasi diperlukan"), - "appVersion": m16, + "appVersion": m17, "appleId": MessageLookupByLibrary.simpleMessage("ID Apple"), "apply": MessageLookupByLibrary.simpleMessage("Terapkan"), "applyCodeTitle": MessageLookupByLibrary.simpleMessage("Terapkan kode"), @@ -345,7 +345,7 @@ class MessageLookup extends MessageLookupByLibrary { "autoPairDesc": MessageLookupByLibrary.simpleMessage( "Taut otomatis hanya tersedia di perangkat yang mendukung Chromecast."), "available": MessageLookupByLibrary.simpleMessage("Tersedia"), - "availableStorageSpace": m17, + "availableStorageSpace": m18, "backedUpFolders": MessageLookupByLibrary.simpleMessage("Folder yang dicadangkan"), "backup": MessageLookupByLibrary.simpleMessage("Pencadangan"), @@ -368,7 +368,7 @@ class MessageLookup extends MessageLookupByLibrary { "canOnlyRemoveFilesOwnedByYou": MessageLookupByLibrary.simpleMessage( "Hanya dapat menghapus berkas yang dimiliki oleh mu"), "cancel": MessageLookupByLibrary.simpleMessage("Batal"), - "cancelOtherSubscription": m18, + "cancelOtherSubscription": m19, "cancelSubscription": MessageLookupByLibrary.simpleMessage("Batalkan langganan"), "cannotAddMorePhotosAfterBecomingViewer": m3, @@ -401,7 +401,7 @@ class MessageLookup extends MessageLookupByLibrary { "claimMore": MessageLookupByLibrary.simpleMessage("Peroleh lebih banyak!"), "claimed": MessageLookupByLibrary.simpleMessage("Diperoleh"), - "claimedStorageSoFar": m19, + "claimedStorageSoFar": m20, "clearIndexes": MessageLookupByLibrary.simpleMessage("Hapus indeks"), "click": MessageLookupByLibrary.simpleMessage("• Click"), "close": MessageLookupByLibrary.simpleMessage("Tutup"), @@ -417,7 +417,7 @@ class MessageLookup extends MessageLookupByLibrary { "Buat link untuk memungkinkan orang lain menambahkan dan melihat foto yang ada pada album bersama kamu tanpa memerlukan app atau akun Ente. Ideal untuk mengumpulkan foto pada suatu acara."), "collaborativeLink": MessageLookupByLibrary.simpleMessage("Link kolaborasi"), - "collaborativeLinkCreatedFor": m20, + "collaborativeLinkCreatedFor": m21, "collaborator": MessageLookupByLibrary.simpleMessage("Kolaborator"), "collaboratorsCanAddPhotosAndVideosToTheSharedAlbum": MessageLookupByLibrary.simpleMessage( @@ -443,10 +443,10 @@ class MessageLookup extends MessageLookupByLibrary { "Konfirmasi kunci pemulihan kamu"), "connectToDevice": MessageLookupByLibrary.simpleMessage("Hubungkan ke perangkat"), - "contactFamilyAdmin": m23, + "contactFamilyAdmin": m24, "contactSupport": MessageLookupByLibrary.simpleMessage("Hubungi dukungan"), - "contactToManageSubscription": m24, + "contactToManageSubscription": m25, "contacts": MessageLookupByLibrary.simpleMessage("Kontak"), "continueLabel": MessageLookupByLibrary.simpleMessage("Lanjut"), "continueOnFreeTrial": MessageLookupByLibrary.simpleMessage( @@ -487,7 +487,7 @@ class MessageLookup extends MessageLookupByLibrary { "currentUsageIs": MessageLookupByLibrary.simpleMessage("Pemakaian saat ini sebesar "), "custom": MessageLookupByLibrary.simpleMessage("Kustom"), - "customEndpoint": m25, + "customEndpoint": m26, "darkTheme": MessageLookupByLibrary.simpleMessage("Gelap"), "dayToday": MessageLookupByLibrary.simpleMessage("Hari Ini"), "dayYesterday": MessageLookupByLibrary.simpleMessage("Kemarin"), @@ -516,9 +516,9 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Hapus dari perangkat ini"), "deleteFromEnte": MessageLookupByLibrary.simpleMessage("Hapus dari Ente"), - "deleteItemCount": m26, + "deleteItemCount": m27, "deletePhotos": MessageLookupByLibrary.simpleMessage("Hapus foto"), - "deleteProgress": m27, + "deleteProgress": m28, "deleteReason1": MessageLookupByLibrary.simpleMessage( "Fitur penting yang saya perlukan tidak ada"), "deleteReason2": MessageLookupByLibrary.simpleMessage( @@ -554,7 +554,7 @@ class MessageLookup extends MessageLookupByLibrary { "Orang yang melihat masih bisa mengambil tangkapan layar atau menyalin foto kamu menggunakan alat eksternal"), "disableDownloadWarningTitle": MessageLookupByLibrary.simpleMessage("Perlu diketahui"), - "disableLinkMessage": m28, + "disableLinkMessage": m29, "disableTwofactor": MessageLookupByLibrary.simpleMessage( "Nonaktifkan autentikasi dua langkah"), "disablingTwofactorAuthentication": @@ -589,8 +589,8 @@ class MessageLookup extends MessageLookupByLibrary { "downloadFailed": MessageLookupByLibrary.simpleMessage("Gagal mengunduh"), "downloading": MessageLookupByLibrary.simpleMessage("Mengunduh..."), - "dropSupportEmail": m29, - "duplicateFileCountWithStorageSaved": m30, + "dropSupportEmail": m30, + "duplicateFileCountWithStorageSaved": m31, "edit": MessageLookupByLibrary.simpleMessage("Edit"), "editLocation": MessageLookupByLibrary.simpleMessage("Edit lokasi"), "editLocationTagTitle": @@ -602,8 +602,8 @@ class MessageLookup extends MessageLookupByLibrary { "Perubahan lokasi hanya akan terlihat di Ente"), "eligible": MessageLookupByLibrary.simpleMessage("memenuhi syarat"), "email": MessageLookupByLibrary.simpleMessage("Email"), - "emailChangedTo": m32, - "emailNoEnteAccount": m33, + "emailChangedTo": m33, + "emailNoEnteAccount": m35, "emailVerificationToggle": MessageLookupByLibrary.simpleMessage("Verifikasi email"), "empty": MessageLookupByLibrary.simpleMessage("Kosongkan"), @@ -700,8 +700,8 @@ class MessageLookup extends MessageLookupByLibrary { "fileTypes": MessageLookupByLibrary.simpleMessage("Jenis file"), "fileTypesAndNames": MessageLookupByLibrary.simpleMessage("Nama dan jenis file"), - "filesBackedUpFromDevice": m35, - "filesBackedUpInAlbum": m36, + "filesBackedUpFromDevice": m37, + "filesBackedUpInAlbum": m38, "filesDeleted": MessageLookupByLibrary.simpleMessage("File terhapus"), "filesSavedToGallery": MessageLookupByLibrary.simpleMessage("File tersimpan ke galeri"), @@ -719,19 +719,19 @@ class MessageLookup extends MessageLookupByLibrary { "freeStorageUsable": MessageLookupByLibrary.simpleMessage( "Kuota gratis yang dapat digunakan"), "freeTrial": MessageLookupByLibrary.simpleMessage("Percobaan gratis"), - "freeTrialValidTill": m37, - "freeUpAccessPostDelete": m38, - "freeUpAmount": m39, + "freeTrialValidTill": m39, + "freeUpAccessPostDelete": m40, + "freeUpAmount": m41, "freeUpDeviceSpace": MessageLookupByLibrary.simpleMessage( "Bersihkan penyimpanan perangkat"), "freeUpDeviceSpaceDesc": MessageLookupByLibrary.simpleMessage( "Hemat ruang penyimpanan di perangkatmu dengan membersihkan file yang sudah tercadangkan."), "freeUpSpace": MessageLookupByLibrary.simpleMessage("Bersihkan ruang"), - "freeUpSpaceSaving": m40, + "freeUpSpaceSaving": m42, "general": MessageLookupByLibrary.simpleMessage("Umum"), "generatingEncryptionKeys": MessageLookupByLibrary.simpleMessage( "Menghasilkan kunci enkripsi..."), - "genericProgress": m41, + "genericProgress": m43, "goToSettings": MessageLookupByLibrary.simpleMessage("Buka pengaturan"), "googlePlayId": MessageLookupByLibrary.simpleMessage("ID Google Play"), "grantFullAccessPrompt": MessageLookupByLibrary.simpleMessage( @@ -791,7 +791,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": m44, "itemsWillBeRemovedFromAlbum": MessageLookupByLibrary.simpleMessage( "Item yang dipilih akan dihapus dari album ini"), "joinDiscord": @@ -818,7 +818,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Batas perangkat"), "linkEnabled": MessageLookupByLibrary.simpleMessage("Aktif"), "linkExpired": MessageLookupByLibrary.simpleMessage("Kedaluwarsa"), - "linkExpiresOn": m44, + "linkExpiresOn": m46, "linkExpiry": MessageLookupByLibrary.simpleMessage("Waktu kedaluwarsa link"), "linkHasExpired": @@ -898,10 +898,10 @@ class MessageLookup extends MessageLookupByLibrary { "moderateStrength": MessageLookupByLibrary.simpleMessage("Sedang"), "moments": MessageLookupByLibrary.simpleMessage("Momen"), "monthly": MessageLookupByLibrary.simpleMessage("Bulanan"), - "moveItem": m45, + "moveItem": m49, "moveToHiddenAlbum": MessageLookupByLibrary.simpleMessage( "Pindahkan ke album tersembunyi"), - "movedSuccessfullyTo": m46, + "movedSuccessfullyTo": m50, "movedToTrash": MessageLookupByLibrary.simpleMessage("Pindah ke sampah"), "movingFilesToAlbum": MessageLookupByLibrary.simpleMessage( @@ -952,7 +952,7 @@ class MessageLookup extends MessageLookupByLibrary { "onDevice": MessageLookupByLibrary.simpleMessage("Di perangkat ini"), "onEnte": MessageLookupByLibrary.simpleMessage( "Di ente"), - "onlyFamilyAdminCanChangeCode": m49, + "onlyFamilyAdminCanChangeCode": m53, "oops": MessageLookupByLibrary.simpleMessage("Aduh"), "oopsCouldNotSaveEdits": MessageLookupByLibrary.simpleMessage( "Aduh, tidak dapat menyimpan perubahan"), @@ -966,8 +966,6 @@ class MessageLookup extends MessageLookupByLibrary { "Opsional, pendek pun tak apa..."), "orPickAnExistingOne": MessageLookupByLibrary.simpleMessage("Atau pilih yang sudah ada"), - "orPickFromYourContacts": - MessageLookupByLibrary.simpleMessage("or pick from your contacts"), "pair": MessageLookupByLibrary.simpleMessage("Tautkan"), "pairWithPin": MessageLookupByLibrary.simpleMessage("Tautkan dengan PIN"), @@ -990,7 +988,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Pembayaran gagal"), "paymentFailedMessage": MessageLookupByLibrary.simpleMessage( "Sayangnya, pembayaranmu gagal. Silakan hubungi tim bantuan agar dapat kami bantu!"), - "paymentFailedTalkToProvider": m50, + "paymentFailedTalkToProvider": m54, "pendingItems": MessageLookupByLibrary.simpleMessage("Item menunggu"), "pendingSync": MessageLookupByLibrary.simpleMessage("Sinkronisasi tertunda"), @@ -1013,7 +1011,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": m52, + "playStoreFreeTrialValidTill": m56, "playstoreSubscription": MessageLookupByLibrary.simpleMessage("Langganan PlayStore"), "pleaseCheckYourInternetConnectionAndTryAgain": @@ -1025,12 +1023,12 @@ class MessageLookup extends MessageLookupByLibrary { "pleaseContactSupportIfTheProblemPersists": MessageLookupByLibrary.simpleMessage( "Silakan hubungi tim bantuan jika masalah terus terjadi"), - "pleaseEmailUsAt": m53, + "pleaseEmailUsAt": m57, "pleaseGrantPermissions": MessageLookupByLibrary.simpleMessage("Harap berikan izin"), "pleaseLoginAgain": MessageLookupByLibrary.simpleMessage("Silakan masuk akun lagi"), - "pleaseSendTheLogsTo": m54, + "pleaseSendTheLogsTo": m58, "pleaseTryAgain": MessageLookupByLibrary.simpleMessage("Silakan coba lagi"), "pleaseVerifyTheCodeYouHaveEntered": @@ -1064,7 +1062,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Buat tiket dukungan"), "rateTheApp": MessageLookupByLibrary.simpleMessage("Nilai app ini"), "rateUs": MessageLookupByLibrary.simpleMessage("Beri kami nilai"), - "rateUsOnStore": m56, + "rateUsOnStore": m60, "recover": MessageLookupByLibrary.simpleMessage("Pulihkan"), "recoverAccount": MessageLookupByLibrary.simpleMessage("Pulihkan akun"), "recoverButton": MessageLookupByLibrary.simpleMessage("Pulihkan"), @@ -1092,7 +1090,7 @@ class MessageLookup extends MessageLookupByLibrary { "1. Berikan kode ini ke teman kamu"), "referralStep2": MessageLookupByLibrary.simpleMessage( "2. Ia perlu daftar ke paket berbayar"), - "referralStep3": m60, + "referralStep3": m65, "referrals": MessageLookupByLibrary.simpleMessage("Referensi"), "referralsAreCurrentlyPaused": MessageLookupByLibrary.simpleMessage("Rujukan sedang dijeda"), @@ -1114,7 +1112,7 @@ class MessageLookup extends MessageLookupByLibrary { "removeLink": MessageLookupByLibrary.simpleMessage("Hapus link"), "removeParticipant": MessageLookupByLibrary.simpleMessage("Hapus peserta"), - "removeParticipantBody": m61, + "removeParticipantBody": m66, "removePersonLabel": MessageLookupByLibrary.simpleMessage("Hapus label orang"), "removePublicLink": @@ -1130,7 +1128,7 @@ class MessageLookup extends MessageLookupByLibrary { "renameFile": MessageLookupByLibrary.simpleMessage("Ubah nama file"), "renewSubscription": MessageLookupByLibrary.simpleMessage("Perpanjang langganan"), - "renewsOn": m62, + "renewsOn": m67, "reportABug": MessageLookupByLibrary.simpleMessage("Laporkan bug"), "reportBug": MessageLookupByLibrary.simpleMessage("Laporkan bug"), "resendEmail": @@ -1181,7 +1179,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Album, nama dan jenis file"), "searchHint5": MessageLookupByLibrary.simpleMessage( "Segera tiba: Penelusuran wajah & ajaib ✨"), - "searchResultCount": m63, + "searchResultCount": m68, "security": MessageLookupByLibrary.simpleMessage("Keamanan"), "selectALocation": MessageLookupByLibrary.simpleMessage("Pilih lokasi"), "selectALocationFirst": MessageLookupByLibrary.simpleMessage( @@ -1207,7 +1205,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage( "Item terpilih akan dihapus dari semua album dan dipindahkan ke sampah."), "selectedPhotos": m6, - "selectedPhotosWithYours": m65, + "selectedPhotosWithYours": m70, "send": MessageLookupByLibrary.simpleMessage("Kirim"), "sendEmail": MessageLookupByLibrary.simpleMessage("Kirim email"), "sendInvite": MessageLookupByLibrary.simpleMessage("Kirim undangan"), @@ -1228,16 +1226,16 @@ class MessageLookup extends MessageLookupByLibrary { "shareAnAlbumNow": MessageLookupByLibrary.simpleMessage("Bagikan album sekarang"), "shareLink": MessageLookupByLibrary.simpleMessage("Bagikan link"), - "shareMyVerificationID": m66, + "shareMyVerificationID": m71, "shareOnlyWithThePeopleYouWant": MessageLookupByLibrary.simpleMessage( "Bagikan hanya dengan orang yang kamu inginkan"), "shareTextConfirmOthersVerificationID": m7, "shareTextRecommendUsingEnte": MessageLookupByLibrary.simpleMessage( "Unduh Ente agar kita bisa berbagi foto dan video kualitas asli dengan mudah\n\nhttps://ente.io"), - "shareTextReferralCode": m67, + "shareTextReferralCode": m72, "shareWithNonenteUsers": MessageLookupByLibrary.simpleMessage( "Bagikan ke pengguna non-Ente"), - "shareWithPeopleSectionTitle": m68, + "shareWithPeopleSectionTitle": m73, "shareYourFirstAlbum": MessageLookupByLibrary.simpleMessage("Bagikan album pertamamu"), "sharedAlbumSectionDescription": MessageLookupByLibrary.simpleMessage( @@ -1250,7 +1248,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": m69, + "sharedWith": m74, "sharedWithMe": MessageLookupByLibrary.simpleMessage("Dibagikan dengan saya"), "sharedWithYou": @@ -1265,11 +1263,11 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Keluar di perangkat lain"), "signUpTerms": MessageLookupByLibrary.simpleMessage( "Saya menyetujui ketentuan layanan dan kebijakan privasi Ente"), - "singleFileDeleteFromDevice": m70, + "singleFileDeleteFromDevice": m75, "singleFileDeleteHighlight": MessageLookupByLibrary.simpleMessage( "Ia akan dihapus dari semua album."), - "singleFileInBothLocalAndRemote": m71, - "singleFileInRemoteOnly": m72, + "singleFileInBothLocalAndRemote": m76, + "singleFileInRemoteOnly": m77, "skip": MessageLookupByLibrary.simpleMessage("Lewati"), "social": MessageLookupByLibrary.simpleMessage("Sosial"), "someItemsAreInBothEnteAndYourDevice": @@ -1314,10 +1312,10 @@ class MessageLookup extends MessageLookupByLibrary { "storageInGB": m1, "storageLimitExceeded": MessageLookupByLibrary.simpleMessage( "Batas penyimpanan terlampaui"), - "storageUsageInfo": m73, + "storageUsageInfo": m78, "strongStrength": MessageLookupByLibrary.simpleMessage("Kuat"), - "subAlreadyLinkedErrMessage": m74, - "subWillBeCancelledOn": m75, + "subAlreadyLinkedErrMessage": m79, + "subWillBeCancelledOn": m80, "subscribe": MessageLookupByLibrary.simpleMessage("Berlangganan"), "subscription": MessageLookupByLibrary.simpleMessage("Langganan"), "success": MessageLookupByLibrary.simpleMessage("Berhasil"), @@ -1371,7 +1369,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Email ini telah digunakan"), "thisImageHasNoExifData": MessageLookupByLibrary.simpleMessage( "Gambar ini tidak memiliki data exif"), - "thisIsPersonVerificationId": m78, + "thisIsPersonVerificationId": m83, "thisIsYourVerificationId": MessageLookupByLibrary.simpleMessage( "Ini adalah ID Verifikasi kamu"), "thisWillLogYouOutOfTheFollowingDevice": @@ -1437,14 +1435,14 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Gunakan kunci pemulihan"), "useSelectedPhoto": MessageLookupByLibrary.simpleMessage("Gunakan foto terpilih"), - "validTill": m84, + "validTill": m89, "verificationFailedPleaseTryAgain": MessageLookupByLibrary.simpleMessage( "Verifikasi gagal, silakan coba lagi"), "verificationId": MessageLookupByLibrary.simpleMessage("ID Verifikasi"), "verify": MessageLookupByLibrary.simpleMessage("Verifikasi"), "verifyEmail": MessageLookupByLibrary.simpleMessage("Verifikasi email"), - "verifyEmailID": m85, + "verifyEmailID": m90, "verifyPasskey": MessageLookupByLibrary.simpleMessage("Verifikasi passkey"), "verifyPassword": @@ -1480,7 +1478,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Selamat datang kembali!"), "whatsNew": MessageLookupByLibrary.simpleMessage("Hal yang baru"), "yearly": MessageLookupByLibrary.simpleMessage("Tahunan"), - "yearsAgo": m87, + "yearsAgo": m92, "yes": MessageLookupByLibrary.simpleMessage("Ya"), "yesCancel": MessageLookupByLibrary.simpleMessage("Ya, batalkan"), "yesConvertToViewer": @@ -1507,7 +1505,7 @@ class MessageLookup extends MessageLookupByLibrary { "Kamu tidak bisa berbagi dengan dirimu sendiri"), "youDontHaveAnyArchivedItems": MessageLookupByLibrary.simpleMessage( "Kamu tidak memiliki item di arsip."), - "youHaveSuccessfullyFreedUp": m88, + "youHaveSuccessfullyFreedUp": m93, "yourAccountHasBeenDeleted": MessageLookupByLibrary.simpleMessage("Akunmu telah dihapus"), "yourMap": MessageLookupByLibrary.simpleMessage("Peta kamu"), @@ -1528,9 +1526,6 @@ class MessageLookup extends MessageLookupByLibrary { "Langgananmu telah berhasil diperbarui"), "yourVerificationCodeHasExpired": MessageLookupByLibrary.simpleMessage( "Kode verifikasi kamu telah kedaluwarsa"), - "youveNoDuplicateFilesThatCanBeCleared": - MessageLookupByLibrary.simpleMessage( - "Kamu tidak memiliki file duplikat yang dapat dihapus"), "zoomOutToSeePhotos": MessageLookupByLibrary.simpleMessage( "Perkecil peta untuk melihat foto lainnya") }; diff --git a/mobile/lib/generated/intl/messages_it.dart b/mobile/lib/generated/intl/messages_it.dart index 60bb38c10d..5ee16f7562 100644 --- a/mobile/lib/generated/intl/messages_it.dart +++ b/mobile/lib/generated/intl/messages_it.dart @@ -20,37 +20,39 @@ typedef String MessageIfAbsent(String messageStr, List args); class MessageLookup extends MessageLookupByLibrary { String get localeName => 'it'; - static String m9(count) => - "${Intl.plural(count, zero: 'Aggiungi collaboratore', one: 'Aggiungi collaboratore', other: 'Aggiungi collaboratori')}"; + static String m9(title) => "${title} (Io)"; static String m10(count) => + "${Intl.plural(count, zero: 'Aggiungi collaboratore', one: 'Aggiungi collaboratore', other: 'Aggiungi collaboratori')}"; + + static String m11(count) => "${Intl.plural(count, one: 'Aggiungi elemento', other: 'Aggiungi elementi')}"; - static String m11(storageAmount, endDate) => + static String m12(storageAmount, endDate) => "Il tuo spazio aggiuntivo di ${storageAmount} è valido fino al ${endDate}"; - static String m12(count) => + static String m13(count) => "${Intl.plural(count, zero: 'Aggiungi visualizzatore', one: 'Aggiungi visualizzatore', other: 'Aggiungi visualizzatori')}"; - static String m13(emailOrName) => "Aggiunto da ${emailOrName}"; + static String m14(emailOrName) => "Aggiunto da ${emailOrName}"; - static String m14(albumName) => "Aggiunto con successo su ${albumName}"; + static String m15(albumName) => "Aggiunto con successo su ${albumName}"; - static String m15(count) => + static String m16(count) => "${Intl.plural(count, zero: 'Nessun partecipante', one: '1 Partecipante', other: '${count} Partecipanti')}"; - static String m16(versionValue) => "Versione: ${versionValue}"; + static String m17(versionValue) => "Versione: ${versionValue}"; - static String m17(freeAmount, storageUnit) => + static String m18(freeAmount, storageUnit) => "${freeAmount} ${storageUnit} liberi"; - static String m18(paymentProvider) => + static String m19(paymentProvider) => "Annulla prima il tuo abbonamento esistente da ${paymentProvider}"; static String m3(user) => "${user} non sarà più in grado di aggiungere altre foto a questo album\n\nSarà ancora in grado di rimuovere le foto esistenti aggiunte da lui o lei"; - static String m19(isFamilyMember, storageAmountInGb) => + static String m20(isFamilyMember, storageAmountInGb) => "${Intl.select(isFamilyMember, { 'true': 'Il tuo piano famiglia ha già richiesto ${storageAmountInGb} GB finora', @@ -58,206 +60,210 @@ class MessageLookup extends MessageLookupByLibrary { 'other': 'Hai già richiesto ${storageAmountInGb} GB finora!', })}"; - static String m20(albumName) => "Link collaborativo creato per ${albumName}"; + static String m21(albumName) => "Link collaborativo creato per ${albumName}"; - static String m21(count) => + static String m22(count) => "${Intl.plural(count, zero: 'Aggiunti 0 collaboratori', one: 'Aggiunto 1 collaboratore', other: 'Aggiunti ${count} collaboratori')}"; - static String m22(email, numOfDays) => + static String m23(email, numOfDays) => "Stai per aggiungere ${email} come contatto fidato. Potranno recuperare il tuo account se sei assente per ${numOfDays} giorni."; - static String m23(familyAdminEmail) => + static String m24(familyAdminEmail) => "Contatta ${familyAdminEmail} per gestire il tuo abbonamento"; - static String m24(provider) => + static String m25(provider) => "Scrivi all\'indirizzo support@ente.io per gestire il tuo abbonamento ${provider}."; - static String m25(endpoint) => "Connesso a ${endpoint}"; + static String m26(endpoint) => "Connesso a ${endpoint}"; - static String m26(count) => + static String m27(count) => "${Intl.plural(count, one: 'Elimina ${count} elemento', other: 'Elimina ${count} elementi')}"; - static String m27(currentlyDeleting, totalCount) => + static String m28(currentlyDeleting, totalCount) => "Eliminazione di ${currentlyDeleting} / ${totalCount}"; - static String m28(albumName) => + static String m29(albumName) => "Questo rimuoverà il link pubblico per accedere a \"${albumName}\"."; - static String m29(supportEmail) => + static String m30(supportEmail) => "Per favore invia un\'email a ${supportEmail} dall\'indirizzo email con cui ti sei registrato"; - static String m30(count, storageSaved) => + static String m31(count, storageSaved) => "Hai ripulito ${Intl.plural(count, one: '${count} doppione', other: '${count} doppioni')}, salvando (${storageSaved}!)"; - static String m31(count, formattedSize) => + static String m32(count, formattedSize) => "${count} file, ${formattedSize} l\'uno"; - static String m32(newEmail) => "Email cambiata in ${newEmail}"; + static String m33(newEmail) => "Email cambiata in ${newEmail}"; - static String m33(email) => + static String m35(email) => "${email} non ha un account Ente.\n\nInvia un invito per condividere foto."; - static String m34(text) => "Trovate foto aggiuntive per ${text}"; + static String m36(text) => "Trovate foto aggiuntive per ${text}"; - static String m35(count, formattedNumber) => + static String m37(count, formattedNumber) => "${Intl.plural(count, one: '1 file', other: '${formattedNumber} file')} di quest\'album sono stati salvati in modo sicuro"; - static String m36(count, formattedNumber) => + static String m38(count, formattedNumber) => "${Intl.plural(count, one: '1 file', other: '${formattedNumber} file')} di quest\'album sono stati salvati in modo sicuro"; static String m4(storageAmountInGB) => "${storageAmountInGB} GB ogni volta che qualcuno si iscrive a un piano a pagamento e applica il tuo codice"; - static String m37(endDate) => "La prova gratuita termina il ${endDate}"; + static String m39(endDate) => "La prova gratuita termina il ${endDate}"; - static String m38(count) => + static String m40(count) => "Puoi ancora accedere a ${Intl.plural(count, one: '', other: 'loro')} su ente finché hai un abbonamento attivo"; - static String m39(sizeInMBorGB) => "Libera ${sizeInMBorGB}"; + static String m41(sizeInMBorGB) => "Libera ${sizeInMBorGB}"; - static String m40(count, formattedSize) => + static String m42(count, formattedSize) => "${Intl.plural(count, one: 'Può essere cancellata per liberare ${formattedSize}', other: 'Possono essere cancellati per liberare ${formattedSize}')}"; - static String m41(currentlyProcessing, totalCount) => + static String m43(currentlyProcessing, totalCount) => "Elaborazione ${currentlyProcessing} / ${totalCount}"; - static String m42(count) => + static String m44(count) => "${Intl.plural(count, one: '${count} elemento', other: '${count} elementi')}"; - static String m43(email) => + static String m45(email) => "${email} ti ha invitato a essere un contatto fidato"; - static String m44(expiryTime) => "Il link scadrà il ${expiryTime}"; + static String m46(expiryTime) => "Il link scadrà il ${expiryTime}"; + + static String m47(email) => "Collega persona a ${email}"; static String m5(count, formattedCount) => "${Intl.plural(count, one: '${formattedCount} ricordo', other: '${formattedCount} ricordi')}"; - static String m45(count) => + static String m49(count) => "${Intl.plural(count, one: 'Sposta elemento', other: 'Sposta elementi')}"; - static String m46(albumName) => "Spostato con successo su ${albumName}"; + static String m50(albumName) => "Spostato con successo su ${albumName}"; - static String m47(personName) => "Nessun suggerimento per ${personName}"; + static String m51(personName) => "Nessun suggerimento per ${personName}"; - static String m48(name) => "Non è ${name}?"; + static String m52(name) => "Non è ${name}?"; - static String m49(familyAdminEmail) => + static String m53(familyAdminEmail) => "Per favore contatta ${familyAdminEmail} per cambiare il tuo codice."; static String m0(passwordStrengthValue) => "Sicurezza password: ${passwordStrengthValue}"; - static String m50(providerName) => + static String m54(providerName) => "Si prega di parlare con il supporto di ${providerName} se ti è stato addebitato qualcosa"; - static String m51(count) => + static String m55(count) => "${Intl.plural(count, zero: '0 foto', one: '1 foto', other: '${count} foto')}"; - static String m52(endDate) => + static String m56(endDate) => "Prova gratuita valida fino al ${endDate}.\nIn seguito potrai scegliere un piano a pagamento."; - static String m53(toEmail) => "Per favore invia un\'email a ${toEmail}"; + static String m57(toEmail) => "Per favore invia un\'email a ${toEmail}"; - static String m54(toEmail) => "Invia i log a \n${toEmail}"; + static String m58(toEmail) => "Invia i log a \n${toEmail}"; - static String m55(folderName) => "Elaborando ${folderName}..."; + static String m59(folderName) => "Elaborando ${folderName}..."; - static String m56(storeName) => "Valutaci su ${storeName}"; + static String m60(storeName) => "Valutaci su ${storeName}"; - static String m57(days, email) => + static String m61(name) => "Riassegnato a ${name}"; + + static String m62(days, email) => "Puoi accedere all\'account dopo ${days} giorni. Una notifica verrà inviata a ${email}."; - static String m58(email) => + static String m63(email) => "Ora puoi recuperare l\'account di ${email} impostando una nuova password."; - static String m59(email) => + static String m64(email) => "${email} sta cercando di recuperare il tuo account."; - static String m60(storageInGB) => + static String m65(storageInGB) => "3. Ottenete entrambi ${storageInGB} GB* gratis"; - static String m61(userEmail) => + static String m66(userEmail) => "${userEmail} verrà rimosso da questo album condiviso\n\nQualsiasi foto aggiunta dall\'utente verrà rimossa dall\'album"; - static String m62(endDate) => "Si rinnova il ${endDate}"; + static String m67(endDate) => "Si rinnova il ${endDate}"; - static String m63(count) => + static String m68(count) => "${Intl.plural(count, one: '${count} risultato trovato', other: '${count} risultati trovati')}"; static String m6(count) => "${count} selezionati"; - static String m65(count, yourCount) => + static String m70(count, yourCount) => "${count} selezionato (${yourCount} tuoi)"; - static String m66(verificationID) => + static String m71(verificationID) => "Ecco il mio ID di verifica: ${verificationID} per ente.io."; static String m7(verificationID) => "Hey, puoi confermare che questo è il tuo ID di verifica: ${verificationID} su ente.io"; - static String m67(referralCode, referralStorageInGB) => + static String m72(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 m68(numberOfPeople) => + static String m73(numberOfPeople) => "${Intl.plural(numberOfPeople, zero: 'Condividi con persone specifiche', one: 'Condividi con una persona', other: 'Condividi con ${numberOfPeople} persone')}"; - static String m69(emailIDs) => "Condiviso con ${emailIDs}"; + static String m74(emailIDs) => "Condiviso con ${emailIDs}"; - static String m70(fileType) => + static String m75(fileType) => "Questo ${fileType} verrà eliminato dal tuo dispositivo."; - static String m71(fileType) => + static String m76(fileType) => "Questo ${fileType} è sia su Ente che sul tuo dispositivo."; - static String m72(fileType) => "Questo ${fileType} verrà eliminato da Ente."; + static String m77(fileType) => "Questo ${fileType} verrà eliminato da Ente."; static String m1(storageAmountInGB) => "${storageAmountInGB} GB"; - static String m73( + static String m78( usedAmount, usedStorageUnit, totalAmount, totalStorageUnit) => "${usedAmount} ${usedStorageUnit} di ${totalAmount} ${totalStorageUnit} utilizzati"; - static String m74(id) => + static String m79(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 m75(endDate) => "L\'abbonamento verrà cancellato il ${endDate}"; + static String m80(endDate) => "L\'abbonamento verrà cancellato il ${endDate}"; - static String m76(completed, total) => + static String m81(completed, total) => "${completed}/${total} ricordi conservati"; - static String m77(ignoreReason) => + static String m82(ignoreReason) => "Tocca per caricare, il caricamento è attualmente ignorato a causa di ${ignoreReason}"; static String m8(storageAmountInGB) => "Anche loro riceveranno ${storageAmountInGB} GB"; - static String m78(email) => "Questo è l\'ID di verifica di ${email}"; + static String m83(email) => "Questo è l\'ID di verifica di ${email}"; - static String m79(count) => + static String m84(count) => "${Intl.plural(count, zero: 'Presto', one: '1 giorno', other: '${count} giorni')}"; - static String m80(email) => + static String m85(email) => "Sei stato invitato a essere un contatto Legacy da ${email}."; - static String m82(ignoreReason) => + static String m87(ignoreReason) => "Il caricamento è ignorato a causa di ${ignoreReason}"; - static String m83(count) => "Conservando ${count} ricordi..."; + static String m88(count) => "Conservando ${count} ricordi..."; - static String m84(endDate) => "Valido fino al ${endDate}"; + static String m89(endDate) => "Valido fino al ${endDate}"; - static String m85(email) => "Verifica ${email}"; + static String m90(email) => "Verifica ${email}"; - static String m86(count) => + static String m91(count) => "${Intl.plural(count, zero: 'Aggiunti 0 visualizzatori', one: 'Aggiunto 1 visualizzatore', other: 'Aggiunti ${count} visualizzatori')}"; static String m2(email) => "Abbiamo inviato una mail a ${email}"; - static String m87(count) => + static String m92(count) => "${Intl.plural(count, one: '${count} anno fa', other: '${count} anni fa')}"; - static String m88(storageSaved) => + static String m93(storageSaved) => "Hai liberato con successo ${storageSaved}!"; final messages = _notInlinedMessages(_notInlinedMessages); @@ -270,6 +276,7 @@ class MessageLookup extends MessageLookupByLibrary { "account": MessageLookupByLibrary.simpleMessage("Account"), "accountIsAlreadyConfigured": MessageLookupByLibrary.simpleMessage( "L\'account è già configurato."), + "accountOwnerPersonAppbarTitle": m9, "accountWelcomeBack": MessageLookupByLibrary.simpleMessage("Bentornato!"), "ackPasswordLostWarning": MessageLookupByLibrary.simpleMessage( @@ -282,11 +289,11 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Aggiungi una nuova email"), "addCollaborator": MessageLookupByLibrary.simpleMessage("Aggiungi collaboratore"), - "addCollaborators": m9, + "addCollaborators": m10, "addFiles": MessageLookupByLibrary.simpleMessage("Aggiungi File"), "addFromDevice": MessageLookupByLibrary.simpleMessage("Aggiungi dal dispositivo"), - "addItem": m10, + "addItem": m11, "addLocation": MessageLookupByLibrary.simpleMessage("Aggiungi luogo"), "addLocationButton": MessageLookupByLibrary.simpleMessage("Aggiungi"), "addMore": MessageLookupByLibrary.simpleMessage("Aggiungi altri"), @@ -298,7 +305,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Aggiungi nuova persona"), "addOnPageSubtitle": MessageLookupByLibrary.simpleMessage( "Dettagli dei componenti aggiuntivi"), - "addOnValidTill": m11, + "addOnValidTill": m12, "addOns": MessageLookupByLibrary.simpleMessage("Componenti aggiuntivi"), "addPhotos": MessageLookupByLibrary.simpleMessage("Aggiungi foto"), "addSelected": @@ -312,12 +319,12 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Aggiungi contatto fidato"), "addViewer": MessageLookupByLibrary.simpleMessage("Aggiungi in sola lettura"), - "addViewers": m12, + "addViewers": m13, "addYourPhotosNow": MessageLookupByLibrary.simpleMessage("Aggiungi le tue foto ora"), "addedAs": MessageLookupByLibrary.simpleMessage("Aggiunto come"), - "addedBy": m13, - "addedSuccessfullyTo": m14, + "addedBy": m14, + "addedSuccessfullyTo": m15, "addingToFavorites": MessageLookupByLibrary.simpleMessage("Aggiunto ai preferiti..."), "advanced": MessageLookupByLibrary.simpleMessage("Avanzate"), @@ -329,7 +336,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Dopo una settimana"), "after1Year": MessageLookupByLibrary.simpleMessage("Dopo un anno"), "albumOwner": MessageLookupByLibrary.simpleMessage("Proprietario"), - "albumParticipantsCount": m15, + "albumParticipantsCount": m16, "albumTitle": MessageLookupByLibrary.simpleMessage("Titolo album"), "albumUpdated": MessageLookupByLibrary.simpleMessage("Album aggiornato"), @@ -344,10 +351,14 @@ class MessageLookup extends MessageLookupByLibrary { "Permetti anche alle persone con il link di aggiungere foto all\'album condiviso."), "allowAddingPhotos": MessageLookupByLibrary.simpleMessage( "Consenti l\'aggiunta di foto"), + "allowAppToOpenSharedAlbumLinks": MessageLookupByLibrary.simpleMessage( + "Consenti all\'app di aprire link all\'album condiviso"), "allowDownloads": MessageLookupByLibrary.simpleMessage("Consenti download"), "allowPeopleToAddPhotos": MessageLookupByLibrary.simpleMessage( "Permetti alle persone di aggiungere foto"), + "allowPermBody": MessageLookupByLibrary.simpleMessage( + "Permetti l\'accesso alle tue foto da Impostazioni in modo che Ente possa visualizzare e fare il backup della tua libreria."), "allowPermTitle": MessageLookupByLibrary.simpleMessage( "Consenti l\'accesso alle foto"), "androidBiometricHint": @@ -374,7 +385,7 @@ class MessageLookup extends MessageLookupByLibrary { "appLock": MessageLookupByLibrary.simpleMessage("Blocco app"), "appLockDescriptions": MessageLookupByLibrary.simpleMessage( "Scegli tra la schermata di blocco predefinita del dispositivo e una schermata di blocco personalizzata con PIN o password."), - "appVersion": m16, + "appVersion": m17, "appleId": MessageLookupByLibrary.simpleMessage("Apple ID"), "apply": MessageLookupByLibrary.simpleMessage("Applica"), "applyCodeTitle": @@ -458,11 +469,12 @@ class MessageLookup extends MessageLookupByLibrary { "autoPairDesc": MessageLookupByLibrary.simpleMessage( "L\'associazione automatica funziona solo con i dispositivi che supportano Chromecast."), "available": MessageLookupByLibrary.simpleMessage("Disponibile"), - "availableStorageSpace": m17, + "availableStorageSpace": m18, "backedUpFolders": MessageLookupByLibrary.simpleMessage("Cartelle salvate"), "backup": MessageLookupByLibrary.simpleMessage("Backup"), "backupFailed": MessageLookupByLibrary.simpleMessage("Backup fallito"), + "backupFile": MessageLookupByLibrary.simpleMessage("File di backup"), "backupOverMobileData": MessageLookupByLibrary.simpleMessage("Backup su dati mobili"), "backupSettings": @@ -495,7 +507,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Annulla il recupero"), "cancelAccountRecoveryBody": MessageLookupByLibrary.simpleMessage( "Sei sicuro di voler annullare il recupero?"), - "cancelOtherSubscription": m18, + "cancelOtherSubscription": m19, "cancelSubscription": MessageLookupByLibrary.simpleMessage("Annulla abbonamento"), "cannotAddMorePhotosAfterBecomingViewer": m3, @@ -544,7 +556,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Richiedi spazio gratuito"), "claimMore": MessageLookupByLibrary.simpleMessage("Richiedine di più!"), "claimed": MessageLookupByLibrary.simpleMessage("Riscattato"), - "claimedStorageSoFar": m19, + "claimedStorageSoFar": m20, "cleanUncategorized": MessageLookupByLibrary.simpleMessage("Pulisci Senza Categoria"), "cleanUncategorizedDescription": MessageLookupByLibrary.simpleMessage( @@ -573,12 +585,12 @@ class MessageLookup extends MessageLookupByLibrary { "Crea un link per consentire alle persone di aggiungere e visualizzare foto nel tuo album condiviso senza bisogno di un\'applicazione o di un account Ente. Ottimo per raccogliere foto di un evento."), "collaborativeLink": MessageLookupByLibrary.simpleMessage("Link collaborativo"), - "collaborativeLinkCreatedFor": m20, + "collaborativeLinkCreatedFor": m21, "collaborator": MessageLookupByLibrary.simpleMessage("Collaboratore"), "collaboratorsCanAddPhotosAndVideosToTheSharedAlbum": MessageLookupByLibrary.simpleMessage( "I collaboratori possono aggiungere foto e video all\'album condiviso."), - "collaboratorsSuccessfullyAdded": m21, + "collaboratorsSuccessfullyAdded": m22, "collageLayout": MessageLookupByLibrary.simpleMessage("Disposizione"), "collageSaved": MessageLookupByLibrary.simpleMessage( "Collage salvato nella galleria"), @@ -596,7 +608,7 @@ class MessageLookup extends MessageLookupByLibrary { "Sei sicuro di voler disattivare l\'autenticazione a due fattori?"), "confirmAccountDeletion": MessageLookupByLibrary.simpleMessage( "Conferma eliminazione account"), - "confirmAddingTrustedContact": m22, + "confirmAddingTrustedContact": m23, "confirmDeletePrompt": MessageLookupByLibrary.simpleMessage( "Sì, voglio eliminare definitivamente questo account e i dati associati a esso su tutte le applicazioni."), "confirmPassword": @@ -609,10 +621,10 @@ class MessageLookup extends MessageLookupByLibrary { "Conferma la tua chiave di recupero"), "connectToDevice": MessageLookupByLibrary.simpleMessage("Connetti al dispositivo"), - "contactFamilyAdmin": m23, + "contactFamilyAdmin": m24, "contactSupport": MessageLookupByLibrary.simpleMessage("Contatta il supporto"), - "contactToManageSubscription": m24, + "contactToManageSubscription": m25, "contacts": MessageLookupByLibrary.simpleMessage("Contatti"), "contents": MessageLookupByLibrary.simpleMessage("Contenuti"), "continueLabel": MessageLookupByLibrary.simpleMessage("Continua"), @@ -659,7 +671,7 @@ class MessageLookup extends MessageLookupByLibrary { "currentlyRunning": MessageLookupByLibrary.simpleMessage("attualmente in esecuzione"), "custom": MessageLookupByLibrary.simpleMessage("Personalizza"), - "customEndpoint": m25, + "customEndpoint": m26, "darkTheme": MessageLookupByLibrary.simpleMessage("Scuro"), "dayToday": MessageLookupByLibrary.simpleMessage("Oggi"), "dayYesterday": MessageLookupByLibrary.simpleMessage("Ieri"), @@ -697,11 +709,11 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Elimina dal dispositivo"), "deleteFromEnte": MessageLookupByLibrary.simpleMessage("Elimina da Ente"), - "deleteItemCount": m26, + "deleteItemCount": m27, "deleteLocation": MessageLookupByLibrary.simpleMessage("Elimina posizione"), "deletePhotos": MessageLookupByLibrary.simpleMessage("Elimina foto"), - "deleteProgress": m27, + "deleteProgress": m28, "deleteReason1": MessageLookupByLibrary.simpleMessage( "Manca una caratteristica chiave di cui ho bisogno"), "deleteReason2": MessageLookupByLibrary.simpleMessage( @@ -742,7 +754,7 @@ class MessageLookup extends MessageLookupByLibrary { "I visualizzatori possono scattare screenshot o salvare una copia delle foto utilizzando strumenti esterni"), "disableDownloadWarningTitle": MessageLookupByLibrary.simpleMessage("Nota bene"), - "disableLinkMessage": m28, + "disableLinkMessage": m29, "disableTwofactor": MessageLookupByLibrary.simpleMessage( "Disabilita autenticazione a due fattori"), "disablingTwofactorAuthentication": @@ -778,6 +790,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage( "Vuoi scartare le modifiche che hai fatto?"), "done": MessageLookupByLibrary.simpleMessage("Completato"), + "dontSave": MessageLookupByLibrary.simpleMessage("Non salvare"), "doubleYourStorage": MessageLookupByLibrary.simpleMessage("Raddoppia il tuo spazio"), "download": MessageLookupByLibrary.simpleMessage("Scarica"), @@ -785,9 +798,9 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Scaricamento fallito"), "downloading": MessageLookupByLibrary.simpleMessage("Scaricamento in corso..."), - "dropSupportEmail": m29, - "duplicateFileCountWithStorageSaved": m30, - "duplicateItemsGroup": m31, + "dropSupportEmail": m30, + "duplicateFileCountWithStorageSaved": m31, + "duplicateItemsGroup": m32, "edit": MessageLookupByLibrary.simpleMessage("Modifica"), "editLocation": MessageLookupByLibrary.simpleMessage("Modifica luogo"), "editLocationTagTitle": @@ -799,8 +812,12 @@ class MessageLookup extends MessageLookupByLibrary { "Le modifiche alla posizione saranno visibili solo all\'interno di Ente"), "eligible": MessageLookupByLibrary.simpleMessage("idoneo"), "email": MessageLookupByLibrary.simpleMessage("Email"), - "emailChangedTo": m32, - "emailNoEnteAccount": m33, + "emailAlreadyRegistered": + MessageLookupByLibrary.simpleMessage("Email già registrata."), + "emailChangedTo": m33, + "emailNoEnteAccount": m35, + "emailNotRegistered": + MessageLookupByLibrary.simpleMessage("Email non registrata."), "emailVerificationToggle": MessageLookupByLibrary.simpleMessage("Verifica Email"), "emailYourLogs": MessageLookupByLibrary.simpleMessage( @@ -880,7 +897,7 @@ class MessageLookup extends MessageLookupByLibrary { "exportYourData": MessageLookupByLibrary.simpleMessage("Esporta dati"), "extraPhotosFound": MessageLookupByLibrary.simpleMessage("Trovate foto aggiuntive"), - "extraPhotosFoundFor": m34, + "extraPhotosFoundFor": m36, "faceNotClusteredYet": MessageLookupByLibrary.simpleMessage( "Faccia non ancora raggruppata, per favore torna più tardi"), "faceRecognition": @@ -930,8 +947,8 @@ class MessageLookup extends MessageLookupByLibrary { "fileTypes": MessageLookupByLibrary.simpleMessage("Tipi di file"), "fileTypesAndNames": MessageLookupByLibrary.simpleMessage("Tipi e nomi di file"), - "filesBackedUpFromDevice": m35, - "filesBackedUpInAlbum": m36, + "filesBackedUpFromDevice": m37, + "filesBackedUpInAlbum": m38, "filesDeleted": MessageLookupByLibrary.simpleMessage("File eliminati"), "filesSavedToGallery": MessageLookupByLibrary.simpleMessage("File salvati nella galleria"), @@ -951,22 +968,22 @@ class MessageLookup extends MessageLookupByLibrary { "freeStorageUsable": MessageLookupByLibrary.simpleMessage("Spazio libero utilizzabile"), "freeTrial": MessageLookupByLibrary.simpleMessage("Prova gratuita"), - "freeTrialValidTill": m37, - "freeUpAccessPostDelete": m38, - "freeUpAmount": m39, + "freeTrialValidTill": m39, + "freeUpAccessPostDelete": m40, + "freeUpAmount": m41, "freeUpDeviceSpace": MessageLookupByLibrary.simpleMessage("Libera spazio"), "freeUpDeviceSpaceDesc": MessageLookupByLibrary.simpleMessage( "Risparmia spazio sul tuo dispositivo cancellando i file che sono già stati salvati online."), "freeUpSpace": MessageLookupByLibrary.simpleMessage("Libera spazio"), - "freeUpSpaceSaving": m40, + "freeUpSpaceSaving": m42, "gallery": MessageLookupByLibrary.simpleMessage("Galleria"), "galleryMemoryLimitInfo": MessageLookupByLibrary.simpleMessage( "Fino a 1000 ricordi mostrati nella galleria"), "general": MessageLookupByLibrary.simpleMessage("Generali"), "generatingEncryptionKeys": MessageLookupByLibrary.simpleMessage( "Generazione delle chiavi di crittografia..."), - "genericProgress": m41, + "genericProgress": m43, "goToSettings": MessageLookupByLibrary.simpleMessage("Vai alle impostazioni"), "googlePlayId": MessageLookupByLibrary.simpleMessage("Google Play ID"), @@ -1051,7 +1068,7 @@ 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": m44, "itemsShowTheNumberOfDaysRemainingBeforePermanentDeletion": MessageLookupByLibrary.simpleMessage( "Gli elementi mostrano il numero di giorni rimanenti prima della cancellazione permanente"), @@ -1077,7 +1094,7 @@ class MessageLookup extends MessageLookupByLibrary { "legacy": MessageLookupByLibrary.simpleMessage("Legacy"), "legacyAccounts": MessageLookupByLibrary.simpleMessage("Account Legacy"), - "legacyInvite": m43, + "legacyInvite": m45, "legacyPageDesc": MessageLookupByLibrary.simpleMessage( "Legacy consente ai contatti fidati di accedere al tuo account in tua assenza."), "legacyPageDesc2": MessageLookupByLibrary.simpleMessage( @@ -1088,13 +1105,19 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Link copiato negli appunti"), "linkDeviceLimit": MessageLookupByLibrary.simpleMessage("Limite dei dispositivi"), + "linkEmailToContactBannerCaption": MessageLookupByLibrary.simpleMessage( + "per una condivisione più veloce"), "linkEnabled": MessageLookupByLibrary.simpleMessage("Attivato"), "linkExpired": MessageLookupByLibrary.simpleMessage("Scaduto"), - "linkExpiresOn": m44, + "linkExpiresOn": m46, "linkExpiry": MessageLookupByLibrary.simpleMessage("Scadenza del link"), "linkHasExpired": MessageLookupByLibrary.simpleMessage("Il link è scaduto"), "linkNeverExpires": MessageLookupByLibrary.simpleMessage("Mai"), + "linkPerson": MessageLookupByLibrary.simpleMessage("Collega persona"), + "linkPersonCaption": MessageLookupByLibrary.simpleMessage( + "per una migliore esperienza di condivisione"), + "linkPersonToEmail": m47, "livePhotos": MessageLookupByLibrary.simpleMessage("Live Photo"), "loadMessage1": MessageLookupByLibrary.simpleMessage( "Puoi condividere il tuo abbonamento con la tua famiglia"), @@ -1146,6 +1169,7 @@ class MessageLookup extends MessageLookupByLibrary { "La sessione è scaduta. Si prega di accedere nuovamente."), "loginTerms": MessageLookupByLibrary.simpleMessage( "Cliccando sul pulsante Accedi, accetti i termini di servizio e la politica sulla privacy"), + "loginWithTOTP": MessageLookupByLibrary.simpleMessage("Login con TOTP"), "logout": MessageLookupByLibrary.simpleMessage("Disconnetti"), "logsDialogBody": MessageLookupByLibrary.simpleMessage( "Invia i log per aiutarci a risolvere il tuo problema. Si prega di notare che i nomi dei file saranno inclusi per aiutare a tenere traccia di problemi con file specifici."), @@ -1183,6 +1207,7 @@ class MessageLookup extends MessageLookupByLibrary { "maps": MessageLookupByLibrary.simpleMessage("Mappe"), "mastodon": MessageLookupByLibrary.simpleMessage("Mastodon"), "matrix": MessageLookupByLibrary.simpleMessage("Matrix"), + "me": MessageLookupByLibrary.simpleMessage("Io"), "memoryCount": m5, "merchandise": MessageLookupByLibrary.simpleMessage("Merchandise"), "mergeWithExisting": @@ -1211,12 +1236,12 @@ class MessageLookup extends MessageLookupByLibrary { "moreDetails": MessageLookupByLibrary.simpleMessage("Più dettagli"), "mostRecent": MessageLookupByLibrary.simpleMessage("Più recenti"), "mostRelevant": MessageLookupByLibrary.simpleMessage("Più rilevanti"), - "moveItem": m45, + "moveItem": m49, "moveToAlbum": MessageLookupByLibrary.simpleMessage("Sposta nell\'album"), "moveToHiddenAlbum": MessageLookupByLibrary.simpleMessage("Sposta in album nascosto"), - "movedSuccessfullyTo": m46, + "movedSuccessfullyTo": m50, "movedToTrash": MessageLookupByLibrary.simpleMessage("Spostato nel cestino"), "movingFilesToAlbum": MessageLookupByLibrary.simpleMessage( @@ -1269,10 +1294,10 @@ class MessageLookup extends MessageLookupByLibrary { "noResults": MessageLookupByLibrary.simpleMessage("Nessun risultato"), "noResultsFound": MessageLookupByLibrary.simpleMessage("Nessun risultato trovato"), - "noSuggestionsForPerson": m47, + "noSuggestionsForPerson": m51, "noSystemLockFound": MessageLookupByLibrary.simpleMessage( "Nessun blocco di sistema trovato"), - "notPersonLabel": m48, + "notPersonLabel": m52, "nothingSharedWithYouYet": MessageLookupByLibrary.simpleMessage( "Ancora nulla di condiviso con te"), "nothingToSeeHere": @@ -1282,13 +1307,17 @@ class MessageLookup extends MessageLookupByLibrary { "onDevice": MessageLookupByLibrary.simpleMessage("Sul dispositivo"), "onEnte": MessageLookupByLibrary.simpleMessage( "Su ente"), - "onlyFamilyAdminCanChangeCode": m49, + "onlyFamilyAdminCanChangeCode": m53, "onlyThem": MessageLookupByLibrary.simpleMessage("Solo loro"), "oops": MessageLookupByLibrary.simpleMessage("Oops"), "oopsCouldNotSaveEdits": MessageLookupByLibrary.simpleMessage( "Ops, impossibile salvare le modifiche"), "oopsSomethingWentWrong": MessageLookupByLibrary.simpleMessage( "Oops! Qualcosa è andato storto"), + "openAlbumInBrowser": + MessageLookupByLibrary.simpleMessage("Apri album nel browser"), + "openAlbumInBrowserTitle": MessageLookupByLibrary.simpleMessage( + "Utilizza l\'app web per aggiungere foto a questo album"), "openFile": MessageLookupByLibrary.simpleMessage("Apri file"), "openSettings": MessageLookupByLibrary.simpleMessage("Apri Impostazioni"), @@ -1302,8 +1331,6 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("O unisci con esistente"), "orPickAnExistingOne": MessageLookupByLibrary.simpleMessage( "Oppure scegline una esistente"), - "orPickFromYourContacts": - MessageLookupByLibrary.simpleMessage("or pick from your contacts"), "pair": MessageLookupByLibrary.simpleMessage("Abbina"), "pairWithPin": MessageLookupByLibrary.simpleMessage("Associa con PIN"), "pairingComplete": @@ -1330,7 +1357,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": m50, + "paymentFailedTalkToProvider": m54, "pendingItems": MessageLookupByLibrary.simpleMessage("Elementi in sospeso"), "pendingSync": @@ -1355,14 +1382,14 @@ class MessageLookup extends MessageLookupByLibrary { "photosAddedByYouWillBeRemovedFromTheAlbum": MessageLookupByLibrary.simpleMessage( "Le foto aggiunte da te verranno rimosse dall\'album"), - "photosCount": m51, + "photosCount": m55, "pickCenterPoint": MessageLookupByLibrary.simpleMessage( "Selezionare il punto centrale"), "pinAlbum": MessageLookupByLibrary.simpleMessage("Fissa l\'album"), "pinLock": MessageLookupByLibrary.simpleMessage("Blocco con PIN"), "playOnTv": MessageLookupByLibrary.simpleMessage("Riproduci album sulla TV"), - "playStoreFreeTrialValidTill": m52, + "playStoreFreeTrialValidTill": m56, "playstoreSubscription": MessageLookupByLibrary.simpleMessage("Abbonamento su PlayStore"), "pleaseCheckYourInternetConnectionAndTryAgain": @@ -1374,14 +1401,14 @@ class MessageLookup extends MessageLookupByLibrary { "pleaseContactSupportIfTheProblemPersists": MessageLookupByLibrary.simpleMessage( "Riprova. Se il problema persiste, ti invitiamo a contattare l\'assistenza"), - "pleaseEmailUsAt": m53, + "pleaseEmailUsAt": m57, "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": m54, + "pleaseSendTheLogsTo": m58, "pleaseTryAgain": MessageLookupByLibrary.simpleMessage("Riprova"), "pleaseVerifyTheCodeYouHaveEntered": MessageLookupByLibrary.simpleMessage( @@ -1406,7 +1433,9 @@ class MessageLookup extends MessageLookupByLibrary { "privateSharing": MessageLookupByLibrary.simpleMessage("Condivisioni private"), "proceed": MessageLookupByLibrary.simpleMessage("Prosegui"), - "processingImport": m55, + "processingImport": m59, + "processingVideos": + MessageLookupByLibrary.simpleMessage("Elaborando video"), "publicLinkCreated": MessageLookupByLibrary.simpleMessage("Link pubblico creato"), "publicLinkEnabled": @@ -1417,7 +1446,11 @@ class MessageLookup extends MessageLookupByLibrary { "raiseTicket": MessageLookupByLibrary.simpleMessage("Invia ticket"), "rateTheApp": MessageLookupByLibrary.simpleMessage("Valuta l\'app"), "rateUs": MessageLookupByLibrary.simpleMessage("Lascia una recensione"), - "rateUsOnStore": m56, + "rateUsOnStore": m60, + "reassignMe": MessageLookupByLibrary.simpleMessage("Riassegna \"Io\""), + "reassignedToName": m61, + "reassigningLoading": + MessageLookupByLibrary.simpleMessage("Riassegnando..."), "recover": MessageLookupByLibrary.simpleMessage("Recupera"), "recoverAccount": MessageLookupByLibrary.simpleMessage("Recupera account"), @@ -1426,7 +1459,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Recupera l\'account"), "recoveryInitiated": MessageLookupByLibrary.simpleMessage("Recupero avviato"), - "recoveryInitiatedDesc": m57, + "recoveryInitiatedDesc": m62, "recoveryKey": MessageLookupByLibrary.simpleMessage("Chiave di recupero"), "recoveryKeyCopiedToClipboard": MessageLookupByLibrary.simpleMessage( @@ -1441,12 +1474,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": m58, + "recoveryReady": m63, "recoverySuccessful": MessageLookupByLibrary.simpleMessage("Recupero riuscito!"), "recoveryWarning": MessageLookupByLibrary.simpleMessage( "Un contatto fidato sta tentando di accedere al tuo account"), - "recoveryWarningBody": m59, + "recoveryWarningBody": m64, "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": @@ -1462,7 +1495,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": m60, + "referralStep3": m65, "referrals": MessageLookupByLibrary.simpleMessage("Invita un Amico"), "referralsAreCurrentlyPaused": MessageLookupByLibrary.simpleMessage( "I referral code sono attualmente in pausa"), @@ -1491,7 +1524,7 @@ class MessageLookup extends MessageLookupByLibrary { "removeLink": MessageLookupByLibrary.simpleMessage("Elimina link"), "removeParticipant": MessageLookupByLibrary.simpleMessage("Rimuovi partecipante"), - "removeParticipantBody": m61, + "removeParticipantBody": m66, "removePersonLabel": MessageLookupByLibrary.simpleMessage("Rimuovi etichetta persona"), "removePublicLink": @@ -1511,7 +1544,7 @@ class MessageLookup extends MessageLookupByLibrary { "renameFile": MessageLookupByLibrary.simpleMessage("Rinomina file"), "renewSubscription": MessageLookupByLibrary.simpleMessage("Rinnova abbonamento"), - "renewsOn": m62, + "renewsOn": m67, "reportABug": MessageLookupByLibrary.simpleMessage("Segnala un bug"), "reportBug": MessageLookupByLibrary.simpleMessage("Segnala un bug"), "resendEmail": MessageLookupByLibrary.simpleMessage("Rinvia email"), @@ -1588,7 +1621,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": m63, + "searchResultCount": m68, "security": MessageLookupByLibrary.simpleMessage("Sicurezza"), "selectALocation": MessageLookupByLibrary.simpleMessage("Seleziona un luogo"), @@ -1606,6 +1639,8 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Seleziona una lingua"), "selectMorePhotos": MessageLookupByLibrary.simpleMessage("Seleziona più foto"), + "selectPersonToLink": MessageLookupByLibrary.simpleMessage( + "Seleziona persona da collegare"), "selectReason": MessageLookupByLibrary.simpleMessage("Seleziona un motivo"), "selectYourPlan": @@ -1619,7 +1654,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage( "Gli elementi selezionati verranno eliminati da tutti gli album e spostati nel cestino."), "selectedPhotos": m6, - "selectedPhotosWithYours": m65, + "selectedPhotosWithYours": m70, "send": MessageLookupByLibrary.simpleMessage("Invia"), "sendEmail": MessageLookupByLibrary.simpleMessage("Invia email"), "sendInvite": MessageLookupByLibrary.simpleMessage("Invita"), @@ -1651,16 +1686,16 @@ class MessageLookup extends MessageLookupByLibrary { "shareAnAlbumNow": MessageLookupByLibrary.simpleMessage("Condividi un album"), "shareLink": MessageLookupByLibrary.simpleMessage("Condividi link"), - "shareMyVerificationID": m66, + "shareMyVerificationID": m71, "shareOnlyWithThePeopleYouWant": MessageLookupByLibrary.simpleMessage( "Condividi solo con le persone che vuoi"), "shareTextConfirmOthersVerificationID": m7, "shareTextRecommendUsingEnte": MessageLookupByLibrary.simpleMessage( "Scarica Ente in modo da poter facilmente condividere foto e video in qualità originale\n\nhttps://ente.io"), - "shareTextReferralCode": m67, + "shareTextReferralCode": m72, "shareWithNonenteUsers": MessageLookupByLibrary.simpleMessage( "Condividi con utenti che non hanno un account Ente"), - "shareWithPeopleSectionTitle": m68, + "shareWithPeopleSectionTitle": m73, "shareYourFirstAlbum": MessageLookupByLibrary.simpleMessage( "Condividi il tuo primo album"), "sharedAlbumSectionDescription": MessageLookupByLibrary.simpleMessage( @@ -1671,7 +1706,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": m69, + "sharedWith": m74, "sharedWithMe": MessageLookupByLibrary.simpleMessage("Condivisi con me"), "sharedWithYou": @@ -1688,11 +1723,11 @@ class MessageLookup extends MessageLookupByLibrary { "Esci dagli altri dispositivi"), "signUpTerms": MessageLookupByLibrary.simpleMessage( "Accetto i termini di servizio e la politica sulla privacy"), - "singleFileDeleteFromDevice": m70, + "singleFileDeleteFromDevice": m75, "singleFileDeleteHighlight": MessageLookupByLibrary.simpleMessage( "Verrà eliminato da tutti gli album."), - "singleFileInBothLocalAndRemote": m71, - "singleFileInRemoteOnly": m72, + "singleFileInBothLocalAndRemote": m76, + "singleFileInRemoteOnly": m77, "skip": MessageLookupByLibrary.simpleMessage("Salta"), "social": MessageLookupByLibrary.simpleMessage("Social"), "someItemsAreInBothEnteAndYourDevice": @@ -1744,10 +1779,10 @@ class MessageLookup extends MessageLookupByLibrary { "storageInGB": m1, "storageLimitExceeded": MessageLookupByLibrary.simpleMessage( "Limite d\'archiviazione superato"), - "storageUsageInfo": m73, + "storageUsageInfo": m78, "strongStrength": MessageLookupByLibrary.simpleMessage("Forte"), - "subAlreadyLinkedErrMessage": m74, - "subWillBeCancelledOn": m75, + "subAlreadyLinkedErrMessage": m79, + "subWillBeCancelledOn": m80, "subscribe": MessageLookupByLibrary.simpleMessage("Iscriviti"), "subscribeToEnableSharing": MessageLookupByLibrary.simpleMessage( "È necessario un abbonamento a pagamento attivo per abilitare la condivisione."), @@ -1764,7 +1799,7 @@ class MessageLookup extends MessageLookupByLibrary { "suggestFeatures": MessageLookupByLibrary.simpleMessage("Suggerisci una funzionalità"), "support": MessageLookupByLibrary.simpleMessage("Assistenza"), - "syncProgress": m76, + "syncProgress": m81, "syncStopped": MessageLookupByLibrary.simpleMessage("Sincronizzazione interrotta"), "syncing": MessageLookupByLibrary.simpleMessage( @@ -1777,7 +1812,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Tocca per sbloccare"), "tapToUpload": MessageLookupByLibrary.simpleMessage("Premi per caricare"), - "tapToUploadIsIgnoredDue": m77, + "tapToUploadIsIgnoredDue": m82, "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"), @@ -1815,7 +1850,9 @@ class MessageLookup extends MessageLookupByLibrary { "Questo indirizzo email è già registrato"), "thisImageHasNoExifData": MessageLookupByLibrary.simpleMessage( "Questa immagine non ha dati EXIF"), - "thisIsPersonVerificationId": m78, + "thisIsMeExclamation": + MessageLookupByLibrary.simpleMessage("Questo sono io!"), + "thisIsPersonVerificationId": m83, "thisIsYourVerificationId": MessageLookupByLibrary.simpleMessage( "Questo è il tuo ID di verifica"), "thisWillLogYouOutOfTheFollowingDevice": @@ -1839,11 +1876,11 @@ class MessageLookup extends MessageLookupByLibrary { "total": MessageLookupByLibrary.simpleMessage("totale"), "totalSize": MessageLookupByLibrary.simpleMessage("Dimensioni totali"), "trash": MessageLookupByLibrary.simpleMessage("Cestino"), - "trashDaysLeft": m79, + "trashDaysLeft": m84, "trim": MessageLookupByLibrary.simpleMessage("Taglia"), "trustedContacts": MessageLookupByLibrary.simpleMessage("Contatti fidati"), - "trustedInviteBody": m80, + "trustedInviteBody": m85, "tryAgain": MessageLookupByLibrary.simpleMessage("Riprova"), "turnOnBackupForAutoUpload": MessageLookupByLibrary.simpleMessage( "Attiva il backup per caricare automaticamente i file aggiunti a questa cartella del dispositivo su Ente."), @@ -1889,10 +1926,10 @@ class MessageLookup extends MessageLookupByLibrary { "Aggiornamento della selezione delle cartelle..."), "upgrade": MessageLookupByLibrary.simpleMessage("Acquista altro spazio"), - "uploadIsIgnoredDueToIgnorereason": m82, + "uploadIsIgnoredDueToIgnorereason": m87, "uploadingFilesToAlbum": MessageLookupByLibrary.simpleMessage( "Caricamento dei file nell\'album..."), - "uploadingMultipleMemories": m83, + "uploadingMultipleMemories": m88, "uploadingSingleMemory": MessageLookupByLibrary.simpleMessage("Conservando 1 ricordo..."), "upto50OffUntil4thDec": MessageLookupByLibrary.simpleMessage( @@ -1911,7 +1948,7 @@ class MessageLookup extends MessageLookupByLibrary { "useSelectedPhoto": MessageLookupByLibrary.simpleMessage("Usa la foto selezionata"), "usedSpace": MessageLookupByLibrary.simpleMessage("Spazio utilizzato"), - "validTill": m84, + "validTill": m89, "verificationFailedPleaseTryAgain": MessageLookupByLibrary.simpleMessage( "Verifica fallita, per favore prova di nuovo"), @@ -1919,7 +1956,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("ID di verifica"), "verify": MessageLookupByLibrary.simpleMessage("Verifica"), "verifyEmail": MessageLookupByLibrary.simpleMessage("Verifica email"), - "verifyEmailID": m85, + "verifyEmailID": m90, "verifyIDLabel": MessageLookupByLibrary.simpleMessage("Verifica"), "verifyPasskey": MessageLookupByLibrary.simpleMessage("Verifica passkey"), @@ -1947,7 +1984,7 @@ class MessageLookup extends MessageLookupByLibrary { "viewRecoveryKey": MessageLookupByLibrary.simpleMessage( "Visualizza chiave di recupero"), "viewer": MessageLookupByLibrary.simpleMessage("Sola lettura"), - "viewersSuccessfullyAdded": m86, + "viewersSuccessfullyAdded": m91, "visitWebToManage": MessageLookupByLibrary.simpleMessage( "Visita web.ente.io per gestire il tuo abbonamento"), "waitingForVerification": @@ -1968,7 +2005,7 @@ class MessageLookup extends MessageLookupByLibrary { "Un contatto fidato può aiutare a recuperare i tuoi dati."), "yearShort": MessageLookupByLibrary.simpleMessage("anno"), "yearly": MessageLookupByLibrary.simpleMessage("Annuale"), - "yearsAgo": m87, + "yearsAgo": m92, "yes": MessageLookupByLibrary.simpleMessage("Si"), "yesCancel": MessageLookupByLibrary.simpleMessage("Sì, cancella"), "yesConvertToViewer": MessageLookupByLibrary.simpleMessage( @@ -2000,7 +2037,7 @@ class MessageLookup extends MessageLookupByLibrary { "Non puoi condividere con te stesso"), "youDontHaveAnyArchivedItems": MessageLookupByLibrary.simpleMessage( "Non hai nulla di archiviato."), - "youHaveSuccessfullyFreedUp": m88, + "youHaveSuccessfullyFreedUp": m93, "yourAccountHasBeenDeleted": MessageLookupByLibrary.simpleMessage( "Il tuo account è stato eliminato"), "yourMap": MessageLookupByLibrary.simpleMessage("La tua mappa"), @@ -2021,9 +2058,6 @@ class MessageLookup extends MessageLookupByLibrary { "Il tuo abbonamento è stato modificato correttamente"), "yourVerificationCodeHasExpired": MessageLookupByLibrary.simpleMessage( "Il tuo codice di verifica è scaduto"), - "youveNoDuplicateFilesThatCanBeCleared": - MessageLookupByLibrary.simpleMessage( - "Non hai file duplicati che possono essere cancellati"), "youveNoFilesInThisAlbumThatCanBeDeleted": MessageLookupByLibrary.simpleMessage( "Non hai file in questo album che possono essere eliminati"), diff --git a/mobile/lib/generated/intl/messages_ja.dart b/mobile/lib/generated/intl/messages_ja.dart index a1dd9a319e..8513ed1d20 100644 --- a/mobile/lib/generated/intl/messages_ja.dart +++ b/mobile/lib/generated/intl/messages_ja.dart @@ -20,201 +20,254 @@ typedef String MessageIfAbsent(String messageStr, List args); class MessageLookup extends MessageLookupByLibrary { String get localeName => 'ja'; - static String m9(count) => - "${Intl.plural(count, zero: '共同編集者を追加', one: '共同編集者を追加', other: '共同編集者を追加')}"; + static String m9(title) => "${title} (私)"; static String m10(count) => + "${Intl.plural(count, zero: '共同編集者を追加', one: '共同編集者を追加', other: '共同編集者を追加')}"; + + static String m11(count) => "${Intl.plural(count, one: '項目を追加', other: '項目を追加')}"; - static String m11(storageAmount, endDate) => + static String m12(storageAmount, endDate) => "あなたの ${storageAmount} アドオンは ${endDate} まで有効です"; - static String m12(count) => + static String m13(count) => "${Intl.plural(count, zero: 'ビューアーを追加', one: 'ビューアーを追加', other: 'ビューアーを追加')}"; - static String m13(emailOrName) => "${emailOrName} が追加"; + static String m14(emailOrName) => "${emailOrName} が追加"; - static String m14(albumName) => "${albumName} に追加しました"; + static String m15(albumName) => "${albumName} に追加しました"; - static String m15(count) => + static String m16(count) => "${Intl.plural(count, zero: '参加者なし', one: '1 参加者', other: '${count} 参加者')}"; - static String m16(versionValue) => "バージョン: ${versionValue}"; + static String m17(versionValue) => "バージョン: ${versionValue}"; - static String m17(freeAmount, storageUnit) => + static String m18(freeAmount, storageUnit) => "${freeAmount} ${storageUnit} 無料"; - static String m18(paymentProvider) => + static String m19(paymentProvider) => "まず${paymentProvider} から既存のサブスクリプションをキャンセルしてください"; static String m3(user) => "${user} は写真をアルバムに追加できなくなります\n\n※${user} が追加した写真は今後も${user} が削除できます"; - static String m19(isFamilyMember, storageAmountInGb) => + static String m20(isFamilyMember, storageAmountInGb) => "${Intl.select(isFamilyMember, { 'true': '家族は ${storageAmountInGb} GB 受け取っています', 'false': 'あなたは ${storageAmountInGb} GB 受け取っています', 'other': 'あなたは ${storageAmountInGb} GB受け取っています', })}"; - static String m20(albumName) => "${albumName} のコラボレーションリンクを生成しました"; + static String m21(albumName) => "${albumName} のコラボレーションリンクを生成しました"; - static String m23(familyAdminEmail) => + static String m22(count) => + "${Intl.plural(count, zero: '${count}人のコラボレーターを追加', one: '${count}人のコラボレーターを追加', other: '${count}人のコラボレーターを追加')}"; + + static String m23(email, numOfDays) => + "${email} を信頼する連絡先として追加しようとしています。 ${numOfDays} 日間あなたの利用がなくなった場合、アカウントを復旧することができるようになります。"; + + static String m24(familyAdminEmail) => "サブスクリプションを管理するには、 ${familyAdminEmail} に連絡してください"; - static String m24(provider) => + static String m25(provider) => "${provider} サブスクリプションを管理するには、support@ente.io までご連絡ください。"; - static String m25(endpoint) => "${endpoint} に接続しました"; + static String m26(endpoint) => "${endpoint} に接続しました"; - static String m26(count) => + static String m27(count) => "${Intl.plural(count, one: '${count} 個の項目を削除', other: '${count} 個の項目を削除')}"; - static String m27(currentlyDeleting, totalCount) => + static String m28(currentlyDeleting, totalCount) => "${currentlyDeleting} / ${totalCount} を削除中"; - static String m28(albumName) => "\"${albumName}\" にアクセスするための公開リンクが削除されます。"; + static String m29(albumName) => "\"${albumName}\" にアクセスするための公開リンクが削除されます。"; - static String m29(supportEmail) => + static String m30(supportEmail) => "あなたの登録したメールアドレスから${supportEmail} にメールを送ってください"; - static String m30(count, storageSaved) => + static String m31(count, storageSaved) => "お掃除しました ${Intl.plural(count, one: '${count} 個の重複ファイル', other: '${count} 個の重複ファイル')}, (${storageSaved}が開放されます!)"; - static String m31(count, formattedSize) => + static String m32(count, formattedSize) => "${count} 個のファイル、それぞれ${formattedSize}"; - static String m32(newEmail) => "メールアドレスが ${newEmail} に変更されました"; + static String m33(newEmail) => "メールアドレスが ${newEmail} に変更されました"; - static String m33(email) => + static String m34(email) => "${email} は Ente アカウントを持っていません。"; + + static String m35(email) => "${email} はEnteアカウントを持っていません。\n\n写真を共有するために「招待」を送信してください。"; - static String m34(text) => "${text} の写真が見つかりました"; + static String m36(text) => "${text} の写真が見つかりました"; - static String m35(count, formattedNumber) => + static String m37(count, formattedNumber) => "${Intl.plural(count, other: '${formattedNumber} 個のファイル')} が安全にバックアップされました"; - static String m36(count, formattedNumber) => + static String m38(count, formattedNumber) => "${Intl.plural(count, other: '${formattedNumber} ファイル')} が安全にバックアップされました"; static String m4(storageAmountInGB) => "誰かが有料プランにサインアップしてコードを適用する度に ${storageAmountInGB} GB"; - static String m37(endDate) => "無料トライアルは${endDate} までです"; + static String m39(endDate) => "無料トライアルは${endDate} までです"; - static String m38(count) => + static String m40(count) => "あなたが有効なサブスクリプションを持っている限りEnte上の ${Intl.plural(count, other: 'それらに')} アクセスできます"; - static String m39(sizeInMBorGB) => "${sizeInMBorGB} を解放する"; + static String m41(sizeInMBorGB) => "${sizeInMBorGB} を解放する"; - static String m40(count, formattedSize) => + static String m42(count, formattedSize) => "${Intl.plural(count, other: 'デバイスから削除して${formattedSize} 解放することができます')}"; - static String m41(currentlyProcessing, totalCount) => + static String m43(currentlyProcessing, totalCount) => "${currentlyProcessing} / ${totalCount} を処理中"; - static String m42(count) => "${Intl.plural(count, other: '${count}個のアイテム')}"; + static String m44(count) => "${Intl.plural(count, other: '${count}個のアイテム')}"; - static String m44(expiryTime) => "リンクは ${expiryTime} に期限切れになります"; + static String m45(email) => "${email} があなたを信頼する連絡先として招待しました"; + + static String m46(expiryTime) => "リンクは ${expiryTime} に期限切れになります"; + + static String m47(email) => "この人物を ${email}に紐づけ"; + + static String m48(personName, email) => "${personName} を ${email} に紐づけします"; static String m5(count, formattedCount) => "${Intl.plural(count, zero: '思い出なし', one: '${formattedCount} 思い出', other: '${formattedCount} 思い出')}"; - static String m45(count) => + static String m49(count) => "${Intl.plural(count, one: '項目を移動', other: '項目を移動')}"; - static String m46(albumName) => "${albumName} に移動しました"; + static String m50(albumName) => "${albumName} に移動しました"; - static String m48(name) => "${name} ではありませんか?"; + static String m51(personName) => "${personName} の候補はありません"; - static String m49(familyAdminEmail) => + static String m52(name) => "${name} ではありませんか?"; + + static String m53(familyAdminEmail) => "コードを変更するには、 ${familyAdminEmail} までご連絡ください。"; static String m0(passwordStrengthValue) => "パスワードの長さ: ${passwordStrengthValue}"; - static String m50(providerName) => "請求された場合は、 ${providerName} のサポートに連絡してください"; + static String m54(providerName) => "請求された場合は、 ${providerName} のサポートに連絡してください"; - static String m52(endDate) => + static String m55(count) => + "${Intl.plural(count, zero: '0枚の写真', one: '1枚の写真', other: '${count} 枚の写真')}"; + + static String m56(endDate) => "${endDate} まで無料トライアルが有効です。\nその後、有料プランを選択することができます。"; - static String m53(toEmail) => "${toEmail} にメールでご連絡ください"; + static String m57(toEmail) => "${toEmail} にメールでご連絡ください"; - static String m54(toEmail) => "ログを以下のアドレスに送信してください \n${toEmail}"; + static String m58(toEmail) => "ログを以下のアドレスに送信してください \n${toEmail}"; - static String m55(folderName) => "${folderName} を処理中..."; + static String m59(folderName) => "${folderName} を処理中..."; - static String m56(storeName) => "${storeName} で評価"; + static String m60(storeName) => "${storeName} で評価"; - static String m60(storageInGB) => "3. お二人とも ${storageInGB} GB*を無料で手に入ります。"; + static String m61(name) => "あなたを ${name} に紐づけました"; - static String m61(userEmail) => + static String m62(days, email) => + "${days} 日後にアカウントにアクセスできます。通知は ${email}に送信されます。"; + + static String m63(email) => "${email}のアカウントを復元できるようになりました。新しいパスワードを設定してください。"; + + static String m64(email) => "${email} はあなたのアカウントを復元しようとしています。"; + + static String m65(storageInGB) => "3. お二人とも ${storageInGB} GB*を無料で手に入ります。"; + + static String m66(userEmail) => "${userEmail} はこの共有アルバムから退出します\n\n${userEmail} が追加した写真もアルバムから削除されます"; - static String m62(endDate) => "サブスクリプションは ${endDate} に更新します"; + static String m67(endDate) => "サブスクリプションは ${endDate} に更新します"; - static String m63(count) => + static String m68(count) => "${Intl.plural(count, one: '${count} 個の結果', other: '${count} 個の結果')}"; + static String m69(snapshotLength, searchLength) => + "セクションの長さの不一致: ${snapshotLength} != ${searchLength}"; + static String m6(count) => "${count} 個を選択"; - static String m65(count, yourCount) => "${count} 個選択中(${yourCount} あなた)"; + static String m70(count, yourCount) => "${count} 個選択中(${yourCount} あなた)"; - static String m66(verificationID) => "私の確認ID: ente.ioの ${verificationID}"; + static String m71(verificationID) => "私の確認ID: ente.ioの ${verificationID}"; static String m7(verificationID) => "これがあなたのente.io確認用IDであることを確認できますか? ${verificationID}"; - static String m67(referralCode, referralStorageInGB) => + static String m72(referralCode, referralStorageInGB) => "リフェラルコード: ${referralCode}\n\n設定→一般→リフェラルで使うことで${referralStorageInGB}が無料になります(あなたが有料プランに加入したあと)。\n\nhttps://ente.io"; - static String m68(numberOfPeople) => + static String m73(numberOfPeople) => "${Intl.plural(numberOfPeople, zero: '誰かと共有しましょう', one: '1人と共有されています', other: '${numberOfPeople} 人と共有されています')}"; - static String m69(emailIDs) => "${emailIDs} と共有中"; + static String m74(emailIDs) => "${emailIDs} と共有中"; - static String m70(fileType) => "${fileType} はEnteから削除されます。"; + static String m75(fileType) => "${fileType} はEnteから削除されます。"; - static String m71(fileType) => "この ${fileType} はEnteとお使いのデバイスの両方にあります。"; + static String m76(fileType) => "この ${fileType} はEnteとお使いのデバイスの両方にあります。"; - static String m72(fileType) => "${fileType} はEnteから削除されます。"; + static String m77(fileType) => "${fileType} はEnteから削除されます。"; static String m1(storageAmountInGB) => "${storageAmountInGB} GB"; - static String m73( + static String m78( usedAmount, usedStorageUnit, totalAmount, totalStorageUnit) => "${usedAmount} ${usedStorageUnit} / ${totalAmount} ${totalStorageUnit} 使用"; - static String m74(id) => + static String m79(id) => "あなたの ${id} はすでに別のEnteアカウントにリンクされています。\nこのアカウントであなたの ${id} を使用したい場合は、サポートにお問い合わせください。"; - static String m75(endDate) => "サブスクリプションは ${endDate} でキャンセルされます"; + static String m80(endDate) => "サブスクリプションは ${endDate} でキャンセルされます"; - static String m76(completed, total) => "${completed}/${total} のメモリが保存されました"; + static String m81(completed, total) => "${completed}/${total} のメモリが保存されました"; + + static String m82(ignoreReason) => + "アップロードするにはタップしてください。 以下の理由のためアップロードは現在無視されています: ${ignoreReason}"; static String m8(storageAmountInGB) => "紹介者も ${storageAmountInGB} GB を得ます"; - static String m78(email) => "これは ${email} の確認用ID"; + static String m83(email) => "これは ${email} の確認用ID"; - static String m83(count) => "${count} メモリを保存しています..."; + static String m84(count) => + "${Intl.plural(count, zero: '', one: '1日', other: '${count} 日')}"; - static String m84(endDate) => "${endDate} まで"; + static String m85(email) => "あなたは ${email}から信頼する連絡先になってもらうよう、お願いされています。"; - static String m85(email) => "${email} を確認"; + static String m86(galleryType) => + "このギャラリーのタイプ ${galleryType} は名前の変更には対応していません"; + + static String m87(ignoreReason) => "以下の理由によりアップロードは無視されます: ${ignoreReason}"; + + static String m88(count) => "${count} メモリを保存しています..."; + + static String m89(endDate) => "${endDate} まで"; + + static String m90(email) => "${email} を確認"; + + static String m91(count) => + "${Intl.plural(count, zero: '${count}人のビューアーを追加', one: '${count}人のビューアーを追加', other: '${count}人のビューアーを追加')}"; static String m2(email) => "${email}にメールを送りました"; - static String m87(count) => + static String m92(count) => "${Intl.plural(count, one: '${count} 年前', other: '${count} 年前')}"; - static String m88(storageSaved) => "${storageSaved} を解放しました"; + static String m93(storageSaved) => "${storageSaved} を解放しました"; final messages = _notInlinedMessages(_notInlinedMessages); static Map _notInlinedMessages(_) => { "aNewVersionOfEnteIsAvailable": MessageLookupByLibrary.simpleMessage("Enteの新しいバージョンが利用可能です。"), "about": MessageLookupByLibrary.simpleMessage("このアプリについて"), + "acceptTrustInvite": MessageLookupByLibrary.simpleMessage("招待を受け入れる"), "account": MessageLookupByLibrary.simpleMessage("アカウント"), + "accountIsAlreadyConfigured": + MessageLookupByLibrary.simpleMessage("アカウントが既に設定されています"), + "accountOwnerPersonAppbarTitle": m9, "accountWelcomeBack": MessageLookupByLibrary.simpleMessage("おかえりなさい!"), "ackPasswordLostWarning": MessageLookupByLibrary.simpleMessage( "もしパスワードを忘れたら、自身のデータを失うことを理解しました"), @@ -223,9 +276,10 @@ class MessageLookup extends MessageLookupByLibrary { "addAName": MessageLookupByLibrary.simpleMessage("名前を追加"), "addANewEmail": MessageLookupByLibrary.simpleMessage("新しいEメールアドレスを追加"), "addCollaborator": MessageLookupByLibrary.simpleMessage("コラボレーターを追加"), - "addCollaborators": m9, + "addCollaborators": m10, + "addFiles": MessageLookupByLibrary.simpleMessage("ファイルを追加"), "addFromDevice": MessageLookupByLibrary.simpleMessage("デバイスから追加"), - "addItem": m10, + "addItem": m11, "addLocation": MessageLookupByLibrary.simpleMessage("位置情報を追加"), "addLocationButton": MessageLookupByLibrary.simpleMessage("追加"), "addMore": MessageLookupByLibrary.simpleMessage("さらに追加"), @@ -235,19 +289,20 @@ class MessageLookup extends MessageLookupByLibrary { "addNew": MessageLookupByLibrary.simpleMessage("新規追加"), "addNewPerson": MessageLookupByLibrary.simpleMessage("新しい人物を追加"), "addOnPageSubtitle": MessageLookupByLibrary.simpleMessage("アドオンの詳細"), - "addOnValidTill": m11, + "addOnValidTill": m12, "addOns": MessageLookupByLibrary.simpleMessage("アドオン"), "addPhotos": MessageLookupByLibrary.simpleMessage("写真を追加"), "addSelected": MessageLookupByLibrary.simpleMessage("選んだものをアルバムに追加"), "addToAlbum": MessageLookupByLibrary.simpleMessage("アルバムに追加"), "addToEnte": MessageLookupByLibrary.simpleMessage("Enteに追加"), "addToHiddenAlbum": MessageLookupByLibrary.simpleMessage("非表示アルバムに追加"), + "addTrustedContact": MessageLookupByLibrary.simpleMessage("信頼する連絡先を追加"), "addViewer": MessageLookupByLibrary.simpleMessage("ビューアーを追加"), - "addViewers": m12, + "addViewers": m13, "addYourPhotosNow": MessageLookupByLibrary.simpleMessage("写真を今すぐ追加する"), "addedAs": MessageLookupByLibrary.simpleMessage("追加:"), - "addedBy": m13, - "addedSuccessfullyTo": m14, + "addedBy": m14, + "addedSuccessfullyTo": m15, "addingToFavorites": MessageLookupByLibrary.simpleMessage("お気に入りに追加しています..."), "advanced": MessageLookupByLibrary.simpleMessage("詳細"), @@ -258,19 +313,27 @@ class MessageLookup extends MessageLookupByLibrary { "after1Week": MessageLookupByLibrary.simpleMessage("1週間後"), "after1Year": MessageLookupByLibrary.simpleMessage("1年後"), "albumOwner": MessageLookupByLibrary.simpleMessage("所有者"), - "albumParticipantsCount": m15, + "albumParticipantsCount": m16, "albumTitle": MessageLookupByLibrary.simpleMessage("アルバムタイトル"), "albumUpdated": MessageLookupByLibrary.simpleMessage("アルバムが更新されました"), "albums": MessageLookupByLibrary.simpleMessage("アルバム"), "allClear": MessageLookupByLibrary.simpleMessage("✨ オールクリア"), "allMemoriesPreserved": MessageLookupByLibrary.simpleMessage("すべての思い出が保存されました"), + "allPersonGroupingWillReset": MessageLookupByLibrary.simpleMessage( + "この人のグループ化がリセットされ、この人かもしれない写真への提案もなくなります"), + "allow": MessageLookupByLibrary.simpleMessage("許可"), "allowAddPhotosDescription": MessageLookupByLibrary.simpleMessage( "リンクを持つ人が共有アルバムに写真を追加できるようにします。"), "allowAddingPhotos": MessageLookupByLibrary.simpleMessage("写真の追加を許可"), + "allowAppToOpenSharedAlbumLinks": + MessageLookupByLibrary.simpleMessage("共有アルバムリンクを開くことをアプリに許可する"), "allowDownloads": MessageLookupByLibrary.simpleMessage("ダウンロードを許可"), "allowPeopleToAddPhotos": MessageLookupByLibrary.simpleMessage("写真の追加をメンバーに許可する"), + "allowPermBody": MessageLookupByLibrary.simpleMessage( + "Enteがライブラリを表示およびバックアップできるように、端末の設定から写真へのアクセスを許可してください。"), + "allowPermTitle": MessageLookupByLibrary.simpleMessage("写真へのアクセスを許可"), "androidBiometricHint": MessageLookupByLibrary.simpleMessage("本人確認を行う"), "androidBiometricNotRecognized": MessageLookupByLibrary.simpleMessage("認識できません。再試行してください。"), @@ -290,7 +353,7 @@ class MessageLookup extends MessageLookupByLibrary { "appLock": MessageLookupByLibrary.simpleMessage("アプリのロック"), "appLockDescriptions": MessageLookupByLibrary.simpleMessage( "デバイスのデフォルトのロック画面と、カスタムロック画面のどちらを利用しますか?"), - "appVersion": m16, + "appVersion": m17, "appleId": MessageLookupByLibrary.simpleMessage("Apple ID"), "apply": MessageLookupByLibrary.simpleMessage("適用"), "applyCodeTitle": MessageLookupByLibrary.simpleMessage("コードを適用"), @@ -311,6 +374,8 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("本当にログアウトしてよろしいですか?"), "areYouSureYouWantToRenew": MessageLookupByLibrary.simpleMessage("更新してもよろしいですか?"), + "areYouSureYouWantToResetThisPerson": + MessageLookupByLibrary.simpleMessage("この人を忘れてもよろしいですね?"), "askCancelReason": MessageLookupByLibrary.simpleMessage( "サブスクリプションはキャンセルされました。理由を教えていただけますか?"), "askDeleteReason": @@ -330,8 +395,12 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("2段階認証を設定するには認証してください"), "authToInitiateAccountDeletion": MessageLookupByLibrary.simpleMessage("アカウントの削除をするためには認証が必要です"), + "authToManageLegacy": + MessageLookupByLibrary.simpleMessage("信頼する連絡先を管理するために認証してください"), "authToViewPasskey": MessageLookupByLibrary.simpleMessage("パスキーを表示するには認証してください"), + "authToViewTrashedFiles": + MessageLookupByLibrary.simpleMessage("削除したファイルを閲覧するには認証が必要です"), "authToViewYourActiveSessions": MessageLookupByLibrary.simpleMessage("アクティブなセッションを表示するためには認証が必要です"), "authToViewYourHiddenFiles": @@ -358,11 +427,12 @@ class MessageLookup extends MessageLookupByLibrary { "autoPairDesc": MessageLookupByLibrary.simpleMessage( "自動ペアリングは Chromecast に対応しているデバイスでのみ動作します。"), "available": MessageLookupByLibrary.simpleMessage("ご利用可能"), - "availableStorageSpace": m17, + "availableStorageSpace": m18, "backedUpFolders": MessageLookupByLibrary.simpleMessage("バックアップされたフォルダ"), "backup": MessageLookupByLibrary.simpleMessage("バックアップ"), "backupFailed": MessageLookupByLibrary.simpleMessage("バックアップ失敗"), + "backupFile": MessageLookupByLibrary.simpleMessage("バックアップファイル"), "backupOverMobileData": MessageLookupByLibrary.simpleMessage("モバイルデータを使ってバックアップ"), "backupSettings": MessageLookupByLibrary.simpleMessage("バックアップ設定"), @@ -375,6 +445,9 @@ class MessageLookup extends MessageLookupByLibrary { "blog": MessageLookupByLibrary.simpleMessage("ブログ"), "cachedData": MessageLookupByLibrary.simpleMessage("キャッシュデータ"), "calculating": MessageLookupByLibrary.simpleMessage("計算中..."), + "canNotOpenBody": MessageLookupByLibrary.simpleMessage( + "申し訳ありません。このアルバムをアプリで開くことができませんでした。"), + "canNotOpenTitle": MessageLookupByLibrary.simpleMessage("このアルバムは開けません"), "canNotUploadToAlbumsOwnedByOthers": MessageLookupByLibrary.simpleMessage("他の人が作ったアルバムにはアップロードできません"), "canOnlyCreateLinkForFilesOwnedByYou": @@ -382,12 +455,17 @@ class MessageLookup extends MessageLookupByLibrary { "canOnlyRemoveFilesOwnedByYou": MessageLookupByLibrary.simpleMessage("あなたが所有しているファイルのみを削除できます"), "cancel": MessageLookupByLibrary.simpleMessage("キャンセル"), - "cancelOtherSubscription": m18, + "cancelAccountRecovery": + MessageLookupByLibrary.simpleMessage("リカバリをキャンセル"), + "cancelAccountRecoveryBody": + MessageLookupByLibrary.simpleMessage("リカバリをキャンセルしてもよろしいですか?"), + "cancelOtherSubscription": m19, "cancelSubscription": MessageLookupByLibrary.simpleMessage("サブスクリプションをキャンセル"), "cannotAddMorePhotosAfterBecomingViewer": m3, "cannotDeleteSharedFiles": MessageLookupByLibrary.simpleMessage("共有ファイルは削除できません"), + "castAlbum": MessageLookupByLibrary.simpleMessage("アルバムをキャスト"), "castIPMismatchBody": MessageLookupByLibrary.simpleMessage("TVと同じネットワーク上にいることを確認してください。"), "castIPMismatchTitle": @@ -399,6 +477,19 @@ class MessageLookup extends MessageLookupByLibrary { "changeEmail": MessageLookupByLibrary.simpleMessage("Eメールを変更"), "changeLocationOfSelectedItems": MessageLookupByLibrary.simpleMessage("選択したアイテムの位置を変更しますか?"), + "changeLogBackupStatusContent": MessageLookupByLibrary.simpleMessage( + "私たちは、失敗と待機中を含む、Enteにアップロードされたすべてのファイルのログを追加しました。"), + "changeLogBackupStatusTitle": + MessageLookupByLibrary.simpleMessage("バックアップ ステータス"), + "changeLogDiscoverContent": MessageLookupByLibrary.simpleMessage( + "IDカード、メモなどの写真をお探しですか?検索タブに移動し、ディスカバーを確認してください。 \\n\\n機械学習を有効にしている場合にのみ利用できます。"), + "changeLogDiscoverTitle": + MessageLookupByLibrary.simpleMessage("ディスカバー"), + "changeLogMagicSearchImprovementContent": + MessageLookupByLibrary.simpleMessage( + "マジック検索が改善しました。もう、あなたが探しているものを見つけるのを待つ必要はありません。"), + "changeLogMagicSearchImprovementTitle": + MessageLookupByLibrary.simpleMessage("マジック検索の改善"), "changePassword": MessageLookupByLibrary.simpleMessage("パスワードを変更"), "changePasswordTitle": MessageLookupByLibrary.simpleMessage("パスワードを変更"), "changePermissions": MessageLookupByLibrary.simpleMessage("権限を変更する"), @@ -409,11 +500,13 @@ class MessageLookup extends MessageLookupByLibrary { "メールボックスを確認してEメールの所有を証明してください(見つからない場合は、スパムの中も確認してください)"), "checkStatus": MessageLookupByLibrary.simpleMessage("ステータスの確認"), "checking": MessageLookupByLibrary.simpleMessage("確認中…"), + "checkingModels": + MessageLookupByLibrary.simpleMessage("モデルを確認しています..."), "claimFreeStorage": MessageLookupByLibrary.simpleMessage("無料のストレージを受け取る"), "claimMore": MessageLookupByLibrary.simpleMessage("もっと!"), "claimed": MessageLookupByLibrary.simpleMessage("受け取り済"), - "claimedStorageSoFar": m19, + "claimedStorageSoFar": m20, "cleanUncategorized": MessageLookupByLibrary.simpleMessage("未分類のクリーンアップ"), "cleanUncategorizedDescription": MessageLookupByLibrary.simpleMessage( @@ -438,11 +531,12 @@ class MessageLookup extends MessageLookupByLibrary { "collabLinkSectionDescription": MessageLookupByLibrary.simpleMessage( "Enteアプリやアカウントを持っていない人にも、共有アルバムに写真を追加したり表示したりできるリンクを作成します。"), "collaborativeLink": MessageLookupByLibrary.simpleMessage("共同作業リンク"), - "collaborativeLinkCreatedFor": m20, + "collaborativeLinkCreatedFor": m21, "collaborator": MessageLookupByLibrary.simpleMessage("コラボレーター"), "collaboratorsCanAddPhotosAndVideosToTheSharedAlbum": MessageLookupByLibrary.simpleMessage( "コラボレーターは共有アルバムに写真やビデオを追加できます。"), + "collaboratorsSuccessfullyAdded": m22, "collageLayout": MessageLookupByLibrary.simpleMessage("レイアウト"), "collageSaved": MessageLookupByLibrary.simpleMessage("コラージュをギャラリーに保存しました"), @@ -459,6 +553,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("2 要素認証を無効にしてよろしいですか。"), "confirmAccountDeletion": MessageLookupByLibrary.simpleMessage("アカウント削除の確認"), + "confirmAddingTrustedContact": m23, "confirmDeletePrompt": MessageLookupByLibrary.simpleMessage("はい、アカウントとすべてのアプリのデータを削除します"), "confirmPassword": MessageLookupByLibrary.simpleMessage("パスワードを確認"), @@ -468,9 +563,9 @@ class MessageLookup extends MessageLookupByLibrary { "confirmYourRecoveryKey": MessageLookupByLibrary.simpleMessage("リカバリーキーを確認"), "connectToDevice": MessageLookupByLibrary.simpleMessage("デバイスに接続"), - "contactFamilyAdmin": m23, + "contactFamilyAdmin": m24, "contactSupport": MessageLookupByLibrary.simpleMessage("お問い合わせ"), - "contactToManageSubscription": m24, + "contactToManageSubscription": m25, "contacts": MessageLookupByLibrary.simpleMessage("連絡先"), "contents": MessageLookupByLibrary.simpleMessage("内容"), "continueLabel": MessageLookupByLibrary.simpleMessage("つづける"), @@ -505,11 +600,13 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("重要なアップデートがあります"), "crop": MessageLookupByLibrary.simpleMessage("クロップ"), "currentUsageIs": MessageLookupByLibrary.simpleMessage("現在の使用状況 "), + "currentlyRunning": MessageLookupByLibrary.simpleMessage("現在実行中"), "custom": MessageLookupByLibrary.simpleMessage("カスタム"), - "customEndpoint": m25, + "customEndpoint": m26, "darkTheme": MessageLookupByLibrary.simpleMessage("ダーク"), "dayToday": MessageLookupByLibrary.simpleMessage("今日"), "dayYesterday": MessageLookupByLibrary.simpleMessage("昨日"), + "declineTrustInvite": MessageLookupByLibrary.simpleMessage("招待を拒否する"), "decrypting": MessageLookupByLibrary.simpleMessage("復号しています"), "decryptingVideo": MessageLookupByLibrary.simpleMessage("ビデオの復号化中..."), "deduplicateFiles": MessageLookupByLibrary.simpleMessage("重複ファイル"), @@ -535,10 +632,10 @@ class MessageLookup extends MessageLookupByLibrary { "deleteFromBoth": MessageLookupByLibrary.simpleMessage("両方から削除"), "deleteFromDevice": MessageLookupByLibrary.simpleMessage("デバイスから削除"), "deleteFromEnte": MessageLookupByLibrary.simpleMessage("Enteから削除"), - "deleteItemCount": m26, + "deleteItemCount": m27, "deleteLocation": MessageLookupByLibrary.simpleMessage("位置情報を削除"), "deletePhotos": MessageLookupByLibrary.simpleMessage("写真を削除"), - "deleteProgress": m27, + "deleteProgress": m28, "deleteReason1": MessageLookupByLibrary.simpleMessage("いちばん必要な機能がない"), "deleteReason2": MessageLookupByLibrary.simpleMessage("アプリや特定の機能が想定通りに動かない"), @@ -570,22 +667,22 @@ class MessageLookup extends MessageLookupByLibrary { "ビューアーはスクリーンショットを撮ったり、外部ツールを使用して写真のコピーを保存したりすることができます"), "disableDownloadWarningTitle": MessageLookupByLibrary.simpleMessage("ご注意ください"), - "disableLinkMessage": m28, + "disableLinkMessage": m29, "disableTwofactor": MessageLookupByLibrary.simpleMessage("2段階認証を無効にする"), "disablingTwofactorAuthentication": MessageLookupByLibrary.simpleMessage("2要素認証を無効にしています..."), "discord": MessageLookupByLibrary.simpleMessage("Discord"), - "discover": MessageLookupByLibrary.simpleMessage("ディスカバー"), + "discover": MessageLookupByLibrary.simpleMessage("新たな発見"), "discover_babies": MessageLookupByLibrary.simpleMessage("赤ちゃん"), "discover_celebrations": MessageLookupByLibrary.simpleMessage("お祝い"), "discover_food": MessageLookupByLibrary.simpleMessage("食べ物"), "discover_greenery": MessageLookupByLibrary.simpleMessage("自然"), "discover_hills": MessageLookupByLibrary.simpleMessage("丘"), - "discover_identity": MessageLookupByLibrary.simpleMessage("アイデンティティ"), + "discover_identity": MessageLookupByLibrary.simpleMessage("身分証"), "discover_memes": MessageLookupByLibrary.simpleMessage("ミーム"), "discover_notes": MessageLookupByLibrary.simpleMessage("メモ"), "discover_pets": MessageLookupByLibrary.simpleMessage("ペット"), - "discover_receipts": MessageLookupByLibrary.simpleMessage("領収書"), + "discover_receipts": MessageLookupByLibrary.simpleMessage("レシート"), "discover_screenshots": MessageLookupByLibrary.simpleMessage("スクリーンショット"), "discover_selfies": MessageLookupByLibrary.simpleMessage("セルフィー"), @@ -600,14 +697,15 @@ class MessageLookup extends MessageLookupByLibrary { "doYouWantToDiscardTheEditsYouHaveMade": MessageLookupByLibrary.simpleMessage("編集を破棄しますか?"), "done": MessageLookupByLibrary.simpleMessage("完了"), + "dontSave": MessageLookupByLibrary.simpleMessage("保存しない"), "doubleYourStorage": MessageLookupByLibrary.simpleMessage("ストレージを倍にしよう"), "download": MessageLookupByLibrary.simpleMessage("ダウンロード"), "downloadFailed": MessageLookupByLibrary.simpleMessage("ダウンロード失敗"), "downloading": MessageLookupByLibrary.simpleMessage("ダウンロード中…"), - "dropSupportEmail": m29, - "duplicateFileCountWithStorageSaved": m30, - "duplicateItemsGroup": m31, + "dropSupportEmail": m30, + "duplicateFileCountWithStorageSaved": m31, + "duplicateItemsGroup": m32, "edit": MessageLookupByLibrary.simpleMessage("編集"), "editLocation": MessageLookupByLibrary.simpleMessage("位置情報を編集"), "editLocationTagTitle": MessageLookupByLibrary.simpleMessage("位置情報を編集"), @@ -617,16 +715,24 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("位置情報の編集はEnteでのみ表示されます"), "eligible": MessageLookupByLibrary.simpleMessage("対象となる"), "email": MessageLookupByLibrary.simpleMessage("Eメール"), - "emailChangedTo": m32, - "emailNoEnteAccount": m33, + "emailAlreadyRegistered": + MessageLookupByLibrary.simpleMessage("このメールアドレスはすでに登録されています。"), + "emailChangedTo": m33, + "emailDoesNotHaveEnteAccount": m34, + "emailNoEnteAccount": m35, + "emailNotRegistered": + MessageLookupByLibrary.simpleMessage("このメールアドレスはまだ登録されていません。"), "emailVerificationToggle": MessageLookupByLibrary.simpleMessage("メール確認"), "emailYourLogs": MessageLookupByLibrary.simpleMessage("ログをメールで送信"), + "emergencyContacts": MessageLookupByLibrary.simpleMessage("緊急連絡先"), "empty": MessageLookupByLibrary.simpleMessage("空"), "emptyTrash": MessageLookupByLibrary.simpleMessage("ゴミ箱を空にしますか?"), "enable": MessageLookupByLibrary.simpleMessage("有効化"), "enableMLIndexingDesc": MessageLookupByLibrary.simpleMessage( "Enteは顔認識、マジック検索、その他の高度な検索機能のため、あなたのデバイス上で機械学習をしています"), + "enableMachineLearningBanner": + MessageLookupByLibrary.simpleMessage("マジック検索と顔認識のため、機械学習を有効にする"), "enableMaps": MessageLookupByLibrary.simpleMessage("マップを有効にする"), "enableMapsDesc": MessageLookupByLibrary.simpleMessage( "世界地図上にあなたの写真を表示します。\n\n地図はOpenStreetMapを利用しており、あなたの写真の位置情報が外部に共有されることはありません。\n\nこの機能は設定から無効にすることができます"), @@ -685,20 +791,29 @@ class MessageLookup extends MessageLookupByLibrary { "exportYourData": MessageLookupByLibrary.simpleMessage("データをエクスポート"), "extraPhotosFound": MessageLookupByLibrary.simpleMessage("追加の写真が見つかりました"), - "extraPhotosFoundFor": m34, + "extraPhotosFoundFor": m36, + "faceNotClusteredYet": + MessageLookupByLibrary.simpleMessage("顔がまだ集まっていません。後で戻ってきてください"), "faceRecognition": MessageLookupByLibrary.simpleMessage("顔認識"), "faces": MessageLookupByLibrary.simpleMessage("顔"), + "failed": MessageLookupByLibrary.simpleMessage("失敗"), "failedToApplyCode": MessageLookupByLibrary.simpleMessage("コードを適用できませんでした"), "failedToCancel": MessageLookupByLibrary.simpleMessage("キャンセルに失敗しました"), "failedToDownloadVideo": MessageLookupByLibrary.simpleMessage("ビデオをダウンロードできませんでした"), + "failedToFetchActiveSessions": + MessageLookupByLibrary.simpleMessage("アクティブなセッションの取得に失敗しました"), "failedToFetchOriginalForEdit": MessageLookupByLibrary.simpleMessage("編集前の状態の取得に失敗しました"), "failedToFetchReferralDetails": MessageLookupByLibrary.simpleMessage( "紹介の詳細を取得できません。後でもう一度お試しください。"), "failedToLoadAlbums": MessageLookupByLibrary.simpleMessage("アルバムの読み込みに失敗しました"), + "failedToPlayVideo": + MessageLookupByLibrary.simpleMessage("動画の再生に失敗しました"), + "failedToRefreshStripeSubscription": + MessageLookupByLibrary.simpleMessage("サブスクリプションの更新に失敗しました"), "failedToRenew": MessageLookupByLibrary.simpleMessage("更新に失敗しました"), "failedToVerifyPaymentStatus": MessageLookupByLibrary.simpleMessage("支払ステータスの確認に失敗しました"), @@ -710,15 +825,18 @@ class MessageLookup extends MessageLookupByLibrary { "faqs": MessageLookupByLibrary.simpleMessage("よくある質問"), "favorite": MessageLookupByLibrary.simpleMessage("お気に入り"), "feedback": MessageLookupByLibrary.simpleMessage("フィードバック"), + "file": MessageLookupByLibrary.simpleMessage("ファイル"), "fileFailedToSaveToGallery": MessageLookupByLibrary.simpleMessage("ギャラリーへの保存に失敗しました"), "fileInfoAddDescHint": MessageLookupByLibrary.simpleMessage("説明を追加..."), + "fileNotUploadedYet": + MessageLookupByLibrary.simpleMessage("ファイルがまだアップロードされていません"), "fileSavedToGallery": MessageLookupByLibrary.simpleMessage("ファイルをギャラリーに保存しました"), "fileTypes": MessageLookupByLibrary.simpleMessage("ファイルの種類"), "fileTypesAndNames": MessageLookupByLibrary.simpleMessage("ファイルの種類と名前"), - "filesBackedUpFromDevice": m35, - "filesBackedUpInAlbum": m36, + "filesBackedUpFromDevice": m37, + "filesBackedUpInAlbum": m38, "filesDeleted": MessageLookupByLibrary.simpleMessage("削除されたファイル"), "filesSavedToGallery": MessageLookupByLibrary.simpleMessage("ギャラリーに保存されたファイル"), @@ -733,21 +851,22 @@ class MessageLookup extends MessageLookupByLibrary { "freeStorageUsable": MessageLookupByLibrary.simpleMessage("無料のストレージが利用可能です"), "freeTrial": MessageLookupByLibrary.simpleMessage("無料トライアル"), - "freeTrialValidTill": m37, - "freeUpAccessPostDelete": m38, - "freeUpAmount": m39, + "freeTrialValidTill": m39, + "freeUpAccessPostDelete": m40, + "freeUpAmount": m41, "freeUpDeviceSpace": MessageLookupByLibrary.simpleMessage("デバイスの空き領域を解放する"), "freeUpDeviceSpaceDesc": MessageLookupByLibrary.simpleMessage( "すでにバックアップされているファイルを消去して、デバイスの容量を空けます。"), "freeUpSpace": MessageLookupByLibrary.simpleMessage("スペースを解放する"), - "freeUpSpaceSaving": m40, + "freeUpSpaceSaving": m42, + "gallery": MessageLookupByLibrary.simpleMessage("ギャラリー"), "galleryMemoryLimitInfo": MessageLookupByLibrary.simpleMessage("ギャラリーに表示されるメモリは最大1000個までです"), "general": MessageLookupByLibrary.simpleMessage("設定"), "generatingEncryptionKeys": MessageLookupByLibrary.simpleMessage("暗号化鍵を生成しています"), - "genericProgress": m41, + "genericProgress": m43, "goToSettings": MessageLookupByLibrary.simpleMessage("設定に移動"), "googlePlayId": MessageLookupByLibrary.simpleMessage("Google Play ID"), "grantFullAccessPrompt": MessageLookupByLibrary.simpleMessage( @@ -770,6 +889,8 @@ class MessageLookup extends MessageLookupByLibrary { "アプリ画面を非表示にし、スクリーンショットを無効にします"), "hideContentDescriptionIos": MessageLookupByLibrary.simpleMessage("アプリ切り替え時に、アプリの画面を非表示にします"), + "hideSharedItemsFromHomeGallery": + MessageLookupByLibrary.simpleMessage("ホームギャラリーから共有された写真等を非表示"), "hiding": MessageLookupByLibrary.simpleMessage("非表示にしています"), "hostedAtOsmFrance": MessageLookupByLibrary.simpleMessage("OSM Franceでホスト"), @@ -782,8 +903,11 @@ class MessageLookup extends MessageLookupByLibrary { "生体認証が無効化されています。画面をロック・ロック解除して生体認証を有効化してください。"), "iOSOkButton": MessageLookupByLibrary.simpleMessage("OK"), "ignoreUpdate": MessageLookupByLibrary.simpleMessage("無視する"), + "ignored": MessageLookupByLibrary.simpleMessage("無視された"), "ignoredFolderUploadReason": MessageLookupByLibrary.simpleMessage( "このアルバムの一部のファイルは、以前にEnteから削除されたため、あえてアップロード時に無視されます"), + "imageNotAnalyzed": + MessageLookupByLibrary.simpleMessage("画像が分析されていません"), "immediately": MessageLookupByLibrary.simpleMessage("すぐに"), "importing": MessageLookupByLibrary.simpleMessage("インポート中..."), "incorrectCode": MessageLookupByLibrary.simpleMessage("誤ったコード"), @@ -798,6 +922,8 @@ class MessageLookup extends MessageLookupByLibrary { "indexedItems": MessageLookupByLibrary.simpleMessage("処理済みの項目"), "indexingIsPaused": MessageLookupByLibrary.simpleMessage( "インデックス作成は一時停止されています。デバイスの準備ができたら自動的に再開します。"), + "ineligible": MessageLookupByLibrary.simpleMessage("対象外"), + "info": MessageLookupByLibrary.simpleMessage("情報"), "insecureDevice": MessageLookupByLibrary.simpleMessage("安全でないデバイス"), "installManually": MessageLookupByLibrary.simpleMessage("手動でインストール"), "invalidEmailAddress": @@ -816,11 +942,19 @@ class MessageLookup extends MessageLookupByLibrary { "itLooksLikeSomethingWentWrongPleaseRetryAfterSome": MessageLookupByLibrary.simpleMessage( "問題が発生したようです。しばらくしてから再試行してください。エラーが解決しない場合は、サポートチームにお問い合わせください。"), - "itemCount": m42, + "itemCount": m44, "itemsShowTheNumberOfDaysRemainingBeforePermanentDeletion": MessageLookupByLibrary.simpleMessage("完全に削除されるまでの日数が項目に表示されます"), "itemsWillBeRemovedFromAlbum": MessageLookupByLibrary.simpleMessage("選択したアイテムはこのアルバムから削除されます"), + "join": MessageLookupByLibrary.simpleMessage("参加する"), + "joinAlbum": MessageLookupByLibrary.simpleMessage("アルバムに参加"), + "joinAlbumConfirmationDialogBody": MessageLookupByLibrary.simpleMessage( + "アルバムに参加すると、参加者にメールアドレスが公開されます。"), + "joinAlbumSubtext": + MessageLookupByLibrary.simpleMessage("写真を表示したり、追加したりするために"), + "joinAlbumSubtextViewer": + MessageLookupByLibrary.simpleMessage("これを共有アルバムに追加するために"), "joinDiscord": MessageLookupByLibrary.simpleMessage("Discordに参加"), "keepPhotos": MessageLookupByLibrary.simpleMessage("写真を残す"), "kiloMeterUnit": MessageLookupByLibrary.simpleMessage("km"), @@ -834,17 +968,33 @@ class MessageLookup extends MessageLookupByLibrary { "leaveSharedAlbum": MessageLookupByLibrary.simpleMessage("共有アルバムを抜けてよいですか?"), "left": MessageLookupByLibrary.simpleMessage("左"), + "legacy": MessageLookupByLibrary.simpleMessage("レガシー"), + "legacyAccounts": MessageLookupByLibrary.simpleMessage("レガシーアカウント"), + "legacyInvite": m45, + "legacyPageDesc": MessageLookupByLibrary.simpleMessage( + "レガシーでは、信頼できる連絡先が不在時(あなたが亡くなった時など)にアカウントにアクセスできます。"), + "legacyPageDesc2": MessageLookupByLibrary.simpleMessage( + "信頼できる連絡先はアカウントの回復を開始することができます。30日以内にあなたが拒否しない場合は、その信頼する人がパスワードをリセットしてあなたのアカウントにアクセスできるようになります。"), "light": MessageLookupByLibrary.simpleMessage("ライト"), "lightTheme": MessageLookupByLibrary.simpleMessage("ライト"), + "link": MessageLookupByLibrary.simpleMessage("リンク"), "linkCopiedToClipboard": MessageLookupByLibrary.simpleMessage("リンクをクリップボードにコピーしました"), "linkDeviceLimit": MessageLookupByLibrary.simpleMessage("デバイスの制限"), + "linkEmail": MessageLookupByLibrary.simpleMessage("メールアドレスをリンクする"), + "linkEmailToContactBannerCaption": + MessageLookupByLibrary.simpleMessage("共有を高速化するために"), "linkEnabled": MessageLookupByLibrary.simpleMessage("有効"), "linkExpired": MessageLookupByLibrary.simpleMessage("期限切れ"), - "linkExpiresOn": m44, + "linkExpiresOn": m46, "linkExpiry": MessageLookupByLibrary.simpleMessage("リンクの期限切れ"), "linkHasExpired": MessageLookupByLibrary.simpleMessage("リンクは期限切れです"), "linkNeverExpires": MessageLookupByLibrary.simpleMessage("なし"), + "linkPerson": MessageLookupByLibrary.simpleMessage("人を紐づけ"), + "linkPersonCaption": + MessageLookupByLibrary.simpleMessage("良い経験を分かち合うために"), + "linkPersonToEmail": m47, + "linkPersonToEmailConfirmation": m48, "livePhotos": MessageLookupByLibrary.simpleMessage("ライブフォト"), "loadMessage1": MessageLookupByLibrary.simpleMessage("サブスクリプションを家族と共有できます"), @@ -891,6 +1041,7 @@ class MessageLookup extends MessageLookupByLibrary { "セッションの有効期限が切れました。再度ログインしてください。"), "loginTerms": MessageLookupByLibrary.simpleMessage( "「ログイン」をクリックすることで、利用規約プライバシーポリシーに同意します"), + "loginWithTOTP": MessageLookupByLibrary.simpleMessage("TOTPでログイン"), "logout": MessageLookupByLibrary.simpleMessage("ログアウト"), "logsDialogBody": MessageLookupByLibrary.simpleMessage( "これにより、問題のデバッグに役立つログが送信されます。 特定のファイルの問題を追跡するために、ファイル名が含まれることに注意してください。"), @@ -907,6 +1058,10 @@ class MessageLookup extends MessageLookupByLibrary { "magicSearchHint": MessageLookupByLibrary.simpleMessage( "マジック検索では、「花」、「赤い車」、「本人確認書類」などの写真に写っているもので検索できます。"), "manage": MessageLookupByLibrary.simpleMessage("管理"), + "manageDeviceStorage": + MessageLookupByLibrary.simpleMessage("端末のキャッシュを管理"), + "manageDeviceStorageDesc": + MessageLookupByLibrary.simpleMessage("端末上のキャッシュを確認・削除"), "manageFamily": MessageLookupByLibrary.simpleMessage("ファミリーの管理"), "manageLink": MessageLookupByLibrary.simpleMessage("リンクを管理"), "manageParticipants": MessageLookupByLibrary.simpleMessage("管理"), @@ -918,6 +1073,7 @@ class MessageLookup extends MessageLookupByLibrary { "maps": MessageLookupByLibrary.simpleMessage("地図"), "mastodon": MessageLookupByLibrary.simpleMessage("Mastodon"), "matrix": MessageLookupByLibrary.simpleMessage("Matrix"), + "me": MessageLookupByLibrary.simpleMessage("自分"), "memoryCount": m5, "merchandise": MessageLookupByLibrary.simpleMessage("グッズ"), "mergeWithExisting": MessageLookupByLibrary.simpleMessage("既存の人物とまとめる"), @@ -937,15 +1093,16 @@ class MessageLookup extends MessageLookupByLibrary { "moderateStrength": MessageLookupByLibrary.simpleMessage("普通のパスワード"), "modifyYourQueryOrTrySearchingFor": MessageLookupByLibrary.simpleMessage("クエリを変更するか、以下のように検索してみてください"), - "moments": MessageLookupByLibrary.simpleMessage("モーメント"), + "moments": MessageLookupByLibrary.simpleMessage("日々の瞬間"), + "month": MessageLookupByLibrary.simpleMessage("月"), "monthly": MessageLookupByLibrary.simpleMessage("月額"), "moreDetails": MessageLookupByLibrary.simpleMessage("さらに詳細を表示"), "mostRecent": MessageLookupByLibrary.simpleMessage("新しい順"), "mostRelevant": MessageLookupByLibrary.simpleMessage("関連度順"), - "moveItem": m45, + "moveItem": m49, "moveToAlbum": MessageLookupByLibrary.simpleMessage("アルバムに移動"), "moveToHiddenAlbum": MessageLookupByLibrary.simpleMessage("隠しアルバムに移動"), - "movedSuccessfullyTo": m46, + "movedSuccessfullyTo": m50, "movedToTrash": MessageLookupByLibrary.simpleMessage("ごみ箱へ移動"), "movingFilesToAlbum": MessageLookupByLibrary.simpleMessage("アルバムにファイルを移動中"), @@ -957,6 +1114,7 @@ class MessageLookup extends MessageLookupByLibrary { "Enteに接続できませんでした。ネットワーク設定を確認し、エラーが解決しない場合はサポートにお問い合わせください。"), "never": MessageLookupByLibrary.simpleMessage("なし"), "newAlbum": MessageLookupByLibrary.simpleMessage("新しいアルバム"), + "newLocation": MessageLookupByLibrary.simpleMessage("新しいロケーション"), "newPerson": MessageLookupByLibrary.simpleMessage("新しい人物"), "newToEnte": MessageLookupByLibrary.simpleMessage("Enteを初めて使用する"), "newest": MessageLookupByLibrary.simpleMessage("新しい順"), @@ -969,7 +1127,10 @@ class MessageLookup extends MessageLookupByLibrary { "noDeviceThatCanBeDeleted": MessageLookupByLibrary.simpleMessage("削除できるファイルがありません"), "noDuplicates": MessageLookupByLibrary.simpleMessage("✨ 重複なし"), + "noEnteAccountExclamation": + MessageLookupByLibrary.simpleMessage("アカウントがありません!"), "noExifData": MessageLookupByLibrary.simpleMessage("EXIFデータはありません"), + "noFacesFound": MessageLookupByLibrary.simpleMessage("顔が見つかりません"), "noHiddenPhotosOrVideos": MessageLookupByLibrary.simpleMessage("非表示の写真やビデオはありません"), "noImagesWithLocation": @@ -987,9 +1148,10 @@ class MessageLookup extends MessageLookupByLibrary { "noResults": MessageLookupByLibrary.simpleMessage("該当なし"), "noResultsFound": MessageLookupByLibrary.simpleMessage("一致する結果が見つかりませんでした"), + "noSuggestionsForPerson": m51, "noSystemLockFound": MessageLookupByLibrary.simpleMessage("システムロックが見つかりませんでした"), - "notPersonLabel": m48, + "notPersonLabel": m52, "nothingSharedWithYouYet": MessageLookupByLibrary.simpleMessage("あなたに共有されたものはありません"), "nothingToSeeHere": @@ -998,13 +1160,19 @@ class MessageLookup extends MessageLookupByLibrary { "ok": MessageLookupByLibrary.simpleMessage("OK"), "onDevice": MessageLookupByLibrary.simpleMessage("デバイス上"), "onEnte": MessageLookupByLibrary.simpleMessage( - "Enteで保管"), - "onlyFamilyAdminCanChangeCode": m49, + "Enteが保管"), + "onlyFamilyAdminCanChangeCode": m53, + "onlyThem": MessageLookupByLibrary.simpleMessage("それらのみ"), "oops": MessageLookupByLibrary.simpleMessage("Oops"), "oopsCouldNotSaveEdits": MessageLookupByLibrary.simpleMessage("編集を保存できませんでした"), "oopsSomethingWentWrong": MessageLookupByLibrary.simpleMessage("問題が発生しました"), + "openAlbumInBrowser": + MessageLookupByLibrary.simpleMessage("ブラウザでアルバムを開く"), + "openAlbumInBrowserTitle": MessageLookupByLibrary.simpleMessage( + "このアルバムに写真を追加するには、Webアプリを使用してください"), + "openFile": MessageLookupByLibrary.simpleMessage("ファイルを開く"), "openSettings": MessageLookupByLibrary.simpleMessage("設定を開く"), "openTheItem": MessageLookupByLibrary.simpleMessage("• アイテムを開く"), "openstreetmapContributors": @@ -1016,7 +1184,7 @@ class MessageLookup extends MessageLookupByLibrary { "orPickAnExistingOne": MessageLookupByLibrary.simpleMessage("または既存のものを選択"), "orPickFromYourContacts": - MessageLookupByLibrary.simpleMessage("or pick from your contacts"), + MessageLookupByLibrary.simpleMessage("または連絡先から選択"), "pair": MessageLookupByLibrary.simpleMessage("ペアリング"), "pairWithPin": MessageLookupByLibrary.simpleMessage("PINを使ってペアリングする"), "pairingComplete": MessageLookupByLibrary.simpleMessage("ペアリング完了"), @@ -1038,7 +1206,7 @@ class MessageLookup extends MessageLookupByLibrary { "paymentFailed": MessageLookupByLibrary.simpleMessage("支払いに失敗しました"), "paymentFailedMessage": MessageLookupByLibrary.simpleMessage( "残念ながらお支払いに失敗しました。サポートにお問い合わせください。お手伝いします!"), - "paymentFailedTalkToProvider": m50, + "paymentFailedTalkToProvider": m54, "pendingItems": MessageLookupByLibrary.simpleMessage("処理待ちの項目"), "pendingSync": MessageLookupByLibrary.simpleMessage("同期を保留中"), "people": MessageLookupByLibrary.simpleMessage("人物"), @@ -1056,11 +1224,14 @@ class MessageLookup extends MessageLookupByLibrary { "photos": MessageLookupByLibrary.simpleMessage("写真"), "photosAddedByYouWillBeRemovedFromTheAlbum": MessageLookupByLibrary.simpleMessage("あなたの追加した写真はこのアルバムから削除されます"), + "photosCount": m55, "pickCenterPoint": MessageLookupByLibrary.simpleMessage("中心点を選択"), "pinAlbum": MessageLookupByLibrary.simpleMessage("アルバムをピンする"), "pinLock": MessageLookupByLibrary.simpleMessage("PINロック"), "playOnTv": MessageLookupByLibrary.simpleMessage("TVでアルバムを再生"), - "playStoreFreeTrialValidTill": m52, + "playOriginal": MessageLookupByLibrary.simpleMessage("元動画を再生"), + "playStoreFreeTrialValidTill": m56, + "playStream": MessageLookupByLibrary.simpleMessage("再生"), "playstoreSubscription": MessageLookupByLibrary.simpleMessage("PlayStoreサブスクリプション"), "pleaseCheckYourInternetConnectionAndTryAgain": @@ -1070,13 +1241,13 @@ class MessageLookup extends MessageLookupByLibrary { "Support@ente.ioにお問い合わせください、お手伝いいたします。"), "pleaseContactSupportIfTheProblemPersists": MessageLookupByLibrary.simpleMessage("問題が解決しない場合はサポートにお問い合わせください"), - "pleaseEmailUsAt": m53, + "pleaseEmailUsAt": m57, "pleaseGrantPermissions": MessageLookupByLibrary.simpleMessage("権限を付与してください"), "pleaseLoginAgain": MessageLookupByLibrary.simpleMessage("もう一度試してください"), "pleaseSelectQuickLinksToRemove": MessageLookupByLibrary.simpleMessage("削除するクイックリンクを選択してください"), - "pleaseSendTheLogsTo": m54, + "pleaseSendTheLogsTo": m58, "pleaseTryAgain": MessageLookupByLibrary.simpleMessage("もう一度試してください"), "pleaseVerifyTheCodeYouHaveEntered": MessageLookupByLibrary.simpleMessage("入力したコードを確認してください"), @@ -1096,20 +1267,32 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("プライバシーポリシー"), "privateBackups": MessageLookupByLibrary.simpleMessage("プライベートバックアップ"), "privateSharing": MessageLookupByLibrary.simpleMessage("プライベート共有"), - "processingImport": m55, + "proceed": MessageLookupByLibrary.simpleMessage("続行"), + "processed": MessageLookupByLibrary.simpleMessage("処理完了"), + "processing": MessageLookupByLibrary.simpleMessage("処理中"), + "processingImport": m59, + "processingVideos": MessageLookupByLibrary.simpleMessage("動画を処理中"), "publicLinkCreated": MessageLookupByLibrary.simpleMessage("公開リンクが作成されました"), "publicLinkEnabled": MessageLookupByLibrary.simpleMessage("公開リンクを有効にしました"), + "queued": MessageLookupByLibrary.simpleMessage("処理待ち"), "quickLinks": MessageLookupByLibrary.simpleMessage("クイックリンク"), "radius": MessageLookupByLibrary.simpleMessage("半径"), "raiseTicket": MessageLookupByLibrary.simpleMessage("サポートを受ける"), "rateTheApp": MessageLookupByLibrary.simpleMessage("アプリを評価"), "rateUs": MessageLookupByLibrary.simpleMessage("評価して下さい"), - "rateUsOnStore": m56, + "rateUsOnStore": m60, + "reassignMe": MessageLookupByLibrary.simpleMessage("\"自分\" を再割り当て"), + "reassignedToName": m61, + "reassigningLoading": MessageLookupByLibrary.simpleMessage("再割り当て中..."), "recover": MessageLookupByLibrary.simpleMessage("復元"), "recoverAccount": MessageLookupByLibrary.simpleMessage("アカウントを復元"), "recoverButton": MessageLookupByLibrary.simpleMessage("復元"), + "recoveryAccount": MessageLookupByLibrary.simpleMessage("アカウントを復元"), + "recoveryInitiated": + MessageLookupByLibrary.simpleMessage("リカバリが開始されました"), + "recoveryInitiatedDesc": m62, "recoveryKey": MessageLookupByLibrary.simpleMessage("リカバリーキー"), "recoveryKeyCopiedToClipboard": MessageLookupByLibrary.simpleMessage("リカバリーキーはクリップボードにコピーされました"), @@ -1123,8 +1306,12 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("リカバリキーが確認されました"), "recoveryKeyVerifyReason": MessageLookupByLibrary.simpleMessage( "パスワードを忘れた場合、リカバリーキーは写真を復元するための唯一の方法になります。なお、設定 > アカウント でリカバリーキーを確認することができます。\n \n\nここにリカバリーキーを入力して、正しく保存できていることを確認してください。"), + "recoveryReady": m63, "recoverySuccessful": MessageLookupByLibrary.simpleMessage("復元に成功しました!"), + "recoveryWarning": MessageLookupByLibrary.simpleMessage( + "信頼する連絡先の持ち主があなたのアカウントにアクセスしようとしています"), + "recoveryWarningBody": m64, "recreatePasswordBody": MessageLookupByLibrary.simpleMessage( "このデバイスではパスワードを確認する能力が足りません。\n\n恐れ入りますが、リカバリーキーを入力してパスワードを再生成する必要があります。"), "recreatePasswordTitle": @@ -1138,10 +1325,11 @@ class MessageLookup extends MessageLookupByLibrary { "referralStep1": MessageLookupByLibrary.simpleMessage("1. このコードを友達に贈りましょう"), "referralStep2": MessageLookupByLibrary.simpleMessage("2. 友達が有料プランに登録"), - "referralStep3": m60, + "referralStep3": m65, "referrals": MessageLookupByLibrary.simpleMessage("リフェラル"), "referralsAreCurrentlyPaused": MessageLookupByLibrary.simpleMessage("リフェラルは現在一時停止しています"), + "rejectRecovery": MessageLookupByLibrary.simpleMessage("リカバリを拒否する"), "remindToEmptyDeviceTrash": MessageLookupByLibrary.simpleMessage( "また、空き領域を取得するには、「設定」→「ストレージ」から「最近削除した項目」を空にします"), "remindToEmptyEnteTrash": MessageLookupByLibrary.simpleMessage( @@ -1159,9 +1347,10 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("アルバムから削除しますか?"), "removeFromFavorite": MessageLookupByLibrary.simpleMessage("お気に入りリストから外す"), + "removeInvite": MessageLookupByLibrary.simpleMessage("招待を削除"), "removeLink": MessageLookupByLibrary.simpleMessage("リンクを削除"), "removeParticipant": MessageLookupByLibrary.simpleMessage("参加者を削除"), - "removeParticipantBody": m61, + "removeParticipantBody": m66, "removePersonLabel": MessageLookupByLibrary.simpleMessage("人名を削除"), "removePublicLink": MessageLookupByLibrary.simpleMessage("公開リンクを削除"), "removePublicLinks": MessageLookupByLibrary.simpleMessage("公開リンクを削除"), @@ -1169,6 +1358,8 @@ class MessageLookup extends MessageLookupByLibrary { "削除したアイテムのいくつかは他の人によって追加されました。あなたはそれらへのアクセスを失います"), "removeWithQuestionMark": MessageLookupByLibrary.simpleMessage("削除しますか?"), + "removeYourselfAsTrustedContact": + MessageLookupByLibrary.simpleMessage("あなた自身を信頼できる連絡先から削除"), "removingFromFavorites": MessageLookupByLibrary.simpleMessage("お気に入りから削除しています..."), "rename": MessageLookupByLibrary.simpleMessage("名前変更"), @@ -1176,7 +1367,7 @@ class MessageLookup extends MessageLookupByLibrary { "renameFile": MessageLookupByLibrary.simpleMessage("ファイル名を変更"), "renewSubscription": MessageLookupByLibrary.simpleMessage("サブスクリプションの更新"), - "renewsOn": m62, + "renewsOn": m67, "reportABug": MessageLookupByLibrary.simpleMessage("バグを報告"), "reportBug": MessageLookupByLibrary.simpleMessage("バグを報告"), "resendEmail": MessageLookupByLibrary.simpleMessage("メールを再送信"), @@ -1184,6 +1375,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("アップロード時に無視されるファイルをリセット"), "resetPasswordTitle": MessageLookupByLibrary.simpleMessage("パスワードをリセット"), + "resetPerson": MessageLookupByLibrary.simpleMessage("削除"), "resetToDefault": MessageLookupByLibrary.simpleMessage("初期設定にリセット"), "restore": MessageLookupByLibrary.simpleMessage("復元"), "restoreToAlbum": MessageLookupByLibrary.simpleMessage("アルバムに戻す"), @@ -1200,6 +1392,8 @@ class MessageLookup extends MessageLookupByLibrary { "rotateRight": MessageLookupByLibrary.simpleMessage("右に回転"), "safelyStored": MessageLookupByLibrary.simpleMessage("保管されています"), "save": MessageLookupByLibrary.simpleMessage("保存"), + "saveChangesBeforeLeavingQuestion": + MessageLookupByLibrary.simpleMessage("その前に変更を保存しますか?"), "saveCollage": MessageLookupByLibrary.simpleMessage("コラージュを保存"), "saveCopy": MessageLookupByLibrary.simpleMessage("コピーを保存"), "saveKey": MessageLookupByLibrary.simpleMessage("キーを保存"), @@ -1221,6 +1415,8 @@ class MessageLookup extends MessageLookupByLibrary { "写真情報に \"#trip\" のように説明を追加すれば、ここで簡単に見つけることができます"), "searchDatesEmptySection": MessageLookupByLibrary.simpleMessage("日付、月または年で検索"), + "searchDiscoverEmptySection": + MessageLookupByLibrary.simpleMessage("処理と同期が完了すると、画像がここに表示されます"), "searchFaceEmptySection": MessageLookupByLibrary.simpleMessage("学習が完了すると、ここに人が表示されます"), "searchFileTypesAndNamesEmptySection": @@ -1235,20 +1431,30 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("当時の直近で撮影された写真をグループ化"), "searchPeopleEmptySection": MessageLookupByLibrary.simpleMessage("友達を招待すると、共有される写真はここから閲覧できます"), - "searchResultCount": m63, + "searchPersonsEmptySection": + MessageLookupByLibrary.simpleMessage("処理と同期が完了すると、ここに人々が表示されます"), + "searchResultCount": m68, + "searchSectionsLengthMismatch": m69, "security": MessageLookupByLibrary.simpleMessage("セキュリティ"), + "seePublicAlbumLinksInApp": + MessageLookupByLibrary.simpleMessage("アプリ内で公開アルバムのリンクを見る"), "selectALocation": MessageLookupByLibrary.simpleMessage("場所を選択"), "selectALocationFirst": MessageLookupByLibrary.simpleMessage("先に場所を選択してください"), "selectAlbum": MessageLookupByLibrary.simpleMessage("アルバムを選択"), "selectAll": MessageLookupByLibrary.simpleMessage("全て選択"), + "selectAllShort": MessageLookupByLibrary.simpleMessage("すべて"), + "selectCoverPhoto": MessageLookupByLibrary.simpleMessage("カバー写真を選択"), "selectFoldersForBackup": MessageLookupByLibrary.simpleMessage("バックアップするフォルダを選択"), "selectItemsToAdd": MessageLookupByLibrary.simpleMessage("追加するアイテムを選んでください"), "selectLanguage": MessageLookupByLibrary.simpleMessage("言語を選ぶ"), + "selectMailApp": MessageLookupByLibrary.simpleMessage("メールアプリを選択"), "selectMorePhotos": MessageLookupByLibrary.simpleMessage("さらに写真を選択"), + "selectPersonToLink": MessageLookupByLibrary.simpleMessage("リンクする人を選択"), "selectReason": MessageLookupByLibrary.simpleMessage(""), + "selectYourFace": MessageLookupByLibrary.simpleMessage("あなたの顔を選択"), "selectYourPlan": MessageLookupByLibrary.simpleMessage("プランを選びましょう"), "selectedFilesAreNotOnEnte": MessageLookupByLibrary.simpleMessage("選択したファイルはEnte上にありません"), @@ -1258,13 +1464,15 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage( "選択したアイテムはすべてのアルバムから削除され、ゴミ箱に移動されます。"), "selectedPhotos": m6, - "selectedPhotosWithYours": m65, + "selectedPhotosWithYours": m70, "send": MessageLookupByLibrary.simpleMessage("送信"), "sendEmail": MessageLookupByLibrary.simpleMessage("メールを送信する"), "sendInvite": MessageLookupByLibrary.simpleMessage("招待を送る"), "sendLink": MessageLookupByLibrary.simpleMessage("リンクを送信"), "serverEndpoint": MessageLookupByLibrary.simpleMessage("サーバーエンドポイント"), "sessionExpired": MessageLookupByLibrary.simpleMessage("セッション切れ"), + "sessionIdMismatch": + MessageLookupByLibrary.simpleMessage("セッションIDが一致しません"), "setAPassword": MessageLookupByLibrary.simpleMessage("パスワードを設定"), "setAs": MessageLookupByLibrary.simpleMessage("設定:"), "setCover": MessageLookupByLibrary.simpleMessage("カバー画像をセット"), @@ -1280,16 +1488,16 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("アルバムを開いて右上のシェアボタンをタップ"), "shareAnAlbumNow": MessageLookupByLibrary.simpleMessage("アルバムを共有"), "shareLink": MessageLookupByLibrary.simpleMessage("リンクの共有"), - "shareMyVerificationID": m66, + "shareMyVerificationID": m71, "shareOnlyWithThePeopleYouWant": MessageLookupByLibrary.simpleMessage("選んだ人と共有します"), "shareTextConfirmOthersVerificationID": m7, "shareTextRecommendUsingEnte": MessageLookupByLibrary.simpleMessage( "Enteをダウンロードして、写真や動画の共有を簡単に!\n\nhttps://ente.io"), - "shareTextReferralCode": m67, + "shareTextReferralCode": m72, "shareWithNonenteUsers": MessageLookupByLibrary.simpleMessage("Enteを使っていない人に共有"), - "shareWithPeopleSectionTitle": m68, + "shareWithPeopleSectionTitle": m73, "shareYourFirstAlbum": MessageLookupByLibrary.simpleMessage("アルバムの共有をしてみましょう"), "sharedAlbumSectionDescription": MessageLookupByLibrary.simpleMessage( @@ -1300,7 +1508,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("新しい共有写真"), "sharedPhotoNotificationsExplanation": MessageLookupByLibrary.simpleMessage("誰かが写真を共有アルバムに追加した時に通知を受け取る"), - "sharedWith": m69, + "sharedWith": m74, "sharedWithMe": MessageLookupByLibrary.simpleMessage("あなたと共有されたアルバム"), "sharedWithYou": MessageLookupByLibrary.simpleMessage("あなたと共有されています"), "sharing": MessageLookupByLibrary.simpleMessage("共有中..."), @@ -1314,11 +1522,11 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("他のデバイスからサインアウトする"), "signUpTerms": MessageLookupByLibrary.simpleMessage( "利用規約プライバシーポリシーに同意します"), - "singleFileDeleteFromDevice": m70, + "singleFileDeleteFromDevice": m75, "singleFileDeleteHighlight": MessageLookupByLibrary.simpleMessage("全てのアルバムから削除されます。"), - "singleFileInBothLocalAndRemote": m71, - "singleFileInRemoteOnly": m72, + "singleFileInBothLocalAndRemote": m76, + "singleFileInRemoteOnly": m77, "skip": MessageLookupByLibrary.simpleMessage("スキップ"), "social": MessageLookupByLibrary.simpleMessage("SNS"), "someItemsAreInBothEnteAndYourDevice": @@ -1349,6 +1557,8 @@ class MessageLookup extends MessageLookupByLibrary { "sortNewestFirst": MessageLookupByLibrary.simpleMessage("新しい順"), "sortOldestFirst": MessageLookupByLibrary.simpleMessage("古い順"), "sparkleSuccess": MessageLookupByLibrary.simpleMessage("成功✨"), + "startAccountRecoveryTitle": + MessageLookupByLibrary.simpleMessage("リカバリを開始"), "startBackup": MessageLookupByLibrary.simpleMessage("バックアップを開始"), "status": MessageLookupByLibrary.simpleMessage("ステータス"), "stopCastingBody": MessageLookupByLibrary.simpleMessage("キャストを停止しますか?"), @@ -1359,10 +1569,11 @@ class MessageLookup extends MessageLookupByLibrary { "storageInGB": m1, "storageLimitExceeded": MessageLookupByLibrary.simpleMessage("ストレージの上限を超えました"), - "storageUsageInfo": m73, + "storageUsageInfo": m78, + "streamDetails": MessageLookupByLibrary.simpleMessage("動画の詳細"), "strongStrength": MessageLookupByLibrary.simpleMessage("強いパスワード"), - "subAlreadyLinkedErrMessage": m74, - "subWillBeCancelledOn": m75, + "subAlreadyLinkedErrMessage": m79, + "subWillBeCancelledOn": m80, "subscribe": MessageLookupByLibrary.simpleMessage("サブスクライブ"), "subscribeToEnableSharing": MessageLookupByLibrary.simpleMessage( "共有を有効にするには、有料サブスクリプションが必要です。"), @@ -1376,13 +1587,15 @@ class MessageLookup extends MessageLookupByLibrary { "successfullyUnhid": MessageLookupByLibrary.simpleMessage("非表示を解除しました"), "suggestFeatures": MessageLookupByLibrary.simpleMessage("機能を提案"), "support": MessageLookupByLibrary.simpleMessage("サポート"), - "syncProgress": m76, + "syncProgress": m81, "syncStopped": MessageLookupByLibrary.simpleMessage("同期が停止しました"), "syncing": MessageLookupByLibrary.simpleMessage("同期中..."), "systemTheme": MessageLookupByLibrary.simpleMessage("システム"), "tapToCopy": MessageLookupByLibrary.simpleMessage("タップしてコピー"), "tapToEnterCode": MessageLookupByLibrary.simpleMessage("タップしてコードを入力"), "tapToUnlock": MessageLookupByLibrary.simpleMessage("タップして解除"), + "tapToUpload": MessageLookupByLibrary.simpleMessage("タップしてアップロード"), + "tapToUploadIsIgnoredDue": m82, "tempErrorContactSupportIfPersists": MessageLookupByLibrary.simpleMessage( "問題が発生したようです。しばらくしてから再試行してください。エラーが解決しない場合は、サポートチームにお問い合わせください。"), "terminate": MessageLookupByLibrary.simpleMessage("終了させる"), @@ -1394,6 +1607,8 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("ありがとうございます!"), "theDownloadCouldNotBeCompleted": MessageLookupByLibrary.simpleMessage("ダウンロードを完了できませんでした"), + "theLinkYouAreTryingToAccessHasExpired": + MessageLookupByLibrary.simpleMessage("アクセスしようとしているリンクの期限が切れています。"), "theRecoveryKeyYouEnteredIsIncorrect": MessageLookupByLibrary.simpleMessage("入力したリカバリーキーが間違っています"), "theme": MessageLookupByLibrary.simpleMessage("テーマ"), @@ -1415,7 +1630,8 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("このメールアドレスはすでに使用されています。"), "thisImageHasNoExifData": MessageLookupByLibrary.simpleMessage("この画像にEXIFデータはありません"), - "thisIsPersonVerificationId": m78, + "thisIsMeExclamation": MessageLookupByLibrary.simpleMessage("これは私です"), + "thisIsPersonVerificationId": m83, "thisIsYourVerificationId": MessageLookupByLibrary.simpleMessage("これはあなたの認証IDです"), "thisWillLogYouOutOfTheFollowingDevice": @@ -1438,7 +1654,10 @@ class MessageLookup extends MessageLookupByLibrary { "total": MessageLookupByLibrary.simpleMessage("合計"), "totalSize": MessageLookupByLibrary.simpleMessage("合計サイズ"), "trash": MessageLookupByLibrary.simpleMessage("ゴミ箱"), + "trashDaysLeft": m84, "trim": MessageLookupByLibrary.simpleMessage("トリミング"), + "trustedContacts": MessageLookupByLibrary.simpleMessage("信頼する連絡先"), + "trustedInviteBody": m85, "tryAgain": MessageLookupByLibrary.simpleMessage("もう一度試してください"), "turnOnBackupForAutoUpload": MessageLookupByLibrary.simpleMessage( "バックアップをオンにすると、このデバイスフォルダに追加されたファイルは自動的にEnteにアップロードされます。"), @@ -1453,6 +1672,7 @@ class MessageLookup extends MessageLookupByLibrary { "twofactorAuthenticationSuccessfullyReset": MessageLookupByLibrary.simpleMessage("2段階認証をリセットしました"), "twofactorSetup": MessageLookupByLibrary.simpleMessage("2段階認証のセットアップ"), + "typeOfGallerGallerytypeIsNotSupportedForRename": m86, "unarchive": MessageLookupByLibrary.simpleMessage("アーカイブ解除"), "unarchiveAlbum": MessageLookupByLibrary.simpleMessage("アルバムのアーカイブ解除"), "unarchiving": MessageLookupByLibrary.simpleMessage("アーカイブを解除中..."), @@ -1472,9 +1692,10 @@ class MessageLookup extends MessageLookupByLibrary { "updatingFolderSelection": MessageLookupByLibrary.simpleMessage("フォルダの選択を更新しています..."), "upgrade": MessageLookupByLibrary.simpleMessage("アップグレード"), + "uploadIsIgnoredDueToIgnorereason": m87, "uploadingFilesToAlbum": MessageLookupByLibrary.simpleMessage("アルバムにファイルをアップロード中"), - "uploadingMultipleMemories": m83, + "uploadingMultipleMemories": m88, "uploadingSingleMemory": MessageLookupByLibrary.simpleMessage("1メモリを保存しています..."), "upto50OffUntil4thDec": @@ -1482,19 +1703,21 @@ class MessageLookup extends MessageLookupByLibrary { "usableReferralStorageInfo": MessageLookupByLibrary.simpleMessage( "使用可能なストレージは現在のプランによって制限されています。プランをアップグレードすると、あなたが手に入れたストレージが自動的に使用可能になります。"), "useAsCover": MessageLookupByLibrary.simpleMessage("カバー写真として使用"), + "useDifferentPlayerInfo": MessageLookupByLibrary.simpleMessage( + "この動画の再生に問題がありますか?別のプレイヤーを試すには、ここを長押ししてください。"), "usePublicLinksForPeopleNotOnEnte": MessageLookupByLibrary.simpleMessage( "公開リンクを使用する(Enteを利用しない人と共有できます)"), "useRecoveryKey": MessageLookupByLibrary.simpleMessage("リカバリーキーを使用"), "useSelectedPhoto": MessageLookupByLibrary.simpleMessage("選択した写真を使用"), "usedSpace": MessageLookupByLibrary.simpleMessage("使用済み領域"), - "validTill": m84, + "validTill": m89, "verificationFailedPleaseTryAgain": MessageLookupByLibrary.simpleMessage("確認に失敗しました、再試行してください"), "verificationId": MessageLookupByLibrary.simpleMessage("確認用ID"), "verify": MessageLookupByLibrary.simpleMessage("確認"), "verifyEmail": MessageLookupByLibrary.simpleMessage("Eメールの確認"), - "verifyEmailID": m85, + "verifyEmailID": m90, "verifyIDLabel": MessageLookupByLibrary.simpleMessage("確認"), "verifyPasskey": MessageLookupByLibrary.simpleMessage("パスキーを確認"), "verifyPassword": MessageLookupByLibrary.simpleMessage("パスワードの確認"), @@ -1503,6 +1726,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("リカバリキーを確認中..."), "videoInfo": MessageLookupByLibrary.simpleMessage("ビデオ情報"), "videoSmallCase": MessageLookupByLibrary.simpleMessage("ビデオ"), + "videoStreaming": MessageLookupByLibrary.simpleMessage("動画ストリーミング"), "videos": MessageLookupByLibrary.simpleMessage("ビデオ"), "viewActiveSessions": MessageLookupByLibrary.simpleMessage("アクティブなセッションを表示"), @@ -1516,11 +1740,13 @@ class MessageLookup extends MessageLookupByLibrary { "viewLogs": MessageLookupByLibrary.simpleMessage("ログを表示"), "viewRecoveryKey": MessageLookupByLibrary.simpleMessage("リカバリキーを表示"), "viewer": MessageLookupByLibrary.simpleMessage("ビューアー"), + "viewersSuccessfullyAdded": m91, "visitWebToManage": MessageLookupByLibrary.simpleMessage( "サブスクリプションを管理するにはweb.ente.ioをご覧ください"), "waitingForVerification": MessageLookupByLibrary.simpleMessage("確認を待っています..."), "waitingForWifi": MessageLookupByLibrary.simpleMessage("WiFi を待っています"), + "warning": MessageLookupByLibrary.simpleMessage("警告"), "weAreOpenSource": MessageLookupByLibrary.simpleMessage("私たちはオープンソースです!"), "weDontSupportEditingPhotosAndAlbumsThatYouDont": @@ -1530,8 +1756,11 @@ class MessageLookup extends MessageLookupByLibrary { "weakStrength": MessageLookupByLibrary.simpleMessage("弱いパスワード"), "welcomeBack": MessageLookupByLibrary.simpleMessage("おかえりなさい!"), "whatsNew": MessageLookupByLibrary.simpleMessage("最新情報"), + "whyAddTrustContact": + MessageLookupByLibrary.simpleMessage("信頼する連絡先は、データの復旧が必要な際に役立ちます。"), + "yearShort": MessageLookupByLibrary.simpleMessage("年"), "yearly": MessageLookupByLibrary.simpleMessage("年額"), - "yearsAgo": m87, + "yearsAgo": m92, "yes": MessageLookupByLibrary.simpleMessage("はい"), "yesCancel": MessageLookupByLibrary.simpleMessage("キャンセル"), "yesConvertToViewer": @@ -1542,6 +1771,7 @@ class MessageLookup extends MessageLookupByLibrary { "yesLogout": MessageLookupByLibrary.simpleMessage("はい、ログアウトします"), "yesRemove": MessageLookupByLibrary.simpleMessage("削除"), "yesRenew": MessageLookupByLibrary.simpleMessage("はい、更新する"), + "yesResetPerson": MessageLookupByLibrary.simpleMessage("リセット"), "you": MessageLookupByLibrary.simpleMessage("あなた"), "youAreOnAFamilyPlan": MessageLookupByLibrary.simpleMessage("ファミリープランに入会しています!"), @@ -1559,7 +1789,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("自分自身と共有することはできません"), "youDontHaveAnyArchivedItems": MessageLookupByLibrary.simpleMessage("アーカイブした項目はありません"), - "youHaveSuccessfullyFreedUp": m88, + "youHaveSuccessfullyFreedUp": m93, "yourAccountHasBeenDeleted": MessageLookupByLibrary.simpleMessage("アカウントは削除されました"), "yourMap": MessageLookupByLibrary.simpleMessage("あなたの地図"), @@ -1577,8 +1807,6 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("サブスクリプションが更新されました"), "yourVerificationCodeHasExpired": MessageLookupByLibrary.simpleMessage("確認用コードが失効しました"), - "youveNoDuplicateFilesThatCanBeCleared": - MessageLookupByLibrary.simpleMessage("削除できる重複ファイルはありません"), "youveNoFilesInThisAlbumThatCanBeDeleted": MessageLookupByLibrary.simpleMessage("このアルバムには消すファイルがありません"), "zoomOutToSeePhotos": diff --git a/mobile/lib/generated/intl/messages_km.dart b/mobile/lib/generated/intl/messages_km.dart index a63a4f6c2b..22d4231361 100644 --- a/mobile/lib/generated/intl/messages_km.dart +++ b/mobile/lib/generated/intl/messages_km.dart @@ -21,8 +21,5 @@ class MessageLookup extends MessageLookupByLibrary { String get localeName => 'km'; final messages = _notInlinedMessages(_notInlinedMessages); - static Map _notInlinedMessages(_) => { - "orPickFromYourContacts": - MessageLookupByLibrary.simpleMessage("or pick from your contacts") - }; + static Map _notInlinedMessages(_) => {}; } diff --git a/mobile/lib/generated/intl/messages_ko.dart b/mobile/lib/generated/intl/messages_ko.dart index c10e432148..e378d62fd9 100644 --- a/mobile/lib/generated/intl/messages_ko.dart +++ b/mobile/lib/generated/intl/messages_ko.dart @@ -40,8 +40,6 @@ class MessageLookup extends MessageLookupByLibrary { "feedback": MessageLookupByLibrary.simpleMessage("피드백"), "invalidEmailAddress": MessageLookupByLibrary.simpleMessage("잘못된 이메일 주소"), - "orPickFromYourContacts": - MessageLookupByLibrary.simpleMessage("or pick from your contacts"), "verify": MessageLookupByLibrary.simpleMessage("인증"), "yourAccountHasBeenDeleted": MessageLookupByLibrary.simpleMessage("계정이 삭제되었습니다.") diff --git a/mobile/lib/generated/intl/messages_lt.dart b/mobile/lib/generated/intl/messages_lt.dart index abbfcf486e..7194453bc2 100644 --- a/mobile/lib/generated/intl/messages_lt.dart +++ b/mobile/lib/generated/intl/messages_lt.dart @@ -20,195 +20,209 @@ typedef String MessageIfAbsent(String messageStr, List args); class MessageLookup extends MessageLookupByLibrary { String get localeName => 'lt'; - static String m9(count) => - "${Intl.plural(count, one: 'Pridėti bendradarbį', few: 'Pridėti bendradarbius', many: 'Pridėti bendradarbio', other: 'Pridėti bendradarbių')}"; + static String m9(title) => "${title} (Aš)"; static String m10(count) => + "${Intl.plural(count, one: 'Pridėti bendradarbį', few: 'Pridėti bendradarbius', many: 'Pridėti bendradarbio', other: 'Pridėti bendradarbių')}"; + + static String m11(count) => "${Intl.plural(count, one: 'Pridėti elementą', few: 'Pridėti elementus', many: 'Pridėti elemento', other: 'Pridėti elementų')}"; - static String m12(count) => + static String m13(count) => "${Intl.plural(count, one: 'Pridėti žiūrėtoją', few: 'Pridėti žiūrėtojus', many: 'Pridėti žiūrėtojo', other: 'Pridėti žiūrėtojų')}"; - static String m15(count) => + static String m16(count) => "${Intl.plural(count, zero: 'Nėra dalyvių', one: '1 dalyvis', other: '${count} dalyviai')}"; - static String m16(versionValue) => "Versija: ${versionValue}"; + static String m17(versionValue) => "Versija: ${versionValue}"; - static String m18(paymentProvider) => + static String m19(paymentProvider) => "Pirmiausia atsisakykite esamos prenumeratos iš ${paymentProvider}"; static String m3(user) => "${user} negalės pridėti daugiau nuotraukų į šį albumą\n\nJie vis tiek galės pašalinti esamas pridėtas nuotraukas"; - static String m19(isFamilyMember, storageAmountInGb) => + static String m20(isFamilyMember, storageAmountInGb) => "${Intl.select(isFamilyMember, { 'true': 'Jūsų šeima gavo ${storageAmountInGb} GB iki šiol', 'false': 'Jūs gavote ${storageAmountInGb} GB iki šiol', 'other': 'Jūs gavote ${storageAmountInGb} GB iki šiol.', })}"; - static String m21(count) => + static String m22(count) => "${Intl.plural(count, zero: 'Pridėta 0 bendradarbių', one: 'Pridėtas 1 bendradarbis', other: 'Pridėta ${count} bendradarbių')}"; - static String m22(email, numOfDays) => + static String m23(email, numOfDays) => "Ketinate pridėti ${email} kaip patikimą kontaktą. Jie galės atkurti jūsų paskyrą, jei jūsų nebus ${numOfDays} dienų."; - static String m25(endpoint) => "Prijungta prie ${endpoint}"; + static String m25(provider) => + "Susisiekite su mumis adresu support@ente.io, kad sutvarkytumėte savo ${provider} prenumeratą."; - static String m26(count) => + static String m26(endpoint) => "Prijungta prie ${endpoint}"; + + static String m27(count) => "${Intl.plural(count, one: 'Ištrinti ${count} elementą', few: 'Ištrinti ${count} elementus', many: 'Ištrinti ${count} elemento', other: 'Ištrinti ${count} elementų')}"; - static String m27(currentlyDeleting, totalCount) => + static String m28(currentlyDeleting, totalCount) => "Ištrinama ${currentlyDeleting} / ${totalCount}"; - static String m28(albumName) => + static String m29(albumName) => "Tai pašalins viešą nuorodą, skirtą pasiekti „${albumName}“."; - static String m29(supportEmail) => + static String m30(supportEmail) => "Iš savo registruoto el. pašto adreso atsiųskite el. laišką adresu ${supportEmail}"; - static String m30(count, storageSaved) => + static String m31(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})."; - static String m31(count, formattedSize) => + static String m32(count, formattedSize) => "${count} failai (-ų), kiekvienas ${formattedSize}"; - static String m33(email) => + static String m33(newEmail) => "El. paštas pakeistas į ${newEmail}"; + + static String m34(email) => "${email} neturi „Ente“ paskyros."; + + static String m35(email) => "${email} neturi „Ente“ paskyros.\n\nSiųskite jiems kvietimą bendrinti nuotraukas."; - static String m34(text) => "Rastos papildomos nuotraukos, skirtos ${text}"; + static String m36(text) => "Rastos papildomos nuotraukos, skirtos ${text}"; static String m4(storageAmountInGB) => "${storageAmountInGB} GB kiekvieną kartą, kai kas nors užsiregistruoja mokamam planui ir pritaiko jūsų kodą."; - static String m37(endDate) => + static String m39(endDate) => "Nemokamas bandomasis laikotarpis galioja iki ${endDate}"; - static String m39(sizeInMBorGB) => "Atlaisvinti ${sizeInMBorGB}"; + static String m41(sizeInMBorGB) => "Atlaisvinti ${sizeInMBorGB}"; - static String m41(currentlyProcessing, totalCount) => + static String m43(currentlyProcessing, totalCount) => "Apdorojama ${currentlyProcessing} / ${totalCount}"; - static String m42(count) => + static String m44(count) => "${Intl.plural(count, one: '${count} elementas', few: '${count} elementai', many: '${count} elemento', other: '${count} elementų')}"; - static String m43(email) => "${email} pakvietė jus būti patikimu kontaktu"; + static String m45(email) => "${email} pakvietė jus būti patikimu kontaktu"; - static String m44(expiryTime) => "Nuoroda nebegalios ${expiryTime}"; + static String m46(expiryTime) => "Nuoroda nebegalios ${expiryTime}"; + + static String m48(personName, email) => + "Tai susies ${personName} su ${email}."; static String m5(count, formattedCount) => "${Intl.plural(count, zero: 'nėra prisiminimų', one: '${formattedCount} prisiminimas', few: '${formattedCount} prisiminimai', many: '${formattedCount} prisiminimo', other: '${formattedCount} prisiminimų')}"; - static String m45(count) => + static String m49(count) => "${Intl.plural(count, one: 'Perkelti elementą', few: 'Perkelti elementus', many: 'Perkelti elemento', other: 'Perkelti elementų')}"; - static String m47(personName) => "Nėra pasiūlymų asmeniui ${personName}."; + static String m51(personName) => "Nėra pasiūlymų asmeniui ${personName}."; - static String m48(name) => "Ne ${name}?"; + static String m52(name) => "Ne ${name}?"; - static String m49(familyAdminEmail) => + static String m53(familyAdminEmail) => "Susisiekite su ${familyAdminEmail}, kad pakeistumėte savo kodą."; static String m0(passwordStrengthValue) => "Slaptažodžio stiprumas: ${passwordStrengthValue}"; - static String m50(providerName) => + static String m54(providerName) => "Kreipkitės į ${providerName} palaikymo komandą, jei jums buvo nuskaičiuota."; - static String m52(endDate) => + static String m56(endDate) => "Nemokama bandomoji versija galioja iki ${endDate}.\nVėliau galėsite pasirinkti mokamą planą."; - static String m54(toEmail) => "Siųskite žurnalus adresu\n${toEmail}"; + static String m58(toEmail) => "Siųskite žurnalus adresu\n${toEmail}"; - static String m55(folderName) => "Apdorojama ${folderName}..."; + static String m59(folderName) => "Apdorojama ${folderName}..."; - static String m56(storeName) => "Vertinti mus parduotuvėje „${storeName}“"; + static String m60(storeName) => "Vertinti mus parduotuvėje „${storeName}“"; - static String m57(days, email) => + static String m61(name) => "Perskirstė jus į ${name}"; + + static String m62(days, email) => "Paskyrą galėsite pasiekti po ${days} dienų. Pranešimas bus išsiųstas į ${email}."; - static String m58(email) => + static String m63(email) => "Dabar galite atkurti ${email} paskyrą nustatydami naują slaptažodį."; - static String m59(email) => "${email} bando atkurti jūsų paskyrą."; + static String m64(email) => "${email} bando atkurti jūsų paskyrą."; - static String m60(storageInGB) => + static String m65(storageInGB) => "3. Abu gaunate ${storageInGB} GB* nemokamai"; - static String m61(userEmail) => + static String m66(userEmail) => "${userEmail} bus pašalintas iš šio bendrinamo albumo.\n\nVisos jų pridėtos nuotraukos taip pat bus pašalintos iš albumo."; - static String m62(endDate) => "Prenumerata atnaujinama ${endDate}"; + static String m67(endDate) => "Prenumerata atnaujinama ${endDate}"; - static String m63(count) => + static String m68(count) => "${Intl.plural(count, one: 'Rastas ${count} rezultatas', few: 'Rasti ${count} rezultatai', many: 'Rasta ${count} rezultato', other: 'Rasta ${count} rezultatų')}"; - static String m64(snapshotLength, searchLength) => + static String m69(snapshotLength, searchLength) => "Sekcijų ilgio neatitikimas: ${snapshotLength} != ${searchLength}"; static String m6(count) => "${count} pasirinkta"; - static String m65(count, yourCount) => + static String m70(count, yourCount) => "${count} pasirinkta (${yourCount} jūsų)"; - static String m66(verificationID) => + static String m71(verificationID) => "Štai mano patvirtinimo ID: ${verificationID}, skirta ente.io."; static String m7(verificationID) => "Ei, ar galite patvirtinti, kad tai yra jūsų ente.io patvirtinimo ID: ${verificationID}"; - static String m67(referralCode, referralStorageInGB) => + static String m72(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 m68(numberOfPeople) => + static String m73(numberOfPeople) => "${Intl.plural(numberOfPeople, zero: 'Bendrinti su konkrečiais asmenimis', one: 'Bendrinta su 1 asmeniu', other: 'Bendrinta su ${numberOfPeople} asmenimis')}"; - static String m70(fileType) => + static String m75(fileType) => "Šis ${fileType} bus ištrintas iš jūsų įrenginio."; - static String m71(fileType) => + static String m76(fileType) => "Šis ${fileType} yra ir saugykloje „Ente“ bei įrenginyje."; - static String m72(fileType) => "Šis ${fileType} bus ištrintas iš „Ente“."; + static String m77(fileType) => "Šis ${fileType} bus ištrintas iš „Ente“."; static String m1(storageAmountInGB) => "${storageAmountInGB} GB"; - static String m74(id) => + static String m79(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 m76(completed, total) => + static String m81(completed, total) => "${completed} / ${total} išsaugomi prisiminimai"; - static String m77(ignoreReason) => + static String m82(ignoreReason) => "Palieskite, kad įkeltumėte. Įkėlimas šiuo metu ignoruojamas dėl ${ignoreReason}."; static String m8(storageAmountInGB) => "Jie taip pat gauna ${storageAmountInGB} GB"; - static String m78(email) => "Tai – ${email} patvirtinimo ID"; + static String m83(email) => "Tai – ${email} patvirtinimo ID"; - static String m79(count) => + static String m84(count) => "${Intl.plural(count, zero: 'Netrukus', one: '1 diena', other: '${count} dienų')}"; - static String m80(email) => + static String m85(email) => "Buvote pakviesti tapti ${email} palikimo kontaktu."; - static String m81(galleryType) => + static String m86(galleryType) => "Galerijos tipas ${galleryType} nepalaikomas pervadinimui."; - static String m82(ignoreReason) => + static String m87(ignoreReason) => "Įkėlimas ignoruojamas dėl ${ignoreReason}."; - static String m84(endDate) => "Galioja iki ${endDate}"; + static String m89(endDate) => "Galioja iki ${endDate}"; - static String m85(email) => "Patvirtinti ${email}"; + static String m90(email) => "Patvirtinti ${email}"; - static String m86(count) => + static String m91(count) => "${Intl.plural(count, zero: 'Pridėta 0 žiūrėtojų', one: 'Pridėtas 1 žiūrėtojas', other: 'Pridėta ${count} žiūrėtojų')}"; static String m2(email) => "Išsiuntėme laišką adresu ${email}"; - static String m87(count) => + static String m92(count) => "${Intl.plural(count, one: 'prieš ${count} metus', few: 'prieš ${count} metus', many: 'prieš ${count} metų', other: 'prieš ${count} metų')}"; final messages = _notInlinedMessages(_notInlinedMessages); @@ -221,6 +235,7 @@ class MessageLookup extends MessageLookupByLibrary { "account": MessageLookupByLibrary.simpleMessage("Paskyra"), "accountIsAlreadyConfigured": MessageLookupByLibrary.simpleMessage("Paskyra jau sukonfigūruota."), + "accountOwnerPersonAppbarTitle": m9, "accountWelcomeBack": MessageLookupByLibrary.simpleMessage("Sveiki sugrįžę!"), "ackPasswordLostWarning": MessageLookupByLibrary.simpleMessage( @@ -233,9 +248,9 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Pridėti naują el. paštą"), "addCollaborator": MessageLookupByLibrary.simpleMessage("Pridėti bendradarbį"), - "addCollaborators": m9, + "addCollaborators": m10, "addFiles": MessageLookupByLibrary.simpleMessage("Pridėti failus"), - "addItem": m10, + "addItem": m11, "addLocation": MessageLookupByLibrary.simpleMessage("Pridėti vietovę"), "addLocationButton": MessageLookupByLibrary.simpleMessage("Pridėti"), "addMore": MessageLookupByLibrary.simpleMessage("Pridėti daugiau"), @@ -253,7 +268,7 @@ class MessageLookup extends MessageLookupByLibrary { "addTrustedContact": MessageLookupByLibrary.simpleMessage("Pridėti patikimą kontaktą"), "addViewer": MessageLookupByLibrary.simpleMessage("Pridėti žiūrėtoją"), - "addViewers": m12, + "addViewers": m13, "addedAs": MessageLookupByLibrary.simpleMessage("Pridėta kaip"), "addingToFavorites": MessageLookupByLibrary.simpleMessage("Pridedama prie mėgstamų..."), @@ -266,10 +281,13 @@ class MessageLookup extends MessageLookupByLibrary { "after1Week": MessageLookupByLibrary.simpleMessage("Po 1 savaitės"), "after1Year": MessageLookupByLibrary.simpleMessage("Po 1 metų"), "albumOwner": MessageLookupByLibrary.simpleMessage("Savininkas"), - "albumParticipantsCount": m15, + "albumParticipantsCount": m16, + "albumTitle": + MessageLookupByLibrary.simpleMessage("Albumo pavadinimas"), "albumUpdated": MessageLookupByLibrary.simpleMessage("Atnaujintas albumas"), "albums": MessageLookupByLibrary.simpleMessage("Albumai"), + "allClear": MessageLookupByLibrary.simpleMessage("✨ Viskas išvalyta"), "allMemoriesPreserved": MessageLookupByLibrary.simpleMessage("Išsaugoti visi prisiminimai"), "allPersonGroupingWillReset": MessageLookupByLibrary.simpleMessage( @@ -309,7 +327,7 @@ class MessageLookup extends MessageLookupByLibrary { "appLock": MessageLookupByLibrary.simpleMessage("Programos užraktas"), "appLockDescriptions": MessageLookupByLibrary.simpleMessage( "Pasirinkite tarp numatytojo įrenginio užrakinimo ekrano ir pasirinktinio užrakinimo ekrano su PIN kodu arba slaptažodžiu."), - "appVersion": m16, + "appVersion": m17, "appleId": MessageLookupByLibrary.simpleMessage("„Apple ID“"), "apply": MessageLookupByLibrary.simpleMessage("Taikyti"), "applyCodeTitle": MessageLookupByLibrary.simpleMessage("Taikyti kodą"), @@ -375,6 +393,8 @@ class MessageLookup extends MessageLookupByLibrary { "autoPairDesc": MessageLookupByLibrary.simpleMessage( "Automatinis susiejimas veikia tik su įrenginiais, kurie palaiko „Chromecast“."), "available": MessageLookupByLibrary.simpleMessage("Prieinama"), + "backedUpFolders": MessageLookupByLibrary.simpleMessage( + "Sukurtos atsarginės aplankų kopijos"), "backup": MessageLookupByLibrary.simpleMessage("Kurti atsarginę kopiją"), "backupFile": MessageLookupByLibrary.simpleMessage( @@ -406,7 +426,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Atšaukti atkūrimą"), "cancelAccountRecoveryBody": MessageLookupByLibrary.simpleMessage( "Ar tikrai norite atšaukti atkūrimą?"), - "cancelOtherSubscription": m18, + "cancelOtherSubscription": m19, "cancelSubscription": MessageLookupByLibrary.simpleMessage("Atsisakyti prenumeratos"), "cannotAddMorePhotosAfterBecomingViewer": m3, @@ -458,7 +478,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Gaukite nemokamos saugyklos"), "claimMore": MessageLookupByLibrary.simpleMessage("Gaukite daugiau!"), "claimed": MessageLookupByLibrary.simpleMessage("Gauta"), - "claimedStorageSoFar": m19, + "claimedStorageSoFar": m20, "cleanUncategorized": MessageLookupByLibrary.simpleMessage("Valyti nekategorizuotus"), "cleanUncategorizedDescription": MessageLookupByLibrary.simpleMessage( @@ -485,7 +505,7 @@ class MessageLookup extends MessageLookupByLibrary { "collaboratorsCanAddPhotosAndVideosToTheSharedAlbum": MessageLookupByLibrary.simpleMessage( "Bendradarbiai gali pridėti nuotraukų ir vaizdo įrašų į bendrintą albumą."), - "collaboratorsSuccessfullyAdded": m21, + "collaboratorsSuccessfullyAdded": m22, "collect": MessageLookupByLibrary.simpleMessage("Rinkti"), "collectEventPhotos": MessageLookupByLibrary.simpleMessage("Rinkti įvykių nuotraukas"), @@ -500,7 +520,7 @@ class MessageLookup extends MessageLookupByLibrary { "Ar tikrai norite išjungti dvigubą tapatybės nustatymą?"), "confirmAccountDeletion": MessageLookupByLibrary.simpleMessage( "Patvirtinti paskyros ištrynimą"), - "confirmAddingTrustedContact": m22, + "confirmAddingTrustedContact": m23, "confirmDeletePrompt": MessageLookupByLibrary.simpleMessage( "Taip, noriu negrįžtamai ištrinti šią paskyrą ir jos duomenis per visas programas"), "confirmPassword": @@ -515,6 +535,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Prijungti prie įrenginio"), "contactSupport": MessageLookupByLibrary.simpleMessage( "Susisiekti su palaikymo komanda"), + "contactToManageSubscription": m25, "contacts": MessageLookupByLibrary.simpleMessage("Kontaktai"), "continueLabel": MessageLookupByLibrary.simpleMessage("Tęsti"), "continueOnFreeTrial": MessageLookupByLibrary.simpleMessage( @@ -551,7 +572,7 @@ class MessageLookup extends MessageLookupByLibrary { "currentlyRunning": MessageLookupByLibrary.simpleMessage("šiuo metu vykdoma"), "custom": MessageLookupByLibrary.simpleMessage("Pasirinktinis"), - "customEndpoint": m25, + "customEndpoint": m26, "darkTheme": MessageLookupByLibrary.simpleMessage("Tamsi"), "dayToday": MessageLookupByLibrary.simpleMessage("Šiandien"), "dayYesterday": MessageLookupByLibrary.simpleMessage("Vakar"), @@ -587,12 +608,12 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Ištrinti iš įrenginio"), "deleteFromEnte": MessageLookupByLibrary.simpleMessage("Ištrinti iš „Ente“"), - "deleteItemCount": m26, + "deleteItemCount": m27, "deleteLocation": MessageLookupByLibrary.simpleMessage("Ištrinti vietovę"), "deletePhotos": MessageLookupByLibrary.simpleMessage("Ištrinti nuotraukas"), - "deleteProgress": m27, + "deleteProgress": m28, "deleteReason1": MessageLookupByLibrary.simpleMessage( "Trūksta pagrindinės funkcijos, kurios man reikia"), "deleteReason2": MessageLookupByLibrary.simpleMessage( @@ -628,9 +649,12 @@ class MessageLookup extends MessageLookupByLibrary { "Žiūrėtojai vis tiek gali daryti ekrano kopijas arba išsaugoti nuotraukų kopijas naudojant išorinius įrankius"), "disableDownloadWarningTitle": MessageLookupByLibrary.simpleMessage("Atkreipkite dėmesį"), - "disableLinkMessage": m28, + "disableLinkMessage": m29, "disableTwofactor": MessageLookupByLibrary.simpleMessage( "Išjungti dvigubą tapatybės nustatymą"), + "disablingTwofactorAuthentication": + MessageLookupByLibrary.simpleMessage( + "Išjungiamas dvigubas tapatybės nustatymas..."), "discord": MessageLookupByLibrary.simpleMessage("„Discord“"), "discover": MessageLookupByLibrary.simpleMessage("Atraskite"), "discover_babies": MessageLookupByLibrary.simpleMessage("Kūdikiai"), @@ -657,15 +681,16 @@ class MessageLookup extends MessageLookupByLibrary { "doThisLater": MessageLookupByLibrary.simpleMessage("Daryti tai vėliau"), "done": MessageLookupByLibrary.simpleMessage("Atlikta"), + "dontSave": MessageLookupByLibrary.simpleMessage("Neišsaugoti"), "doubleYourStorage": MessageLookupByLibrary.simpleMessage("Padvigubinkite saugyklą"), "download": MessageLookupByLibrary.simpleMessage("Atsisiųsti"), "downloadFailed": MessageLookupByLibrary.simpleMessage("Atsisiuntimas nepavyko."), "downloading": MessageLookupByLibrary.simpleMessage("Atsisiunčiama..."), - "dropSupportEmail": m29, - "duplicateFileCountWithStorageSaved": m30, - "duplicateItemsGroup": m31, + "dropSupportEmail": m30, + "duplicateFileCountWithStorageSaved": m31, + "duplicateItemsGroup": m32, "edit": MessageLookupByLibrary.simpleMessage("Redaguoti"), "editLocation": MessageLookupByLibrary.simpleMessage("Redaguoti vietovę"), @@ -679,7 +704,9 @@ class MessageLookup extends MessageLookupByLibrary { "email": MessageLookupByLibrary.simpleMessage("El. paštas"), "emailAlreadyRegistered": MessageLookupByLibrary.simpleMessage( "El. paštas jau užregistruotas."), - "emailNoEnteAccount": m33, + "emailChangedTo": m33, + "emailDoesNotHaveEnteAccount": m34, + "emailNoEnteAccount": m35, "emailNotRegistered": MessageLookupByLibrary.simpleMessage("El. paštas neregistruotas."), "emailVerificationToggle": @@ -701,6 +728,8 @@ class MessageLookup extends MessageLookupByLibrary { "enableMapsDesc": MessageLookupByLibrary.simpleMessage( "Tai parodys jūsų nuotraukas pasaulio žemėlapyje.\n\nŠį žemėlapį talpina „OpenStreetMap“, o tiksliomis nuotraukų vietovėmis niekada nebendrinama.\n\nŠią funkciją bet kada galite išjungti iš nustatymų."), "enabled": MessageLookupByLibrary.simpleMessage("Įjungta"), + "encryptingBackup": MessageLookupByLibrary.simpleMessage( + "Šifruojama atsarginė kopija..."), "encryption": MessageLookupByLibrary.simpleMessage("Šifravimas"), "encryptionKeys": MessageLookupByLibrary.simpleMessage("Šifravimo raktai"), @@ -759,12 +788,13 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Eksportuoti duomenis"), "extraPhotosFound": MessageLookupByLibrary.simpleMessage( "Rastos papildomos nuotraukos"), - "extraPhotosFoundFor": m34, + "extraPhotosFoundFor": m36, "faceNotClusteredYet": MessageLookupByLibrary.simpleMessage( "Veidas dar nesugrupuotas. Grįžkite vėliau."), "faceRecognition": MessageLookupByLibrary.simpleMessage("Veido atpažinimas"), "faces": MessageLookupByLibrary.simpleMessage("Veidai"), + "failed": MessageLookupByLibrary.simpleMessage("Nepavyko"), "failedToApplyCode": MessageLookupByLibrary.simpleMessage("Nepavyko pritaikyti kodo."), "failedToCancel": @@ -811,15 +841,19 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Naudojama nemokama saugykla"), "freeTrial": MessageLookupByLibrary.simpleMessage( "Nemokamas bandomasis laikotarpis"), - "freeTrialValidTill": m37, - "freeUpAmount": m39, + "freeTrialValidTill": m39, + "freeUpAmount": m41, + "freeUpDeviceSpace": + MessageLookupByLibrary.simpleMessage("Atlaisvinti įrenginio vietą"), + "freeUpDeviceSpaceDesc": MessageLookupByLibrary.simpleMessage( + "Sutaupykite vietos savo įrenginyje išvalydami failus, kurių atsarginės kopijos jau buvo sukurtos."), "gallery": MessageLookupByLibrary.simpleMessage("Galerija"), "galleryMemoryLimitInfo": MessageLookupByLibrary.simpleMessage( "Galerijoje rodoma iki 1000 prisiminimų"), "general": MessageLookupByLibrary.simpleMessage("Bendrieji"), "generatingEncryptionKeys": MessageLookupByLibrary.simpleMessage( "Generuojami šifravimo raktai..."), - "genericProgress": m41, + "genericProgress": m43, "goToSettings": MessageLookupByLibrary.simpleMessage("Eiti į nustatymus"), "googlePlayId": @@ -872,6 +906,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Indeksuoti elementai"), "indexingIsPaused": MessageLookupByLibrary.simpleMessage( "Indeksavimas pristabdytas. Jis bus automatiškai tęsiamas, kai įrenginys yra paruoštas."), + "ineligible": MessageLookupByLibrary.simpleMessage("Netinkami"), "info": MessageLookupByLibrary.simpleMessage("Informacija"), "insecureDevice": MessageLookupByLibrary.simpleMessage("Nesaugus įrenginys"), @@ -894,7 +929,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": m44, "itemsShowTheNumberOfDaysRemainingBeforePermanentDeletion": MessageLookupByLibrary.simpleMessage( "Elementai rodo likusių dienų skaičių iki visiško ištrynimo."), @@ -903,6 +938,8 @@ class MessageLookup extends MessageLookupByLibrary { "join": MessageLookupByLibrary.simpleMessage("Jungtis"), "joinAlbum": MessageLookupByLibrary.simpleMessage("Junkitės prie albumo"), + "joinAlbumConfirmationDialogBody": MessageLookupByLibrary.simpleMessage( + "Prisijungus prie albumo, jūsų el. paštas bus matomas jo dalyviams."), "joinAlbumSubtext": MessageLookupByLibrary.simpleMessage( "kad peržiūrėtumėte ir pridėtumėte savo nuotraukas"), "joinAlbumSubtextViewer": MessageLookupByLibrary.simpleMessage( @@ -925,7 +962,7 @@ class MessageLookup extends MessageLookupByLibrary { "legacy": MessageLookupByLibrary.simpleMessage("Palikimas"), "legacyAccounts": MessageLookupByLibrary.simpleMessage("Palikimo paskyros"), - "legacyInvite": m43, + "legacyInvite": m45, "legacyPageDesc": MessageLookupByLibrary.simpleMessage( "Palikimas leidžia patikimiems kontaktams pasiekti jūsų paskyrą jums nesant."), "legacyPageDesc2": MessageLookupByLibrary.simpleMessage( @@ -938,12 +975,16 @@ class MessageLookup extends MessageLookupByLibrary { "linkEmail": MessageLookupByLibrary.simpleMessage("Susieti el. paštą"), "linkEnabled": MessageLookupByLibrary.simpleMessage("Įjungta"), "linkExpired": MessageLookupByLibrary.simpleMessage("Nebegalioja"), - "linkExpiresOn": m44, + "linkExpiresOn": m46, "linkExpiry": MessageLookupByLibrary.simpleMessage("Nuorodos galiojimo laikas"), "linkHasExpired": MessageLookupByLibrary.simpleMessage("Nuoroda nebegalioja"), "linkNeverExpires": MessageLookupByLibrary.simpleMessage("Niekada"), + "linkPerson": MessageLookupByLibrary.simpleMessage("Susiekite asmenį,"), + "linkPersonCaption": MessageLookupByLibrary.simpleMessage( + "kad geriau bendrintumėte patirtį"), + "linkPersonToEmailConfirmation": m48, "livePhotos": MessageLookupByLibrary.simpleMessage("Gyvos nuotraukos"), "loadMessage1": MessageLookupByLibrary.simpleMessage( "Galite bendrinti savo prenumeratą su šeima."), @@ -982,6 +1023,7 @@ class MessageLookup extends MessageLookupByLibrary { "Vietos žymė grupuoja visas nuotraukas, kurios buvo padarytos tam tikru spinduliu nuo nuotraukos"), "locations": MessageLookupByLibrary.simpleMessage("Vietovės"), "lockButtonLabel": MessageLookupByLibrary.simpleMessage("Užrakinti"), + "lockscreen": MessageLookupByLibrary.simpleMessage("Ekrano užraktas"), "logInLabel": MessageLookupByLibrary.simpleMessage("Prisijungti"), "loggingOut": MessageLookupByLibrary.simpleMessage("Atsijungiama..."), "loginSessionExpired": @@ -1025,6 +1067,7 @@ class MessageLookup extends MessageLookupByLibrary { "maps": MessageLookupByLibrary.simpleMessage("Žemėlapiai"), "mastodon": MessageLookupByLibrary.simpleMessage("„Mastodon“"), "matrix": MessageLookupByLibrary.simpleMessage("„Matrix“"), + "me": MessageLookupByLibrary.simpleMessage("Aš"), "memoryCount": m5, "merchandise": MessageLookupByLibrary.simpleMessage("Atributika"), "mergeWithExisting": @@ -1053,7 +1096,7 @@ class MessageLookup extends MessageLookupByLibrary { "Daugiau išsamios informacijos"), "mostRecent": MessageLookupByLibrary.simpleMessage("Naujausią"), "mostRelevant": MessageLookupByLibrary.simpleMessage("Aktualiausią"), - "moveItem": m45, + "moveItem": m49, "movedToTrash": MessageLookupByLibrary.simpleMessage("Perkelta į šiukšlinę"), "name": MessageLookupByLibrary.simpleMessage("Pavadinimą"), @@ -1075,6 +1118,8 @@ class MessageLookup extends MessageLookupByLibrary { "noDeviceFound": MessageLookupByLibrary.simpleMessage("Įrenginys nerastas"), "noDeviceLimit": MessageLookupByLibrary.simpleMessage("Jokio"), + "noDeviceThatCanBeDeleted": MessageLookupByLibrary.simpleMessage( + "Neturite šiame įrenginyje failų, kuriuos galima ištrinti."), "noDuplicates": MessageLookupByLibrary.simpleMessage("✨ Dublikatų nėra"), "noEnteAccountExclamation": @@ -1094,10 +1139,10 @@ class MessageLookup extends MessageLookupByLibrary { "noResults": MessageLookupByLibrary.simpleMessage("Rezultatų nėra."), "noResultsFound": MessageLookupByLibrary.simpleMessage("Rezultatų nerasta."), - "noSuggestionsForPerson": m47, + "noSuggestionsForPerson": m51, "noSystemLockFound": MessageLookupByLibrary.simpleMessage("Nerastas sistemos užraktas"), - "notPersonLabel": m48, + "notPersonLabel": m52, "nothingSharedWithYouYet": MessageLookupByLibrary.simpleMessage( "Kol kas su jumis niekuo nesibendrinama."), "nothingToSeeHere": MessageLookupByLibrary.simpleMessage( @@ -1107,7 +1152,7 @@ class MessageLookup extends MessageLookupByLibrary { "onDevice": MessageLookupByLibrary.simpleMessage("Įrenginyje"), "onEnte": MessageLookupByLibrary.simpleMessage( "Saugykloje ente"), - "onlyFamilyAdminCanChangeCode": m49, + "onlyFamilyAdminCanChangeCode": m53, "onlyThem": MessageLookupByLibrary.simpleMessage("Tik jiems"), "oops": MessageLookupByLibrary.simpleMessage("Ups"), "oopsSomethingWentWrong": @@ -1123,8 +1168,8 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Arba sujunkite su esamais"), "orPickAnExistingOne": MessageLookupByLibrary.simpleMessage("Arba pasirinkite esamą"), - "orPickFromYourContacts": - MessageLookupByLibrary.simpleMessage("or pick from your contacts"), + "orPickFromYourContacts": MessageLookupByLibrary.simpleMessage( + "arba pasirinkite iš savo kontaktų"), "pair": MessageLookupByLibrary.simpleMessage("Susieti"), "pairWithPin": MessageLookupByLibrary.simpleMessage("Susieti su PIN"), "pairingComplete": @@ -1151,7 +1196,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": m50, + "paymentFailedTalkToProvider": m54, "pendingItems": MessageLookupByLibrary.simpleMessage("Laukiami elementai"), "pendingSync": @@ -1175,19 +1220,26 @@ class MessageLookup extends MessageLookupByLibrary { "pinLock": MessageLookupByLibrary.simpleMessage("PIN užrakinimas"), "playOnTv": MessageLookupByLibrary.simpleMessage( "Paleisti albumą televizoriuje"), - "playStoreFreeTrialValidTill": m52, + "playOriginal": + MessageLookupByLibrary.simpleMessage("Leisti originalą"), + "playStoreFreeTrialValidTill": m56, + "playStream": + MessageLookupByLibrary.simpleMessage("Leisti srautinį perdavimą"), "playstoreSubscription": MessageLookupByLibrary.simpleMessage("„PlayStore“ prenumerata"), "pleaseCheckYourInternetConnectionAndTryAgain": MessageLookupByLibrary.simpleMessage( "Patikrinkite savo interneto ryšį ir bandykite dar kartą."), + "pleaseContactSupportAndWeWillBeHappyToHelp": + MessageLookupByLibrary.simpleMessage( + "Susisiekite adresu support@ente.io ir mes mielai padėsime!"), "pleaseGrantPermissions": MessageLookupByLibrary.simpleMessage("Suteikite leidimus."), "pleaseLoginAgain": MessageLookupByLibrary.simpleMessage("Prisijunkite iš naujo."), "pleaseSelectQuickLinksToRemove": MessageLookupByLibrary.simpleMessage( "Pasirinkite sparčiąsias nuorodas, kad pašalintumėte"), - "pleaseSendTheLogsTo": m54, + "pleaseSendTheLogsTo": m58, "pleaseTryAgain": MessageLookupByLibrary.simpleMessage("Bandykite dar kartą."), "pleaseVerifyTheCodeYouHaveEntered": @@ -1211,13 +1263,18 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Privatus bendrinimas"), "proceed": MessageLookupByLibrary.simpleMessage("Tęsti"), "processed": MessageLookupByLibrary.simpleMessage("Apdorota"), - "processingImport": m55, + "processing": MessageLookupByLibrary.simpleMessage("Apdorojama"), + "processingImport": m59, + "processingVideos": + MessageLookupByLibrary.simpleMessage("Apdorojami vaizdo įrašai"), "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": m56, + "rateUsOnStore": m60, + "reassignedToName": m61, "recover": MessageLookupByLibrary.simpleMessage("Atkurti"), "recoverAccount": MessageLookupByLibrary.simpleMessage("Atkurti paskyrą"), @@ -1226,7 +1283,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Atkurti paskyrą"), "recoveryInitiated": MessageLookupByLibrary.simpleMessage("Pradėtas atkūrimas"), - "recoveryInitiatedDesc": m57, + "recoveryInitiatedDesc": m62, "recoveryKey": MessageLookupByLibrary.simpleMessage("Atkūrimo raktas"), "recoveryKeyCopiedToClipboard": MessageLookupByLibrary.simpleMessage( "Nukopijuotas atkūrimo raktas į iškarpinę"), @@ -1240,12 +1297,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": m58, + "recoveryReady": m63, "recoverySuccessful": MessageLookupByLibrary.simpleMessage("Atkūrimas sėkmingas."), "recoveryWarning": MessageLookupByLibrary.simpleMessage( "Patikimas kontaktas bando pasiekti jūsų paskyrą."), - "recoveryWarningBody": m59, + "recoveryWarningBody": m64, "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": @@ -1261,7 +1318,7 @@ class MessageLookup extends MessageLookupByLibrary { "1. Duokite šį kodą savo draugams"), "referralStep2": MessageLookupByLibrary.simpleMessage( "2. Jie užsiregistruoja mokamą planą"), - "referralStep3": m60, + "referralStep3": m65, "referralsAreCurrentlyPaused": MessageLookupByLibrary.simpleMessage( "Šiuo metu rekomendacijos yra pristabdytos"), "rejectRecovery": @@ -1279,6 +1336,8 @@ class MessageLookup extends MessageLookupByLibrary { "remove": MessageLookupByLibrary.simpleMessage("Šalinti"), "removeDuplicates": MessageLookupByLibrary.simpleMessage("Šalinti dublikatus"), + "removeDuplicatesDesc": MessageLookupByLibrary.simpleMessage( + "Peržiūrėkite ir pašalinkite failus, kurie yra tiksliai dublikatai."), "removeFromAlbum": MessageLookupByLibrary.simpleMessage("Šalinti iš albumo"), "removeFromAlbumTitle": @@ -1290,7 +1349,7 @@ class MessageLookup extends MessageLookupByLibrary { "removeLink": MessageLookupByLibrary.simpleMessage("Šalinti nuorodą"), "removeParticipant": MessageLookupByLibrary.simpleMessage("Šalinti dalyvį"), - "removeParticipantBody": m61, + "removeParticipantBody": m66, "removePersonLabel": MessageLookupByLibrary.simpleMessage("Šalinti asmens žymą"), "removePublicLink": @@ -1309,7 +1368,7 @@ class MessageLookup extends MessageLookupByLibrary { "renameFile": MessageLookupByLibrary.simpleMessage("Pervadinti failą"), "renewSubscription": MessageLookupByLibrary.simpleMessage("Atnaujinti prenumeratą"), - "renewsOn": m62, + "renewsOn": m67, "reportABug": MessageLookupByLibrary.simpleMessage("Pranešti apie riktą"), "reportBug": @@ -1333,6 +1392,9 @@ class MessageLookup extends MessageLookupByLibrary { "right": MessageLookupByLibrary.simpleMessage("Dešinė"), "rotate": MessageLookupByLibrary.simpleMessage("Sukti"), "safelyStored": MessageLookupByLibrary.simpleMessage("Saugiai saugoma"), + "saveChangesBeforeLeavingQuestion": + MessageLookupByLibrary.simpleMessage( + "Išsaugoti pakeitimus prieš išeinant?"), "saveKey": MessageLookupByLibrary.simpleMessage("Išsaugoti raktą"), "savePerson": MessageLookupByLibrary.simpleMessage("Išsaugoti asmenį"), "saveYourRecoveryKeyIfYouHaventAlready": @@ -1345,6 +1407,8 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage( "Skenuokite šį QR kodą\nsu autentifikatoriaus programa"), "search": MessageLookupByLibrary.simpleMessage("Ieškokite"), + "searchByAlbumNameHint": + MessageLookupByLibrary.simpleMessage("Albumo pavadinimas"), "searchByExamples": MessageLookupByLibrary.simpleMessage( "• 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“)"), "searchCaptionEmptySection": MessageLookupByLibrary.simpleMessage( @@ -1362,8 +1426,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": m63, - "searchSectionsLengthMismatch": m64, + "searchResultCount": m68, + "searchSectionsLengthMismatch": m69, "security": MessageLookupByLibrary.simpleMessage("Saugumas"), "seePublicAlbumLinksInApp": MessageLookupByLibrary.simpleMessage( "Žiūrėti viešų albumų nuorodas programoje"), @@ -1391,7 +1455,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage( "Pasirinkti aplankai bus užšifruoti ir sukurtos atsarginės kopijos."), "selectedPhotos": m6, - "selectedPhotosWithYours": m65, + "selectedPhotosWithYours": m70, "send": MessageLookupByLibrary.simpleMessage("Siųsti"), "sendEmail": MessageLookupByLibrary.simpleMessage("Siųsti el. laišką"), "sendInvite": MessageLookupByLibrary.simpleMessage("Siųsti kvietimą"), @@ -1422,16 +1486,16 @@ class MessageLookup extends MessageLookupByLibrary { "shareAnAlbumNow": MessageLookupByLibrary.simpleMessage("Bendrinti albumą dabar"), "shareLink": MessageLookupByLibrary.simpleMessage("Bendrinti nuorodą"), - "shareMyVerificationID": m66, + "shareMyVerificationID": m71, "shareOnlyWithThePeopleYouWant": MessageLookupByLibrary.simpleMessage( "Bendrinkite tik su tais asmenimis, su kuriais norite"), "shareTextConfirmOthersVerificationID": m7, "shareTextRecommendUsingEnte": MessageLookupByLibrary.simpleMessage( "Atsisiųskite „Ente“, kad galėtume lengvai bendrinti originalios kokybės nuotraukas ir vaizdo įrašus.\n\nhttps://ente.io"), - "shareTextReferralCode": m67, + "shareTextReferralCode": m72, "shareWithNonenteUsers": MessageLookupByLibrary.simpleMessage( "Bendrinkite su ne „Ente“ naudotojais."), - "shareWithPeopleSectionTitle": m68, + "shareWithPeopleSectionTitle": m73, "sharedAlbumSectionDescription": MessageLookupByLibrary.simpleMessage( "Sukurkite bendrinamus ir bendradarbiaujamus albumus su kitais „Ente“ naudotojais, įskaitant naudotojus nemokamuose planuose."), "sharedByYou": @@ -1452,11 +1516,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": m70, + "singleFileDeleteFromDevice": m75, "singleFileDeleteHighlight": MessageLookupByLibrary.simpleMessage( "Jis bus ištrintas iš visų albumų."), - "singleFileInBothLocalAndRemote": m71, - "singleFileInRemoteOnly": m72, + "singleFileInBothLocalAndRemote": m76, + "singleFileInRemoteOnly": m77, "skip": MessageLookupByLibrary.simpleMessage("Praleisti"), "social": MessageLookupByLibrary.simpleMessage("Socialinės"), "someOfTheFilesYouAreTryingToDeleteAre": @@ -1500,8 +1564,10 @@ class MessageLookup extends MessageLookupByLibrary { "storageInGB": m1, "storageLimitExceeded": MessageLookupByLibrary.simpleMessage("Viršyta saugyklos riba."), + "streamDetails": MessageLookupByLibrary.simpleMessage( + "Srautinio perdavimo išsami informacija"), "strongStrength": MessageLookupByLibrary.simpleMessage("Stipri"), - "subAlreadyLinkedErrMessage": m74, + "subAlreadyLinkedErrMessage": m79, "subscribe": MessageLookupByLibrary.simpleMessage("Prenumeruoti"), "subscribeToEnableSharing": MessageLookupByLibrary.simpleMessage( "Kad įjungtumėte bendrinimą, reikia aktyvios mokamos prenumeratos."), @@ -1513,8 +1579,8 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Sėkmingai išarchyvuota"), "suggestFeatures": MessageLookupByLibrary.simpleMessage("Siūlyti funkcijas"), - "support": MessageLookupByLibrary.simpleMessage("Palaikymas"), - "syncProgress": m76, + "support": MessageLookupByLibrary.simpleMessage("Pagalba"), + "syncProgress": m81, "syncStopped": MessageLookupByLibrary.simpleMessage( "Sinchronizavimas sustabdytas"), "syncing": MessageLookupByLibrary.simpleMessage("Sinchronizuojama..."), @@ -1527,7 +1593,7 @@ class MessageLookup extends MessageLookupByLibrary { "Palieskite, kad atrakintumėte"), "tapToUpload": MessageLookupByLibrary.simpleMessage("Palieskite, kad įkeltumėte"), - "tapToUploadIsIgnoredDue": m77, + "tapToUploadIsIgnoredDue": m82, "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"), @@ -1554,7 +1620,8 @@ class MessageLookup extends MessageLookupByLibrary { "Šis el. paštas jau naudojamas."), "thisImageHasNoExifData": MessageLookupByLibrary.simpleMessage( "Šis vaizdas neturi Exif duomenų"), - "thisIsPersonVerificationId": m78, + "thisIsMeExclamation": MessageLookupByLibrary.simpleMessage("Tai aš!"), + "thisIsPersonVerificationId": m83, "thisIsYourVerificationId": MessageLookupByLibrary.simpleMessage("Tai – jūsų patvirtinimo ID"), "thisWillLogYouOutOfTheFollowingDevice": @@ -1576,11 +1643,11 @@ class MessageLookup extends MessageLookupByLibrary { "Per daug neteisingų bandymų."), "total": MessageLookupByLibrary.simpleMessage("iš viso"), "trash": MessageLookupByLibrary.simpleMessage("Šiukšlinė"), - "trashDaysLeft": m79, + "trashDaysLeft": m84, "trim": MessageLookupByLibrary.simpleMessage("Trumpinti"), "trustedContacts": MessageLookupByLibrary.simpleMessage("Patikimi kontaktai"), - "trustedInviteBody": m80, + "trustedInviteBody": m85, "tryAgain": MessageLookupByLibrary.simpleMessage("Bandyti dar kartą"), "twitter": MessageLookupByLibrary.simpleMessage("„Twitter“"), "twoMonthsFreeOnYearlyPlans": MessageLookupByLibrary.simpleMessage( @@ -1592,7 +1659,7 @@ class MessageLookup extends MessageLookupByLibrary { "Dvigubas tapatybės nustatymas"), "twofactorSetup": MessageLookupByLibrary.simpleMessage( "Dvigubo tapatybės nustatymo sąranka"), - "typeOfGallerGallerytypeIsNotSupportedForRename": m81, + "typeOfGallerGallerytypeIsNotSupportedForRename": m86, "unarchive": MessageLookupByLibrary.simpleMessage("Išarchyvuoti"), "unarchiveAlbum": MessageLookupByLibrary.simpleMessage("Išarchyvuoti albumą"), @@ -1611,7 +1678,7 @@ class MessageLookup extends MessageLookupByLibrary { "updatingFolderSelection": MessageLookupByLibrary.simpleMessage( "Atnaujinamas aplankų pasirinkimas..."), "upgrade": MessageLookupByLibrary.simpleMessage("Keisti planą"), - "uploadIsIgnoredDueToIgnorereason": m82, + "uploadIsIgnoredDueToIgnorereason": m87, "upto50OffUntil4thDec": MessageLookupByLibrary.simpleMessage( "Iki 50% nuolaida, gruodžio 4 d."), "usableReferralStorageInfo": MessageLookupByLibrary.simpleMessage( @@ -1625,7 +1692,7 @@ class MessageLookup extends MessageLookupByLibrary { "useRecoveryKey": MessageLookupByLibrary.simpleMessage("Naudoti atkūrimo raktą"), "usedSpace": MessageLookupByLibrary.simpleMessage("Naudojama vieta"), - "validTill": m84, + "validTill": m89, "verificationFailedPleaseTryAgain": MessageLookupByLibrary.simpleMessage( "Patvirtinimas nepavyko. Bandykite dar kartą."), @@ -1634,7 +1701,7 @@ class MessageLookup extends MessageLookupByLibrary { "verify": MessageLookupByLibrary.simpleMessage("Patvirtinti"), "verifyEmail": MessageLookupByLibrary.simpleMessage("Patvirtinti el. paštą"), - "verifyEmailID": m85, + "verifyEmailID": m90, "verifyIDLabel": MessageLookupByLibrary.simpleMessage("Patvirtinti"), "verifyPasskey": MessageLookupByLibrary.simpleMessage("Patvirtinti slaptaraktį"), @@ -1646,15 +1713,20 @@ class MessageLookup extends MessageLookupByLibrary { "videoInfo": MessageLookupByLibrary.simpleMessage("Vaizdo įrašo informacija"), "videoSmallCase": MessageLookupByLibrary.simpleMessage("vaizdo įrašas"), + "videoStreaming": MessageLookupByLibrary.simpleMessage( + "Vaizdo įrašų srautinis perdavimas"), "videos": MessageLookupByLibrary.simpleMessage("Vaizdo įrašai"), + "viewActiveSessions": + MessageLookupByLibrary.simpleMessage("Peržiūrėti aktyvius seansus"), "viewAddOnButton": MessageLookupByLibrary.simpleMessage("Peržiūrėti priedus"), "viewAll": MessageLookupByLibrary.simpleMessage("Peržiūrėti viską"), + "viewLargeFiles": MessageLookupByLibrary.simpleMessage("Dideli failai"), "viewLogs": MessageLookupByLibrary.simpleMessage("Peržiūrėti žurnalus"), "viewRecoveryKey": MessageLookupByLibrary.simpleMessage("Peržiūrėti atkūrimo raktą"), "viewer": MessageLookupByLibrary.simpleMessage("Žiūrėtojas"), - "viewersSuccessfullyAdded": m86, + "viewersSuccessfullyAdded": m91, "visitWebToManage": MessageLookupByLibrary.simpleMessage( "Aplankykite web.ente.io, kad tvarkytumėte savo prenumeratą"), "waitingForVerification": @@ -1675,7 +1747,7 @@ class MessageLookup extends MessageLookupByLibrary { "Patikimas kontaktas gali padėti atkurti jūsų duomenis."), "yearShort": MessageLookupByLibrary.simpleMessage("m."), "yearly": MessageLookupByLibrary.simpleMessage("Metinis"), - "yearsAgo": m87, + "yearsAgo": m92, "yes": MessageLookupByLibrary.simpleMessage("Taip"), "yesCancel": MessageLookupByLibrary.simpleMessage("Taip, atsisakyti"), "yesConvertToViewer": @@ -1705,9 +1777,6 @@ class MessageLookup extends MessageLookupByLibrary { "yourSubscriptionHasExpired": MessageLookupByLibrary.simpleMessage("Jūsų prenumerata baigėsi."), "yourVerificationCodeHasExpired": MessageLookupByLibrary.simpleMessage( - "Jūsų patvirtinimo kodas nebegaliojantis."), - "youveNoDuplicateFilesThatCanBeCleared": - MessageLookupByLibrary.simpleMessage( - "Neturite dubliuotų failų, kuriuos būtų galima išvalyti.") + "Jūsų patvirtinimo kodas nebegaliojantis.") }; } diff --git a/mobile/lib/generated/intl/messages_ml.dart b/mobile/lib/generated/intl/messages_ml.dart index 6eacf1ed40..6a3eec447c 100644 --- a/mobile/lib/generated/intl/messages_ml.dart +++ b/mobile/lib/generated/intl/messages_ml.dart @@ -90,8 +90,6 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("ഇവിടൊന്നും കാണ്മാനില്ല! 👀"), "ok": MessageLookupByLibrary.simpleMessage("ശരി"), "oops": MessageLookupByLibrary.simpleMessage("അയ്യോ"), - "orPickFromYourContacts": - MessageLookupByLibrary.simpleMessage("or pick from your contacts"), "password": MessageLookupByLibrary.simpleMessage("സങ്കേതക്കുറി"), "pleaseTryAgain": MessageLookupByLibrary.simpleMessage("ദയവായി വീണ്ടും ശ്രമിക്കുക"), diff --git a/mobile/lib/generated/intl/messages_nl.dart b/mobile/lib/generated/intl/messages_nl.dart index c76ec206ef..6bf05c05d5 100644 --- a/mobile/lib/generated/intl/messages_nl.dart +++ b/mobile/lib/generated/intl/messages_nl.dart @@ -20,37 +20,39 @@ typedef String MessageIfAbsent(String messageStr, List args); class MessageLookup extends MessageLookupByLibrary { String get localeName => 'nl'; - static String m9(count) => - "${Intl.plural(count, zero: 'Voeg samenwerker toe', one: 'Voeg samenwerker toe', other: 'Voeg samenwerkers toe')}"; + static String m9(title) => "${title} (Ik)"; static String m10(count) => + "${Intl.plural(count, zero: 'Voeg samenwerker toe', one: 'Voeg samenwerker toe', other: 'Voeg samenwerkers toe')}"; + + static String m11(count) => "${Intl.plural(count, one: 'Bestand toevoegen', other: 'Bestanden toevoegen')}"; - static String m11(storageAmount, endDate) => + static String m12(storageAmount, endDate) => "Jouw ${storageAmount} add-on is geldig tot ${endDate}"; - static String m12(count) => + static String m13(count) => "${Intl.plural(count, one: 'Voeg kijker toe', other: 'Voeg kijkers toe')}"; - static String m13(emailOrName) => "Toegevoegd door ${emailOrName}"; + static String m14(emailOrName) => "Toegevoegd door ${emailOrName}"; - static String m14(albumName) => "Succesvol toegevoegd aan ${albumName}"; + static String m15(albumName) => "Succesvol toegevoegd aan ${albumName}"; - static String m15(count) => + static String m16(count) => "${Intl.plural(count, zero: 'Geen deelnemers', one: '1 deelnemer', other: '${count} deelnemers')}"; - static String m16(versionValue) => "Versie: ${versionValue}"; + static String m17(versionValue) => "Versie: ${versionValue}"; - static String m17(freeAmount, storageUnit) => + static String m18(freeAmount, storageUnit) => "${freeAmount} ${storageUnit} vrij"; - static String m18(paymentProvider) => + static String m19(paymentProvider) => "Annuleer eerst uw bestaande abonnement bij ${paymentProvider}"; static String m3(user) => "${user} zal geen foto\'s meer kunnen toevoegen aan dit album\n\nDe gebruiker zal nog steeds bestaande foto\'s kunnen verwijderen die door hen zijn toegevoegd"; - static String m19(isFamilyMember, storageAmountInGb) => + static String m20(isFamilyMember, storageAmountInGb) => "${Intl.select(isFamilyMember, { 'true': 'Jouw familie heeft ${storageAmountInGb} GB geclaimd tot nu toe', @@ -58,214 +60,223 @@ class MessageLookup extends MessageLookupByLibrary { 'other': 'Je hebt ${storageAmountInGb} GB geclaimd tot nu toe!', })}"; - static String m20(albumName) => + static String m21(albumName) => "Gezamenlijke link aangemaakt voor ${albumName}"; - static String m21(count) => + static String m22(count) => "${Intl.plural(count, zero: '0 samenwerkers toegevoegd', one: '1 samenwerker toegevoegd', other: '${count} samenwerkers toegevoegd')}"; - static String m22(email, numOfDays) => + static String m23(email, numOfDays) => "Je staat op het punt ${email} toe te voegen als vertrouwde contactpersoon. Ze kunnen je account herstellen als je ${numOfDays} dagen afwezig bent."; - static String m23(familyAdminEmail) => + static String m24(familyAdminEmail) => "Neem contact op met ${familyAdminEmail} om uw abonnement te beheren"; - static String m24(provider) => + static String m25(provider) => "Neem contact met ons op via support@ente.io om uw ${provider} abonnement te beheren."; - static String m25(endpoint) => "Verbonden met ${endpoint}"; + static String m26(endpoint) => "Verbonden met ${endpoint}"; - static String m26(count) => + static String m27(count) => "${Intl.plural(count, one: 'Verwijder ${count} bestand', other: 'Verwijder ${count} bestanden')}"; - static String m27(currentlyDeleting, totalCount) => + static String m28(currentlyDeleting, totalCount) => "Verwijderen van ${currentlyDeleting} / ${totalCount}"; - static String m28(albumName) => + static String m29(albumName) => "Dit verwijdert de openbare link voor toegang tot \"${albumName}\"."; - static String m29(supportEmail) => + static String m30(supportEmail) => "Stuur een e-mail naar ${supportEmail} vanaf het door jou geregistreerde e-mailadres"; - static String m30(count, storageSaved) => + static String m31(count, storageSaved) => "Je hebt ${Intl.plural(count, one: '${count} dubbel bestand', other: '${count} dubbele bestanden')} opgeruimd, totaal (${storageSaved}!)"; - static String m31(count, formattedSize) => + static String m32(count, formattedSize) => "${count} bestanden, elk ${formattedSize}"; - static String m32(newEmail) => "E-mailadres gewijzigd naar ${newEmail}"; + static String m33(newEmail) => "E-mailadres gewijzigd naar ${newEmail}"; - static String m33(email) => + static String m34(email) => "${email} heeft geen Ente account."; + + static String m35(email) => "${email} heeft geen Ente account.\n\nStuur ze een uitnodiging om foto\'s te delen."; - static String m34(text) => "Extra foto\'s gevonden voor ${text}"; + static String m36(text) => "Extra foto\'s gevonden voor ${text}"; - static String m35(count, formattedNumber) => + static String m37(count, formattedNumber) => "${Intl.plural(count, one: '1 bestand', other: '${formattedNumber} bestanden')} in dit album zijn veilig geback-upt"; - static String m36(count, formattedNumber) => + static String m38(count, formattedNumber) => "${Intl.plural(count, one: '1 bestand', other: '${formattedNumber} bestanden')} in dit album is veilig geback-upt"; static String m4(storageAmountInGB) => "${storageAmountInGB} GB telkens als iemand zich aanmeldt voor een betaald abonnement en je code toepast"; - static String m37(endDate) => "Gratis proefversie geldig tot ${endDate}"; + static String m39(endDate) => "Gratis proefversie geldig tot ${endDate}"; - static String m38(count) => + static String m40(count) => "Je hebt nog steeds toegang tot ${Intl.plural(count, one: 'het', other: 'ze')} op Ente zolang je een actief abonnement hebt"; - static String m39(sizeInMBorGB) => "Maak ${sizeInMBorGB} vrij"; + static String m41(sizeInMBorGB) => "Maak ${sizeInMBorGB} vrij"; - static String m40(count, formattedSize) => + static String m42(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) => + static String m43(currentlyProcessing, totalCount) => "Verwerken van ${currentlyProcessing} / ${totalCount}"; - static String m42(count) => + static String m44(count) => "${Intl.plural(count, one: '${count} item', other: '${count} items')}"; - static String m43(email) => + static String m45(email) => "${email} heeft je uitgenodigd om een vertrouwd contact te zijn"; - static String m44(expiryTime) => "Link vervalt op ${expiryTime}"; + static String m46(expiryTime) => "Link vervalt op ${expiryTime}"; + + static String m47(email) => "Link persoon aan ${email}"; + + static String m48(personName, email) => + "Dit linkt ${personName} aan ${email}"; static String m5(count, formattedCount) => "${Intl.plural(count, zero: 'geen herinneringen', one: '${formattedCount} herinnering', other: '${formattedCount} herinneringen')}"; - static String m45(count) => + static String m49(count) => "${Intl.plural(count, one: 'Bestand verplaatsen', other: 'Bestanden verplaatsen')}"; - static String m46(albumName) => "Succesvol verplaatst naar ${albumName}"; + static String m50(albumName) => "Succesvol verplaatst naar ${albumName}"; - static String m47(personName) => "Geen suggesties voor ${personName}"; + static String m51(personName) => "Geen suggesties voor ${personName}"; - static String m48(name) => "Niet ${name}?"; + static String m52(name) => "Niet ${name}?"; - static String m49(familyAdminEmail) => + static String m53(familyAdminEmail) => "Neem contact op met ${familyAdminEmail} om uw code te wijzigen."; static String m0(passwordStrengthValue) => "Wachtwoord sterkte: ${passwordStrengthValue}"; - static String m50(providerName) => + static String m54(providerName) => "Praat met ${providerName} klantenservice als u in rekening bent gebracht"; - static String m51(count) => + static String m55(count) => "${Intl.plural(count, zero: '0 foto\'s', one: '1 foto', other: '${count} foto\'s')}"; - static String m52(endDate) => + static String m56(endDate) => "Gratis proefperiode geldig tot ${endDate}.\nU kunt naderhand een betaald abonnement kiezen."; - static String m53(toEmail) => "Stuur ons een e-mail op ${toEmail}"; + static String m57(toEmail) => "Stuur ons een e-mail op ${toEmail}"; - static String m54(toEmail) => + static String m58(toEmail) => "Verstuur de logboeken alstublieft naar ${toEmail}"; - static String m55(folderName) => "Verwerken van ${folderName}..."; + static String m59(folderName) => "Verwerken van ${folderName}..."; - static String m56(storeName) => "Beoordeel ons op ${storeName}"; + static String m60(storeName) => "Beoordeel ons op ${storeName}"; - static String m57(days, email) => + static String m61(name) => "Toegewezen aan ${name}"; + + static String m62(days, email) => "U krijgt toegang tot het account na ${days} dagen. Een melding zal worden verzonden naar ${email}."; - static String m58(email) => + static String m63(email) => "U kunt nu het account van ${email} herstellen door een nieuw wachtwoord in te stellen."; - static String m59(email) => "${email} probeert je account te herstellen."; + static String m64(email) => "${email} probeert je account te herstellen."; - static String m60(storageInGB) => + static String m65(storageInGB) => "Jullie krijgen allebei ${storageInGB} GB* gratis"; - static String m61(userEmail) => + static String m66(userEmail) => "${userEmail} zal worden verwijderd uit dit gedeelde album\n\nAlle door hen toegevoegde foto\'s worden ook uit het album verwijderd"; - static String m62(endDate) => "Wordt verlengd op ${endDate}"; + static String m67(endDate) => "Wordt verlengd op ${endDate}"; - static String m63(count) => + static String m68(count) => "${Intl.plural(count, one: '${count} resultaat gevonden', other: '${count} resultaten gevonden')}"; - static String m64(snapshotLength, searchLength) => + static String m69(snapshotLength, searchLength) => "Lengte van secties komt niet overeen: ${snapshotLength} != ${searchLength}"; static String m6(count) => "${count} geselecteerd"; - static String m65(count, yourCount) => + static String m70(count, yourCount) => "${count} geselecteerd (${yourCount} van jou)"; - static String m66(verificationID) => + static String m71(verificationID) => "Hier is mijn verificatie-ID: ${verificationID} voor ente.io."; static String m7(verificationID) => "Hey, kunt u bevestigen dat dit uw ente.io verificatie-ID is: ${verificationID}"; - static String m67(referralCode, referralStorageInGB) => + static String m72(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 m68(numberOfPeople) => + static String m73(numberOfPeople) => "${Intl.plural(numberOfPeople, zero: 'Deel met specifieke mensen', one: 'Gedeeld met 1 persoon', other: 'Gedeeld met ${numberOfPeople} mensen')}"; - static String m69(emailIDs) => "Gedeeld met ${emailIDs}"; + static String m74(emailIDs) => "Gedeeld met ${emailIDs}"; - static String m70(fileType) => + static String m75(fileType) => "Deze ${fileType} zal worden verwijderd van jouw apparaat."; - static String m71(fileType) => + static String m76(fileType) => "Deze ${fileType} staat zowel in Ente als op jouw apparaat."; - static String m72(fileType) => + static String m77(fileType) => "Deze ${fileType} zal worden verwijderd uit Ente."; static String m1(storageAmountInGB) => "${storageAmountInGB} GB"; - static String m73( + static String m78( usedAmount, usedStorageUnit, totalAmount, totalStorageUnit) => "${usedAmount} ${usedStorageUnit} van ${totalAmount} ${totalStorageUnit} gebruikt"; - static String m74(id) => + static String m79(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 m75(endDate) => "Uw abonnement loopt af op ${endDate}"; + static String m80(endDate) => "Uw abonnement loopt af op ${endDate}"; - static String m76(completed, total) => + static String m81(completed, total) => "${completed}/${total} herinneringen bewaard"; - static String m77(ignoreReason) => + static String m82(ignoreReason) => "Tik om te uploaden, upload wordt momenteel genegeerd vanwege ${ignoreReason}"; static String m8(storageAmountInGB) => "Zij krijgen ook ${storageAmountInGB} GB"; - static String m78(email) => "Dit is de verificatie-ID van ${email}"; + static String m83(email) => "Dit is de verificatie-ID van ${email}"; - static String m79(count) => + static String m84(count) => "${Intl.plural(count, zero: 'Binnenkort', one: '1 dag', other: '${count} dagen')}"; - static String m80(email) => + static String m85(email) => "Je bent uitgenodigd om een legacy contact van ${email} te zijn."; - static String m81(galleryType) => + static String m86(galleryType) => "Galerijtype ${galleryType} wordt niet ondersteund voor hernoemen"; - static String m82(ignoreReason) => + static String m87(ignoreReason) => "Upload wordt genegeerd omdat ${ignoreReason}"; - static String m83(count) => "${count} herinneringen veiligstellen..."; + static String m88(count) => "${count} herinneringen veiligstellen..."; - static String m84(endDate) => "Geldig tot ${endDate}"; + static String m89(endDate) => "Geldig tot ${endDate}"; - static String m85(email) => "Verifieer ${email}"; + static String m90(email) => "Verifieer ${email}"; - static String m86(count) => + static String m91(count) => "${Intl.plural(count, zero: '0 kijkers toegevoegd', one: '1 kijker toegevoegd', other: '${count} kijkers toegevoegd')}"; static String m2(email) => "We hebben een e-mail gestuurd naar ${email}"; - static String m87(count) => + static String m92(count) => "${Intl.plural(count, one: '${count} jaar geleden', other: '${count} jaar geleden')}"; - static String m88(storageSaved) => + static String m93(storageSaved) => "Je hebt ${storageSaved} succesvol vrijgemaakt!"; final messages = _notInlinedMessages(_notInlinedMessages); @@ -278,6 +289,7 @@ class MessageLookup extends MessageLookupByLibrary { "account": MessageLookupByLibrary.simpleMessage("Account"), "accountIsAlreadyConfigured": MessageLookupByLibrary.simpleMessage( "Account is al geconfigureerd."), + "accountOwnerPersonAppbarTitle": m9, "accountWelcomeBack": MessageLookupByLibrary.simpleMessage("Welkom terug!"), "ackPasswordLostWarning": MessageLookupByLibrary.simpleMessage( @@ -290,11 +302,11 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Nieuw e-mailadres toevoegen"), "addCollaborator": MessageLookupByLibrary.simpleMessage("Samenwerker toevoegen"), - "addCollaborators": m9, + "addCollaborators": m10, "addFiles": MessageLookupByLibrary.simpleMessage("Bestanden toevoegen"), "addFromDevice": MessageLookupByLibrary.simpleMessage("Toevoegen vanaf apparaat"), - "addItem": m10, + "addItem": m11, "addLocation": MessageLookupByLibrary.simpleMessage("Locatie toevoegen"), "addLocationButton": MessageLookupByLibrary.simpleMessage("Toevoegen"), @@ -307,7 +319,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Nieuw persoon toevoegen"), "addOnPageSubtitle": MessageLookupByLibrary.simpleMessage("Details van add-ons"), - "addOnValidTill": m11, + "addOnValidTill": m12, "addOns": MessageLookupByLibrary.simpleMessage("Add-ons"), "addPhotos": MessageLookupByLibrary.simpleMessage("Foto\'s toevoegen"), "addSelected": @@ -320,12 +332,12 @@ class MessageLookup extends MessageLookupByLibrary { "addTrustedContact": MessageLookupByLibrary.simpleMessage("Vertrouwd contact toevoegen"), "addViewer": MessageLookupByLibrary.simpleMessage("Voeg kijker toe"), - "addViewers": m12, + "addViewers": m13, "addYourPhotosNow": MessageLookupByLibrary.simpleMessage("Voeg nu je foto\'s toe"), "addedAs": MessageLookupByLibrary.simpleMessage("Toegevoegd als"), - "addedBy": m13, - "addedSuccessfullyTo": m14, + "addedBy": m14, + "addedSuccessfullyTo": m15, "addingToFavorites": MessageLookupByLibrary.simpleMessage("Toevoegen aan favorieten..."), "advanced": MessageLookupByLibrary.simpleMessage("Geavanceerd"), @@ -336,7 +348,7 @@ class MessageLookup extends MessageLookupByLibrary { "after1Week": MessageLookupByLibrary.simpleMessage("Na 1 week"), "after1Year": MessageLookupByLibrary.simpleMessage("Na 1 jaar"), "albumOwner": MessageLookupByLibrary.simpleMessage("Eigenaar"), - "albumParticipantsCount": m15, + "albumParticipantsCount": m16, "albumTitle": MessageLookupByLibrary.simpleMessage("Albumtitel"), "albumUpdated": MessageLookupByLibrary.simpleMessage("Album bijgewerkt"), @@ -384,7 +396,7 @@ class MessageLookup extends MessageLookupByLibrary { "appLock": MessageLookupByLibrary.simpleMessage("App-vergrendeling"), "appLockDescriptions": MessageLookupByLibrary.simpleMessage( "Kies tussen het standaard vergrendelscherm van uw apparaat en een aangepast vergrendelscherm met een pincode of wachtwoord."), - "appVersion": m16, + "appVersion": m17, "appleId": MessageLookupByLibrary.simpleMessage("Apple ID"), "apply": MessageLookupByLibrary.simpleMessage("Toepassen"), "applyCodeTitle": @@ -469,7 +481,7 @@ class MessageLookup extends MessageLookupByLibrary { "autoPairDesc": MessageLookupByLibrary.simpleMessage( "Automatisch koppelen werkt alleen met apparaten die Chromecast ondersteunen."), "available": MessageLookupByLibrary.simpleMessage("Beschikbaar"), - "availableStorageSpace": m17, + "availableStorageSpace": m18, "backedUpFolders": MessageLookupByLibrary.simpleMessage("Back-up mappen"), "backup": MessageLookupByLibrary.simpleMessage("Back-up"), @@ -479,7 +491,7 @@ class MessageLookup extends MessageLookupByLibrary { "Back-up maken via mobiele data"), "backupSettings": MessageLookupByLibrary.simpleMessage("Back-up instellingen"), - "backupStatus": MessageLookupByLibrary.simpleMessage("Back-upstatus"), + "backupStatus": MessageLookupByLibrary.simpleMessage("Back-up status"), "backupStatusDescription": MessageLookupByLibrary.simpleMessage( "Items die zijn geback-upt, worden hier getoond"), "backupVideos": @@ -507,7 +519,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Herstel annuleren"), "cancelAccountRecoveryBody": MessageLookupByLibrary.simpleMessage( "Weet je zeker dat je het herstel wilt annuleren?"), - "cancelOtherSubscription": m18, + "cancelOtherSubscription": m19, "cancelSubscription": MessageLookupByLibrary.simpleMessage("Abonnement opzeggen"), "cannotAddMorePhotosAfterBecomingViewer": m3, @@ -559,7 +571,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Claim gratis opslag"), "claimMore": MessageLookupByLibrary.simpleMessage("Claim meer!"), "claimed": MessageLookupByLibrary.simpleMessage("Geclaimd"), - "claimedStorageSoFar": m19, + "claimedStorageSoFar": m20, "cleanUncategorized": MessageLookupByLibrary.simpleMessage("Ongecategoriseerd opschonen"), "cleanUncategorizedDescription": MessageLookupByLibrary.simpleMessage( @@ -588,12 +600,12 @@ class MessageLookup extends MessageLookupByLibrary { "Maak een link waarmee mensen foto\'s in jouw gedeelde album kunnen toevoegen en bekijken zonder dat ze daarvoor een Ente app of account nodig hebben. Handig voor het verzamelen van foto\'s van evenementen."), "collaborativeLink": MessageLookupByLibrary.simpleMessage("Gezamenlijke link"), - "collaborativeLinkCreatedFor": m20, + "collaborativeLinkCreatedFor": m21, "collaborator": MessageLookupByLibrary.simpleMessage("Samenwerker"), "collaboratorsCanAddPhotosAndVideosToTheSharedAlbum": MessageLookupByLibrary.simpleMessage( "Samenwerkers kunnen foto\'s en video\'s toevoegen aan het gedeelde album."), - "collaboratorsSuccessfullyAdded": m21, + "collaboratorsSuccessfullyAdded": m22, "collageLayout": MessageLookupByLibrary.simpleMessage("Layout"), "collageSaved": MessageLookupByLibrary.simpleMessage( "Collage opgeslagen in gallerij"), @@ -611,7 +623,7 @@ class MessageLookup extends MessageLookupByLibrary { "Weet u zeker dat u tweestapsverificatie wilt uitschakelen?"), "confirmAccountDeletion": MessageLookupByLibrary.simpleMessage( "Account verwijderen bevestigen"), - "confirmAddingTrustedContact": m22, + "confirmAddingTrustedContact": m23, "confirmDeletePrompt": MessageLookupByLibrary.simpleMessage( "Ja, ik wil mijn account en de bijbehorende gegevens verspreid over alle apps permanent verwijderen."), "confirmPassword": @@ -624,10 +636,10 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Bevestig herstelsleutel"), "connectToDevice": MessageLookupByLibrary.simpleMessage( "Verbinding maken met apparaat"), - "contactFamilyAdmin": m23, + "contactFamilyAdmin": m24, "contactSupport": MessageLookupByLibrary.simpleMessage("Contacteer klantenservice"), - "contactToManageSubscription": m24, + "contactToManageSubscription": m25, "contacts": MessageLookupByLibrary.simpleMessage("Contacten"), "contents": MessageLookupByLibrary.simpleMessage("Inhoud"), "continueLabel": MessageLookupByLibrary.simpleMessage("Doorgaan"), @@ -674,7 +686,7 @@ class MessageLookup extends MessageLookupByLibrary { "currentlyRunning": MessageLookupByLibrary.simpleMessage("momenteel bezig"), "custom": MessageLookupByLibrary.simpleMessage("Aangepast"), - "customEndpoint": m25, + "customEndpoint": m26, "darkTheme": MessageLookupByLibrary.simpleMessage("Donker"), "dayToday": MessageLookupByLibrary.simpleMessage("Vandaag"), "dayYesterday": MessageLookupByLibrary.simpleMessage("Gisteren"), @@ -712,12 +724,12 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Verwijder van apparaat"), "deleteFromEnte": MessageLookupByLibrary.simpleMessage("Verwijder van Ente"), - "deleteItemCount": m26, + "deleteItemCount": m27, "deleteLocation": MessageLookupByLibrary.simpleMessage("Verwijder locatie"), "deletePhotos": MessageLookupByLibrary.simpleMessage("Foto\'s verwijderen"), - "deleteProgress": m27, + "deleteProgress": m28, "deleteReason1": MessageLookupByLibrary.simpleMessage( "Ik mis een belangrijke functie"), "deleteReason2": MessageLookupByLibrary.simpleMessage( @@ -758,7 +770,7 @@ class MessageLookup extends MessageLookupByLibrary { "Kijkers kunnen nog steeds screenshots maken of een kopie van je foto\'s opslaan met behulp van externe tools"), "disableDownloadWarningTitle": MessageLookupByLibrary.simpleMessage("Let op"), - "disableLinkMessage": m28, + "disableLinkMessage": m29, "disableTwofactor": MessageLookupByLibrary.simpleMessage( "Tweestapsverificatie uitschakelen"), "disablingTwofactorAuthentication": @@ -794,15 +806,16 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage( "Wilt u de bewerkingen die u hebt gemaakt annuleren?"), "done": MessageLookupByLibrary.simpleMessage("Voltooid"), + "dontSave": MessageLookupByLibrary.simpleMessage("Niet opslaan"), "doubleYourStorage": MessageLookupByLibrary.simpleMessage("Verdubbel uw opslagruimte"), "download": MessageLookupByLibrary.simpleMessage("Downloaden"), "downloadFailed": MessageLookupByLibrary.simpleMessage("Download mislukt"), "downloading": MessageLookupByLibrary.simpleMessage("Downloaden..."), - "dropSupportEmail": m29, - "duplicateFileCountWithStorageSaved": m30, - "duplicateItemsGroup": m31, + "dropSupportEmail": m30, + "duplicateFileCountWithStorageSaved": m31, + "duplicateItemsGroup": m32, "edit": MessageLookupByLibrary.simpleMessage("Bewerken"), "editLocation": MessageLookupByLibrary.simpleMessage("Locatie bewerken"), @@ -818,8 +831,9 @@ class MessageLookup extends MessageLookupByLibrary { "email": MessageLookupByLibrary.simpleMessage("E-mail"), "emailAlreadyRegistered": MessageLookupByLibrary.simpleMessage("E-mail is al geregistreerd."), - "emailChangedTo": m32, - "emailNoEnteAccount": m33, + "emailChangedTo": m33, + "emailDoesNotHaveEnteAccount": m34, + "emailNoEnteAccount": m35, "emailNotRegistered": MessageLookupByLibrary.simpleMessage("E-mail niet geregistreerd."), "emailVerificationToggle": @@ -888,7 +902,7 @@ class MessageLookup extends MessageLookupByLibrary { "enterValidEmail": MessageLookupByLibrary.simpleMessage( "Voer een geldig e-mailadres in."), "enterYourEmailAddress": - MessageLookupByLibrary.simpleMessage("Voer uw e-mailadres in"), + MessageLookupByLibrary.simpleMessage("Voer je e-mailadres in"), "enterYourPassword": MessageLookupByLibrary.simpleMessage("Voer je wachtwoord in"), "enterYourRecoveryKey": @@ -906,12 +920,13 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Exporteer je gegevens"), "extraPhotosFound": MessageLookupByLibrary.simpleMessage("Extra foto\'s gevonden"), - "extraPhotosFoundFor": m34, + "extraPhotosFoundFor": m36, "faceNotClusteredYet": MessageLookupByLibrary.simpleMessage( "Gezicht nog niet geclusterd, kom later terug"), "faceRecognition": MessageLookupByLibrary.simpleMessage("Gezichtsherkenning"), "faces": MessageLookupByLibrary.simpleMessage("Gezichten"), + "failed": MessageLookupByLibrary.simpleMessage("Mislukt"), "failedToApplyCode": MessageLookupByLibrary.simpleMessage("Code toepassen mislukt"), "failedToCancel": @@ -958,8 +973,8 @@ class MessageLookup extends MessageLookupByLibrary { "fileTypes": MessageLookupByLibrary.simpleMessage("Bestandstype"), "fileTypesAndNames": MessageLookupByLibrary.simpleMessage("Bestandstypen en namen"), - "filesBackedUpFromDevice": m35, - "filesBackedUpInAlbum": m36, + "filesBackedUpFromDevice": m37, + "filesBackedUpInAlbum": m38, "filesDeleted": MessageLookupByLibrary.simpleMessage("Bestanden verwijderd"), "filesSavedToGallery": MessageLookupByLibrary.simpleMessage( @@ -980,22 +995,22 @@ class MessageLookup extends MessageLookupByLibrary { "freeStorageUsable": MessageLookupByLibrary.simpleMessage("Gratis opslag bruikbaar"), "freeTrial": MessageLookupByLibrary.simpleMessage("Gratis proefversie"), - "freeTrialValidTill": m37, - "freeUpAccessPostDelete": m38, - "freeUpAmount": m39, + "freeTrialValidTill": m39, + "freeUpAccessPostDelete": m40, + "freeUpAmount": m41, "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, + "freeUpSpaceSaving": m42, "gallery": MessageLookupByLibrary.simpleMessage("Galerij"), "galleryMemoryLimitInfo": MessageLookupByLibrary.simpleMessage( "Tot 1000 herinneringen getoond in de galerij"), "general": MessageLookupByLibrary.simpleMessage("Algemeen"), "generatingEncryptionKeys": MessageLookupByLibrary.simpleMessage( "Encryptiesleutels genereren..."), - "genericProgress": m41, + "genericProgress": m43, "goToSettings": MessageLookupByLibrary.simpleMessage("Ga naar instellingen"), "googlePlayId": MessageLookupByLibrary.simpleMessage("Google Play ID"), @@ -1054,6 +1069,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Geïndexeerde bestanden"), "indexingIsPaused": MessageLookupByLibrary.simpleMessage( "Indexeren is gepauzeerd. Het zal automatisch hervatten wanneer het apparaat klaar is."), + "ineligible": MessageLookupByLibrary.simpleMessage("Ongerechtigd"), "info": MessageLookupByLibrary.simpleMessage("Info"), "insecureDevice": MessageLookupByLibrary.simpleMessage("Onveilig apparaat"), @@ -1078,7 +1094,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": m44, "itemsShowTheNumberOfDaysRemainingBeforePermanentDeletion": MessageLookupByLibrary.simpleMessage( "Bestanden tonen het aantal resterende dagen voordat ze permanent worden verwijderd"), @@ -1087,6 +1103,8 @@ class MessageLookup extends MessageLookupByLibrary { "join": MessageLookupByLibrary.simpleMessage("Deelnemen"), "joinAlbum": MessageLookupByLibrary.simpleMessage("Deelnemen aan album"), + "joinAlbumConfirmationDialogBody": MessageLookupByLibrary.simpleMessage( + "Deelnemen aan een album maakt je e-mail zichtbaar voor de deelnemers."), "joinAlbumSubtext": MessageLookupByLibrary.simpleMessage( "om je foto\'s te bekijken en toe te voegen"), "joinAlbumSubtextViewer": MessageLookupByLibrary.simpleMessage( @@ -1108,24 +1126,33 @@ class MessageLookup extends MessageLookupByLibrary { "legacy": MessageLookupByLibrary.simpleMessage("Legacy"), "legacyAccounts": MessageLookupByLibrary.simpleMessage("Legacy accounts"), - "legacyInvite": m43, + "legacyInvite": m45, "legacyPageDesc": MessageLookupByLibrary.simpleMessage( "Legacy geeft vertrouwde contacten toegang tot je account bij afwezigheid."), "legacyPageDesc2": MessageLookupByLibrary.simpleMessage( "Vertrouwde contacten kunnen accountherstel starten, en indien deze niet binnen 30 dagen wordt geblokkeerd, je wachtwoord resetten en toegang krijgen tot je account."), "light": MessageLookupByLibrary.simpleMessage("Licht"), "lightTheme": MessageLookupByLibrary.simpleMessage("Licht"), + "link": MessageLookupByLibrary.simpleMessage("Link"), "linkCopiedToClipboard": MessageLookupByLibrary.simpleMessage( "Link gekopieerd naar klembord"), "linkDeviceLimit": MessageLookupByLibrary.simpleMessage("Apparaat limiet"), + "linkEmail": MessageLookupByLibrary.simpleMessage("Link email"), + "linkEmailToContactBannerCaption": + MessageLookupByLibrary.simpleMessage("voor sneller delen"), "linkEnabled": MessageLookupByLibrary.simpleMessage("Ingeschakeld"), "linkExpired": MessageLookupByLibrary.simpleMessage("Verlopen"), - "linkExpiresOn": m44, + "linkExpiresOn": m46, "linkExpiry": MessageLookupByLibrary.simpleMessage("Vervaldatum"), "linkHasExpired": MessageLookupByLibrary.simpleMessage("Link is vervallen"), "linkNeverExpires": MessageLookupByLibrary.simpleMessage("Nooit"), + "linkPerson": MessageLookupByLibrary.simpleMessage("Link persoon"), + "linkPersonCaption": MessageLookupByLibrary.simpleMessage( + "voor een betere ervaring met delen"), + "linkPersonToEmail": m47, + "linkPersonToEmailConfirmation": m48, "livePhotos": MessageLookupByLibrary.simpleMessage("Live foto"), "loadMessage1": MessageLookupByLibrary.simpleMessage( "U kunt uw abonnement met uw familie delen"), @@ -1214,6 +1241,7 @@ class MessageLookup extends MessageLookupByLibrary { "maps": MessageLookupByLibrary.simpleMessage("Kaarten"), "mastodon": MessageLookupByLibrary.simpleMessage("Mastodon"), "matrix": MessageLookupByLibrary.simpleMessage("Matrix"), + "me": MessageLookupByLibrary.simpleMessage("Ik"), "memoryCount": m5, "merchandise": MessageLookupByLibrary.simpleMessage("Merchandise"), "mergeWithExisting": @@ -1244,12 +1272,12 @@ class MessageLookup extends MessageLookupByLibrary { "moreDetails": MessageLookupByLibrary.simpleMessage("Meer details"), "mostRecent": MessageLookupByLibrary.simpleMessage("Meest recent"), "mostRelevant": MessageLookupByLibrary.simpleMessage("Meest relevant"), - "moveItem": m45, + "moveItem": m49, "moveToAlbum": MessageLookupByLibrary.simpleMessage("Verplaats naar album"), "moveToHiddenAlbum": MessageLookupByLibrary.simpleMessage( "Verplaatsen naar verborgen album"), - "movedSuccessfullyTo": m46, + "movedSuccessfullyTo": m50, "movedToTrash": MessageLookupByLibrary.simpleMessage("Naar prullenbak verplaatst"), "movingFilesToAlbum": MessageLookupByLibrary.simpleMessage( @@ -1277,6 +1305,8 @@ class MessageLookup extends MessageLookupByLibrary { "Je hebt geen bestanden op dit apparaat die verwijderd kunnen worden"), "noDuplicates": MessageLookupByLibrary.simpleMessage("✨ Geen duplicaten"), + "noEnteAccountExclamation": + MessageLookupByLibrary.simpleMessage("Geen Ente account!"), "noExifData": MessageLookupByLibrary.simpleMessage("Geen EXIF gegevens"), "noFacesFound": @@ -1301,10 +1331,10 @@ class MessageLookup extends MessageLookupByLibrary { "noResults": MessageLookupByLibrary.simpleMessage("Geen resultaten"), "noResultsFound": MessageLookupByLibrary.simpleMessage("Geen resultaten gevonden"), - "noSuggestionsForPerson": m47, + "noSuggestionsForPerson": m51, "noSystemLockFound": MessageLookupByLibrary.simpleMessage( "Geen systeemvergrendeling gevonden"), - "notPersonLabel": m48, + "notPersonLabel": m52, "nothingSharedWithYouYet": MessageLookupByLibrary.simpleMessage("Nog niets met je gedeeld"), "nothingToSeeHere": @@ -1314,7 +1344,7 @@ class MessageLookup extends MessageLookupByLibrary { "onDevice": MessageLookupByLibrary.simpleMessage("Op het apparaat"), "onEnte": MessageLookupByLibrary.simpleMessage( "Op ente"), - "onlyFamilyAdminCanChangeCode": m49, + "onlyFamilyAdminCanChangeCode": m53, "onlyThem": MessageLookupByLibrary.simpleMessage("Alleen hen"), "oops": MessageLookupByLibrary.simpleMessage("Oeps"), "oopsCouldNotSaveEdits": MessageLookupByLibrary.simpleMessage( @@ -1338,7 +1368,7 @@ class MessageLookup extends MessageLookupByLibrary { "orPickAnExistingOne": MessageLookupByLibrary.simpleMessage("Of kies een bestaande"), "orPickFromYourContacts": - MessageLookupByLibrary.simpleMessage("or pick from your contacts"), + MessageLookupByLibrary.simpleMessage("of kies uit je contacten"), "pair": MessageLookupByLibrary.simpleMessage("Koppelen"), "pairWithPin": MessageLookupByLibrary.simpleMessage("Koppelen met PIN"), "pairingComplete": @@ -1364,7 +1394,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": m50, + "paymentFailedTalkToProvider": m54, "pendingItems": MessageLookupByLibrary.simpleMessage("Bestanden in behandeling"), "pendingSync": MessageLookupByLibrary.simpleMessage( @@ -1388,7 +1418,7 @@ class MessageLookup extends MessageLookupByLibrary { "photosAddedByYouWillBeRemovedFromTheAlbum": MessageLookupByLibrary.simpleMessage( "Foto\'s toegevoegd door u zullen worden verwijderd uit het album"), - "photosCount": m51, + "photosCount": m55, "pickCenterPoint": MessageLookupByLibrary.simpleMessage("Kies middelpunt"), "pinAlbum": @@ -1396,7 +1426,10 @@ class MessageLookup extends MessageLookupByLibrary { "pinLock": MessageLookupByLibrary.simpleMessage("PIN vergrendeling"), "playOnTv": MessageLookupByLibrary.simpleMessage("Album afspelen op TV"), - "playStoreFreeTrialValidTill": m52, + "playOriginal": + MessageLookupByLibrary.simpleMessage("Origineel afspelen"), + "playStoreFreeTrialValidTill": m56, + "playStream": MessageLookupByLibrary.simpleMessage("Stream afspelen"), "playstoreSubscription": MessageLookupByLibrary.simpleMessage("PlayStore abonnement"), "pleaseCheckYourInternetConnectionAndTryAgain": @@ -1408,14 +1441,14 @@ class MessageLookup extends MessageLookupByLibrary { "pleaseContactSupportIfTheProblemPersists": MessageLookupByLibrary.simpleMessage( "Neem contact op met klantenservice als het probleem aanhoudt"), - "pleaseEmailUsAt": m53, + "pleaseEmailUsAt": m57, "pleaseGrantPermissions": MessageLookupByLibrary.simpleMessage( "Geef alstublieft toestemming"), "pleaseLoginAgain": MessageLookupByLibrary.simpleMessage("Log opnieuw in"), "pleaseSelectQuickLinksToRemove": MessageLookupByLibrary.simpleMessage( "Selecteer snelle links om te verwijderen"), - "pleaseSendTheLogsTo": m54, + "pleaseSendTheLogsTo": m58, "pleaseTryAgain": MessageLookupByLibrary.simpleMessage("Probeer het nog eens"), "pleaseVerifyTheCodeYouHaveEntered": @@ -1443,17 +1476,26 @@ class MessageLookup extends MessageLookupByLibrary { "privateSharing": MessageLookupByLibrary.simpleMessage("Privé delen"), "proceed": MessageLookupByLibrary.simpleMessage("Verder"), "processed": MessageLookupByLibrary.simpleMessage("Verwerkt"), - "processingImport": m55, + "processing": MessageLookupByLibrary.simpleMessage("Verwerken"), + "processingImport": m59, + "processingVideos": + MessageLookupByLibrary.simpleMessage("Video\'s verwerken"), "publicLinkCreated": MessageLookupByLibrary.simpleMessage("Publieke link aangemaakt"), "publicLinkEnabled": MessageLookupByLibrary.simpleMessage("Publieke link ingeschakeld"), + "queued": MessageLookupByLibrary.simpleMessage("In wachtrij"), "quickLinks": MessageLookupByLibrary.simpleMessage("Snelle links"), "radius": MessageLookupByLibrary.simpleMessage("Straal"), "raiseTicket": MessageLookupByLibrary.simpleMessage("Meld probleem"), "rateTheApp": MessageLookupByLibrary.simpleMessage("Beoordeel de app"), "rateUs": MessageLookupByLibrary.simpleMessage("Beoordeel ons"), - "rateUsOnStore": m56, + "rateUsOnStore": m60, + "reassignMe": + MessageLookupByLibrary.simpleMessage("\"Ik\" opnieuw toewijzen"), + "reassignedToName": m61, + "reassigningLoading": + MessageLookupByLibrary.simpleMessage("Opnieuw toewijzen..."), "recover": MessageLookupByLibrary.simpleMessage("Herstellen"), "recoverAccount": MessageLookupByLibrary.simpleMessage("Account herstellen"), @@ -1462,7 +1504,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Account herstellen"), "recoveryInitiated": MessageLookupByLibrary.simpleMessage("Herstel gestart"), - "recoveryInitiatedDesc": m57, + "recoveryInitiatedDesc": m62, "recoveryKey": MessageLookupByLibrary.simpleMessage("Herstelsleutel"), "recoveryKeyCopiedToClipboard": MessageLookupByLibrary.simpleMessage( "Herstelsleutel gekopieerd naar klembord"), @@ -1476,12 +1518,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": m58, + "recoveryReady": m63, "recoverySuccessful": MessageLookupByLibrary.simpleMessage("Herstel succesvol!"), "recoveryWarning": MessageLookupByLibrary.simpleMessage( "Een vertrouwd contact probeert toegang te krijgen tot je account"), - "recoveryWarningBody": m59, + "recoveryWarningBody": m64, "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( @@ -1497,7 +1539,7 @@ class MessageLookup extends MessageLookupByLibrary { "1. Geef deze code aan je vrienden"), "referralStep2": MessageLookupByLibrary.simpleMessage( "2. Ze registreren voor een betaald plan"), - "referralStep3": m60, + "referralStep3": m65, "referrals": MessageLookupByLibrary.simpleMessage("Referenties"), "referralsAreCurrentlyPaused": MessageLookupByLibrary.simpleMessage( "Verwijzingen zijn momenteel gepauzeerd"), @@ -1529,7 +1571,7 @@ class MessageLookup extends MessageLookupByLibrary { "removeLink": MessageLookupByLibrary.simpleMessage("Verwijder link"), "removeParticipant": MessageLookupByLibrary.simpleMessage("Deelnemer verwijderen"), - "removeParticipantBody": m61, + "removeParticipantBody": m66, "removePersonLabel": MessageLookupByLibrary.simpleMessage("Verwijder persoonslabel"), "removePublicLink": @@ -1551,7 +1593,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Bestandsnaam wijzigen"), "renewSubscription": MessageLookupByLibrary.simpleMessage("Abonnement verlengen"), - "renewsOn": m62, + "renewsOn": m67, "reportABug": MessageLookupByLibrary.simpleMessage("Een fout melden"), "reportBug": MessageLookupByLibrary.simpleMessage("Fout melden"), "resendEmail": @@ -1583,6 +1625,9 @@ class MessageLookup extends MessageLookupByLibrary { "safelyStored": MessageLookupByLibrary.simpleMessage("Veilig opgeslagen"), "save": MessageLookupByLibrary.simpleMessage("Opslaan"), + "saveChangesBeforeLeavingQuestion": + MessageLookupByLibrary.simpleMessage( + "Wijzigingen opslaan voor verlaten?"), "saveCollage": MessageLookupByLibrary.simpleMessage("Sla collage op"), "saveCopy": MessageLookupByLibrary.simpleMessage("Kopie opslaan"), "saveKey": MessageLookupByLibrary.simpleMessage("Bewaar sleutel"), @@ -1629,8 +1674,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": m63, - "searchSectionsLengthMismatch": m64, + "searchResultCount": m68, + "searchSectionsLengthMismatch": m69, "security": MessageLookupByLibrary.simpleMessage("Beveiliging"), "seePublicAlbumLinksInApp": MessageLookupByLibrary.simpleMessage( "Bekijk publieke album links in de app"), @@ -1653,7 +1698,11 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Selecteer mail app"), "selectMorePhotos": MessageLookupByLibrary.simpleMessage("Selecteer meer foto\'s"), + "selectPersonToLink": + MessageLookupByLibrary.simpleMessage("Kies persoon om te linken"), "selectReason": MessageLookupByLibrary.simpleMessage("Selecteer reden"), + "selectYourFace": + MessageLookupByLibrary.simpleMessage("Selecteer je gezicht"), "selectYourPlan": MessageLookupByLibrary.simpleMessage("Kies uw abonnement"), "selectedFilesAreNotOnEnte": MessageLookupByLibrary.simpleMessage( @@ -1665,7 +1714,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage( "Geselecteerde bestanden worden verwijderd uit alle albums en verplaatst naar de prullenbak."), "selectedPhotos": m6, - "selectedPhotosWithYours": m65, + "selectedPhotosWithYours": m70, "send": MessageLookupByLibrary.simpleMessage("Verzenden"), "sendEmail": MessageLookupByLibrary.simpleMessage("E-mail versturen"), "sendInvite": @@ -1697,16 +1746,16 @@ class MessageLookup extends MessageLookupByLibrary { "shareAnAlbumNow": MessageLookupByLibrary.simpleMessage("Deel nu een album"), "shareLink": MessageLookupByLibrary.simpleMessage("Link delen"), - "shareMyVerificationID": m66, + "shareMyVerificationID": m71, "shareOnlyWithThePeopleYouWant": MessageLookupByLibrary.simpleMessage( "Deel alleen met de mensen die u wilt"), "shareTextConfirmOthersVerificationID": m7, "shareTextRecommendUsingEnte": MessageLookupByLibrary.simpleMessage( "Download Ente zodat we gemakkelijk foto\'s en video\'s in originele kwaliteit kunnen delen\n\nhttps://ente.io"), - "shareTextReferralCode": m67, + "shareTextReferralCode": m72, "shareWithNonenteUsers": MessageLookupByLibrary.simpleMessage( "Delen met niet-Ente gebruikers"), - "shareWithPeopleSectionTitle": m68, + "shareWithPeopleSectionTitle": m73, "shareYourFirstAlbum": MessageLookupByLibrary.simpleMessage("Deel jouw eerste album"), "sharedAlbumSectionDescription": MessageLookupByLibrary.simpleMessage( @@ -1717,7 +1766,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": m69, + "sharedWith": m74, "sharedWithMe": MessageLookupByLibrary.simpleMessage("Gedeeld met mij"), "sharedWithYou": MessageLookupByLibrary.simpleMessage("Gedeeld met jou"), @@ -1733,11 +1782,11 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Log uit op andere apparaten"), "signUpTerms": MessageLookupByLibrary.simpleMessage( "Ik ga akkoord met de gebruiksvoorwaarden en privacybeleid"), - "singleFileDeleteFromDevice": m70, + "singleFileDeleteFromDevice": m75, "singleFileDeleteHighlight": MessageLookupByLibrary.simpleMessage( "Het wordt uit alle albums verwijderd."), - "singleFileInBothLocalAndRemote": m71, - "singleFileInRemoteOnly": m72, + "singleFileInBothLocalAndRemote": m76, + "singleFileInRemoteOnly": m77, "skip": MessageLookupByLibrary.simpleMessage("Overslaan"), "social": MessageLookupByLibrary.simpleMessage("Sociale media"), "someItemsAreInBothEnteAndYourDevice": MessageLookupByLibrary.simpleMessage( @@ -1785,10 +1834,11 @@ class MessageLookup extends MessageLookupByLibrary { "storageInGB": m1, "storageLimitExceeded": MessageLookupByLibrary.simpleMessage("Opslaglimiet overschreden"), - "storageUsageInfo": m73, + "storageUsageInfo": m78, + "streamDetails": MessageLookupByLibrary.simpleMessage("Stream details"), "strongStrength": MessageLookupByLibrary.simpleMessage("Sterk"), - "subAlreadyLinkedErrMessage": m74, - "subWillBeCancelledOn": m75, + "subAlreadyLinkedErrMessage": m79, + "subWillBeCancelledOn": m80, "subscribe": MessageLookupByLibrary.simpleMessage("Abonneer"), "subscribeToEnableSharing": MessageLookupByLibrary.simpleMessage( "Je hebt een actief betaald abonnement nodig om delen mogelijk te maken."), @@ -1805,7 +1855,7 @@ class MessageLookup extends MessageLookupByLibrary { "suggestFeatures": MessageLookupByLibrary.simpleMessage("Features voorstellen"), "support": MessageLookupByLibrary.simpleMessage("Ondersteuning"), - "syncProgress": m76, + "syncProgress": m81, "syncStopped": MessageLookupByLibrary.simpleMessage("Synchronisatie gestopt"), "syncing": MessageLookupByLibrary.simpleMessage("Synchroniseren..."), @@ -1817,7 +1867,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Tik om te ontgrendelen"), "tapToUpload": MessageLookupByLibrary.simpleMessage("Tik om te uploaden"), - "tapToUploadIsIgnoredDue": m77, + "tapToUploadIsIgnoredDue": m82, "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"), @@ -1857,7 +1907,9 @@ class MessageLookup extends MessageLookupByLibrary { "Dit e-mailadres is al in gebruik"), "thisImageHasNoExifData": MessageLookupByLibrary.simpleMessage( "Deze foto heeft geen exif gegevens"), - "thisIsPersonVerificationId": m78, + "thisIsMeExclamation": + MessageLookupByLibrary.simpleMessage("Dit ben ik!"), + "thisIsPersonVerificationId": m83, "thisIsYourVerificationId": MessageLookupByLibrary.simpleMessage("Dit is uw verificatie-ID"), "thisWillLogYouOutOfTheFollowingDevice": @@ -1882,11 +1934,11 @@ class MessageLookup extends MessageLookupByLibrary { "total": MessageLookupByLibrary.simpleMessage("totaal"), "totalSize": MessageLookupByLibrary.simpleMessage("Totale grootte"), "trash": MessageLookupByLibrary.simpleMessage("Prullenbak"), - "trashDaysLeft": m79, + "trashDaysLeft": m84, "trim": MessageLookupByLibrary.simpleMessage("Knippen"), "trustedContacts": MessageLookupByLibrary.simpleMessage("Vertrouwde contacten"), - "trustedInviteBody": m80, + "trustedInviteBody": m85, "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."), @@ -1905,7 +1957,7 @@ class MessageLookup extends MessageLookupByLibrary { "Tweestapsverificatie succesvol gereset"), "twofactorSetup": MessageLookupByLibrary.simpleMessage("Tweestapsverificatie"), - "typeOfGallerGallerytypeIsNotSupportedForRename": m81, + "typeOfGallerGallerytypeIsNotSupportedForRename": m86, "unarchive": MessageLookupByLibrary.simpleMessage("Uit archief halen"), "unarchiveAlbum": MessageLookupByLibrary.simpleMessage("Album uit archief halen"), @@ -1931,10 +1983,10 @@ class MessageLookup extends MessageLookupByLibrary { "updatingFolderSelection": MessageLookupByLibrary.simpleMessage("Map selectie bijwerken..."), "upgrade": MessageLookupByLibrary.simpleMessage("Upgraden"), - "uploadIsIgnoredDueToIgnorereason": m82, + "uploadIsIgnoredDueToIgnorereason": m87, "uploadingFilesToAlbum": MessageLookupByLibrary.simpleMessage( "Bestanden worden geüpload naar album..."), - "uploadingMultipleMemories": m83, + "uploadingMultipleMemories": m88, "uploadingSingleMemory": MessageLookupByLibrary.simpleMessage( "1 herinnering veiligstellen..."), "upto50OffUntil4thDec": MessageLookupByLibrary.simpleMessage( @@ -1952,7 +2004,7 @@ class MessageLookup extends MessageLookupByLibrary { "useSelectedPhoto": MessageLookupByLibrary.simpleMessage("Gebruik geselecteerde foto"), "usedSpace": MessageLookupByLibrary.simpleMessage("Gebruikte ruimte"), - "validTill": m84, + "validTill": m89, "verificationFailedPleaseTryAgain": MessageLookupByLibrary.simpleMessage( "Verificatie mislukt, probeer het opnieuw"), @@ -1960,7 +2012,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Verificatie ID"), "verify": MessageLookupByLibrary.simpleMessage("Verifiëren"), "verifyEmail": MessageLookupByLibrary.simpleMessage("Bevestig e-mail"), - "verifyEmailID": m85, + "verifyEmailID": m90, "verifyIDLabel": MessageLookupByLibrary.simpleMessage("Verifiëren"), "verifyPasskey": MessageLookupByLibrary.simpleMessage("Bevestig passkey"), @@ -1971,6 +2023,8 @@ class MessageLookup extends MessageLookupByLibrary { "Herstelsleutel verifiëren..."), "videoInfo": MessageLookupByLibrary.simpleMessage("Video-info"), "videoSmallCase": MessageLookupByLibrary.simpleMessage("video"), + "videoStreaming": + MessageLookupByLibrary.simpleMessage("Video streamen"), "videos": MessageLookupByLibrary.simpleMessage("Video\'s"), "viewActiveSessions": MessageLookupByLibrary.simpleMessage("Actieve sessies bekijken"), @@ -1987,7 +2041,7 @@ class MessageLookup extends MessageLookupByLibrary { "viewRecoveryKey": MessageLookupByLibrary.simpleMessage("Toon herstelsleutel"), "viewer": MessageLookupByLibrary.simpleMessage("Kijker"), - "viewersSuccessfullyAdded": m86, + "viewersSuccessfullyAdded": m91, "visitWebToManage": MessageLookupByLibrary.simpleMessage( "Bezoek alstublieft web.ente.io om uw abonnement te beheren"), "waitingForVerification": @@ -2008,7 +2062,7 @@ class MessageLookup extends MessageLookupByLibrary { "Vertrouwde contacten kunnen helpen bij het herstellen van je data."), "yearShort": MessageLookupByLibrary.simpleMessage("jr"), "yearly": MessageLookupByLibrary.simpleMessage("Jaarlijks"), - "yearsAgo": m87, + "yearsAgo": m92, "yes": MessageLookupByLibrary.simpleMessage("Ja"), "yesCancel": MessageLookupByLibrary.simpleMessage("Ja, opzeggen"), "yesConvertToViewer": @@ -2040,7 +2094,7 @@ class MessageLookup extends MessageLookupByLibrary { "Je kunt niet met jezelf delen"), "youDontHaveAnyArchivedItems": MessageLookupByLibrary.simpleMessage( "U heeft geen gearchiveerde bestanden."), - "youHaveSuccessfullyFreedUp": m88, + "youHaveSuccessfullyFreedUp": m93, "yourAccountHasBeenDeleted": MessageLookupByLibrary.simpleMessage("Je account is verwijderd"), "yourMap": MessageLookupByLibrary.simpleMessage("Jouw kaart"), @@ -2061,9 +2115,6 @@ class MessageLookup extends MessageLookupByLibrary { "Uw abonnement is succesvol bijgewerkt"), "yourVerificationCodeHasExpired": MessageLookupByLibrary.simpleMessage( "Uw verificatiecode is verlopen"), - "youveNoDuplicateFilesThatCanBeCleared": - MessageLookupByLibrary.simpleMessage( - "Je hebt geen dubbele bestanden die kunnen worden gewist"), "youveNoFilesInThisAlbumThatCanBeDeleted": MessageLookupByLibrary.simpleMessage( "Je hebt geen bestanden in dit album die verwijderd kunnen worden"), diff --git a/mobile/lib/generated/intl/messages_no.dart b/mobile/lib/generated/intl/messages_no.dart index d54454d5c6..7b10ae5ab3 100644 --- a/mobile/lib/generated/intl/messages_no.dart +++ b/mobile/lib/generated/intl/messages_no.dart @@ -20,28 +20,28 @@ typedef String MessageIfAbsent(String messageStr, List args); class MessageLookup extends MessageLookupByLibrary { String get localeName => 'no'; - static String m15(count) => + static String m16(count) => "${Intl.plural(count, zero: 'Ingen deltakere', one: '1 deltaker', other: '${count} deltakere')}"; static String m3(user) => "${user} vil ikke kunne legge til flere bilder til dette albumet\n\nDe vil fortsatt kunne fjerne eksisterende bilder lagt til av dem"; - static String m26(count) => + static String m27(count) => "${Intl.plural(count, one: 'Slett ${count} element', other: 'Slett ${count} elementer')}"; - static String m28(albumName) => + static String m29(albumName) => "Dette fjerner den offentlige lenken for tilgang til \"${albumName}\"."; - static String m29(supportEmail) => + static String m30(supportEmail) => "Vennligst send en e-post til ${supportEmail} fra din registrerte e-postadresse"; - static String m31(count, formattedSize) => + static String m32(count, formattedSize) => "${count} filer, ${formattedSize} hver"; - static String m42(count) => + static String m44(count) => "${Intl.plural(count, one: '${count} element', other: '${count} elementer')}"; - static String m44(expiryTime) => "Lenken utløper på ${expiryTime}"; + static String m46(expiryTime) => "Lenken utløper på ${expiryTime}"; static String m5(count, formattedCount) => "${Intl.plural(count, zero: 'ingen minner', one: '${formattedCount} minne', other: '${formattedCount} minner')}"; @@ -51,20 +51,20 @@ class MessageLookup extends MessageLookupByLibrary { static String m6(count) => "${count} valgt"; - static String m65(count, yourCount) => "${count} valgt (${yourCount} dine)"; + static String m70(count, yourCount) => "${count} valgt (${yourCount} dine)"; - static String m66(verificationID) => + static String m71(verificationID) => "Her er min verifiserings-ID: ${verificationID} for ente.io."; static String m7(verificationID) => "Hei, kan du bekrefte at dette er din ente.io verifiserings-ID: ${verificationID}"; - static String m68(numberOfPeople) => + static String m73(numberOfPeople) => "${Intl.plural(numberOfPeople, zero: 'Del med bestemte personer', one: 'Delt med 1 person', other: 'Delt med ${numberOfPeople} personer')}"; - static String m78(email) => "Dette er ${email} sin verifiserings-ID"; + static String m83(email) => "Dette er ${email} sin verifiserings-ID"; - static String m85(email) => "Verifiser ${email}"; + static String m90(email) => "Verifiser ${email}"; static String m2(email) => "Vi har sendt en e-post til ${email}"; @@ -91,7 +91,7 @@ class MessageLookup extends MessageLookupByLibrary { "after1Week": MessageLookupByLibrary.simpleMessage("Etter 1 uke"), "after1Year": MessageLookupByLibrary.simpleMessage("Etter 1 år"), "albumOwner": MessageLookupByLibrary.simpleMessage("Eier"), - "albumParticipantsCount": m15, + "albumParticipantsCount": m16, "albumUpdated": MessageLookupByLibrary.simpleMessage("Album oppdatert"), "albums": MessageLookupByLibrary.simpleMessage("Album"), "allowAddPhotosDescription": MessageLookupByLibrary.simpleMessage( @@ -164,7 +164,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Slett fra begge"), "deleteFromDevice": MessageLookupByLibrary.simpleMessage("Slett fra enhet"), - "deleteItemCount": m26, + "deleteItemCount": m27, "deletePhotos": MessageLookupByLibrary.simpleMessage("Slett bilder"), "deleteReason1": MessageLookupByLibrary.simpleMessage( "Det mangler en hovedfunksjon jeg trenger"), @@ -180,12 +180,12 @@ class MessageLookup extends MessageLookupByLibrary { "Seere kan fremdeles ta skjermbilder eller lagre en kopi av bildene dine ved bruk av eksterne verktøy"), "disableDownloadWarningTitle": MessageLookupByLibrary.simpleMessage("Vær oppmerksom på"), - "disableLinkMessage": m28, + "disableLinkMessage": m29, "doThisLater": MessageLookupByLibrary.simpleMessage("Gjør dette senere"), "done": MessageLookupByLibrary.simpleMessage("Ferdig"), - "dropSupportEmail": m29, - "duplicateItemsGroup": m31, + "dropSupportEmail": m30, + "duplicateItemsGroup": m32, "email": MessageLookupByLibrary.simpleMessage("E-post"), "encryption": MessageLookupByLibrary.simpleMessage("Kryptering"), "encryptionKeys": @@ -245,14 +245,14 @@ class MessageLookup extends MessageLookupByLibrary { "invalidKey": MessageLookupByLibrary.simpleMessage("Ugyldig nøkkel"), "invalidRecoveryKey": MessageLookupByLibrary.simpleMessage( "Gjenopprettingsnøkkelen du har skrevet inn er ikke gyldig. Kontroller at den inneholder 24 ord og kontroller stavemåten av hvert ord.\n\nHvis du har angitt en eldre gjenopprettingskode, må du kontrollere at den er 64 tegn lang, og kontrollere hvert av dem."), - "itemCount": m42, + "itemCount": m44, "keepPhotos": MessageLookupByLibrary.simpleMessage("Behold Bilder"), "kindlyHelpUsWithThisInformation": MessageLookupByLibrary.simpleMessage( "Vær vennlig og hjelp oss med denne informasjonen"), "linkDeviceLimit": MessageLookupByLibrary.simpleMessage("Enhetsgrense"), "linkEnabled": MessageLookupByLibrary.simpleMessage("Aktivert"), "linkExpired": MessageLookupByLibrary.simpleMessage("Utløpt"), - "linkExpiresOn": m44, + "linkExpiresOn": m46, "linkExpiry": MessageLookupByLibrary.simpleMessage("Lenkeutløp"), "linkHasExpired": MessageLookupByLibrary.simpleMessage("Lenken har utløpt"), @@ -284,8 +284,6 @@ class MessageLookup extends MessageLookupByLibrary { "oops": MessageLookupByLibrary.simpleMessage("Oisann"), "orPickAnExistingOne": MessageLookupByLibrary.simpleMessage("Eller velg en eksisterende"), - "orPickFromYourContacts": - MessageLookupByLibrary.simpleMessage("or pick from your contacts"), "password": MessageLookupByLibrary.simpleMessage("Passord"), "passwordChangedSuccessfully": MessageLookupByLibrary.simpleMessage("Passordet ble endret"), @@ -357,7 +355,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage( "Valgte mapper vil bli kryptert og sikkerhetskopiert"), "selectedPhotos": m6, - "selectedPhotosWithYours": m65, + "selectedPhotosWithYours": m70, "sendEmail": MessageLookupByLibrary.simpleMessage("Send e-post"), "sendInvite": MessageLookupByLibrary.simpleMessage("Send invitasjon"), "sendLink": MessageLookupByLibrary.simpleMessage("Send lenke"), @@ -367,9 +365,9 @@ class MessageLookup extends MessageLookupByLibrary { "setupComplete": MessageLookupByLibrary.simpleMessage("Oppsett fullført"), "shareALink": MessageLookupByLibrary.simpleMessage("Del en lenke"), - "shareMyVerificationID": m66, + "shareMyVerificationID": m71, "shareTextConfirmOthersVerificationID": m7, - "shareWithPeopleSectionTitle": m68, + "shareWithPeopleSectionTitle": m73, "sharedPhotoNotifications": MessageLookupByLibrary.simpleMessage("Nye delte bilder"), "sharedPhotoNotificationsExplanation": MessageLookupByLibrary.simpleMessage( @@ -407,7 +405,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage( "Dette kan brukes til å gjenopprette kontoen din hvis du mister din andre faktor"), "thisDevice": MessageLookupByLibrary.simpleMessage("Denne enheten"), - "thisIsPersonVerificationId": m78, + "thisIsPersonVerificationId": m83, "thisIsYourVerificationId": MessageLookupByLibrary.simpleMessage( "Dette er din bekreftelses-ID"), "thisWillLogYouOutOfTheFollowingDevice": @@ -434,7 +432,7 @@ class MessageLookup extends MessageLookupByLibrary { "verify": MessageLookupByLibrary.simpleMessage("Bekreft"), "verifyEmail": MessageLookupByLibrary.simpleMessage("Bekreft e-postadresse"), - "verifyEmailID": m85, + "verifyEmailID": m90, "verifyPassword": MessageLookupByLibrary.simpleMessage("Bekreft passord"), "verifyingRecoveryKey": MessageLookupByLibrary.simpleMessage( diff --git a/mobile/lib/generated/intl/messages_pl.dart b/mobile/lib/generated/intl/messages_pl.dart index 5b921f075b..82fad2b66b 100644 --- a/mobile/lib/generated/intl/messages_pl.dart +++ b/mobile/lib/generated/intl/messages_pl.dart @@ -20,37 +20,37 @@ typedef String MessageIfAbsent(String messageStr, List args); class MessageLookup extends MessageLookupByLibrary { String get localeName => 'pl'; - static String m9(count) => + static String m10(count) => "${Intl.plural(count, one: 'Dodaj współuczestnika', few: 'Dodaj współuczestników', many: 'Dodaj współuczestników', other: 'Dodaj współuczestników')}"; - static String m10(count) => + static String m11(count) => "${Intl.plural(count, one: 'Dodaj element', few: 'Dodaj elementy', other: 'Dodaj elementów')}"; - static String m11(storageAmount, endDate) => + static String m12(storageAmount, endDate) => "Twój dodatek ${storageAmount} jest ważny do ${endDate}"; - static String m12(count) => + static String m13(count) => "${Intl.plural(count, one: 'Dodaj widza', few: 'Dodaj widzów', many: 'Dodaj widzów', other: 'Dodaj widzów')}"; - static String m13(emailOrName) => "Dodane przez ${emailOrName}"; + static String m14(emailOrName) => "Dodane przez ${emailOrName}"; - static String m14(albumName) => "Pomyślnie dodano do ${albumName}"; + static String m15(albumName) => "Pomyślnie dodano do ${albumName}"; - static String m15(count) => + static String m16(count) => "${Intl.plural(count, zero: 'Brak Uczestników', one: '1 Uczestnik', other: '${count} Uczestników')}"; - static String m16(versionValue) => "Wersja: ${versionValue}"; + static String m17(versionValue) => "Wersja: ${versionValue}"; - static String m17(freeAmount, storageUnit) => + static String m18(freeAmount, storageUnit) => "${freeAmount} ${storageUnit} wolne"; - static String m18(paymentProvider) => + static String m19(paymentProvider) => "Prosimy najpierw anulować istniejącą subskrypcję z ${paymentProvider}"; static String m3(user) => "${user} nie będzie mógł dodać więcej zdjęć do tego albumu\n\nJednak nadal będą mogli usunąć istniejące zdjęcia, które dodali"; - static String m19(isFamilyMember, storageAmountInGb) => + static String m20(isFamilyMember, storageAmountInGb) => "${Intl.select(isFamilyMember, { 'true': 'Twoja rodzina odebrała ${storageAmountInGb} GB do tej pory', @@ -58,213 +58,213 @@ class MessageLookup extends MessageLookupByLibrary { 'other': 'Odebrałeś ${storageAmountInGb} GB do tej pory!', })}"; - static String m20(albumName) => "Utworzono link współpracy dla ${albumName}"; + static String m21(albumName) => "Utworzono link współpracy dla ${albumName}"; - static String m21(count) => + static String m22(count) => "${Intl.plural(count, zero: 'Dodano 0 współuczestników', one: 'Dodano 1 współuczestnika', other: 'Dodano ${count} współuczestników')}"; - static String m22(email, numOfDays) => + static String m23(email, numOfDays) => "Zamierzasz dodać ${email} jako zaufany kontakt. Będą mogli odzyskać Twoje konto, jeśli jesteś nieobecny przez ${numOfDays} dni."; - static String m23(familyAdminEmail) => + static String m24(familyAdminEmail) => "Prosimy skontaktować się z ${familyAdminEmail}, by zarzadząć swoją subskrypcją"; - static String m24(provider) => + static String m25(provider) => "Skontaktuj się z nami pod adresem support@ente.io, aby zarządzać subskrypcją ${provider}."; - static String m25(endpoint) => "Połączono z ${endpoint}"; + static String m26(endpoint) => "Połączono z ${endpoint}"; - static String m26(count) => + static String m27(count) => "${Intl.plural(count, one: 'Usuń ${count} element', few: 'Usuń ${count} elementy', many: 'Usuń ${count} elementów', other: 'Usuń ${count} elementu')}"; - static String m27(currentlyDeleting, totalCount) => + static String m28(currentlyDeleting, totalCount) => "Usuwanie ${currentlyDeleting} / ${totalCount}"; - static String m28(albumName) => + static String m29(albumName) => "Spowoduje to usunięcie publicznego linku dostępu do \"${albumName}\"."; - static String m29(supportEmail) => + static String m30(supportEmail) => "Wyślij wiadomość e-mail na ${supportEmail} z zarejestrowanego adresu e-mail"; - static String m30(count, storageSaved) => + static String m31(count, storageSaved) => "Wyczyszczono ${Intl.plural(count, one: '${count} zdduplikowany plik', other: '${count} zdduplikowane pliki')}, oszczędzając (${storageSaved}!)"; - static String m31(count, formattedSize) => + static String m32(count, formattedSize) => "${count} plików, każdy po ${formattedSize}"; - static String m32(newEmail) => "Adres e-mail został zmieniony na ${newEmail}"; + static String m33(newEmail) => "Adres e-mail został zmieniony na ${newEmail}"; - static String m33(email) => + static String m35(email) => "${email} nie posiada konta Ente.\n\nWyślij im zaproszenie do udostępniania zdjęć."; - static String m34(text) => "Znaleziono dodatkowe zdjęcia dla ${text}"; + static String m36(text) => "Znaleziono dodatkowe zdjęcia dla ${text}"; - static String m35(count, formattedNumber) => + static String m37(count, formattedNumber) => "${Intl.plural(count, one: '1 plikowi', other: '${formattedNumber} plikom')} na tym urządzeniu została bezpiecznie utworzona kopia zapasowa"; - static String m36(count, formattedNumber) => + static String m38(count, formattedNumber) => "${Intl.plural(count, one: '1 plikowi', other: '${formattedNumber} plikom')} w tym albumie została bezpiecznie utworzona kopia zapasowa"; static String m4(storageAmountInGB) => "${storageAmountInGB} GB za każdym razem, gdy ktoś zarejestruje się w płatnym planie i użyje twojego kodu"; - static String m37(endDate) => "Okres próbny ważny do ${endDate}"; + static String m39(endDate) => "Okres próbny ważny do ${endDate}"; - static String m38(count) => + static String m40(count) => "Nadal możesz mieć dostęp ${Intl.plural(count, one: 'do tego', other: 'do tych')} na Ente tak długo, jak masz aktywną subskrypcję"; - static String m39(sizeInMBorGB) => "Zwolnij ${sizeInMBorGB}"; + static String m41(sizeInMBorGB) => "Zwolnij ${sizeInMBorGB}"; - static String m40(count, formattedSize) => + static String m42(count, formattedSize) => "${Intl.plural(count, one: 'Można to usunąć z urządzenia, aby zwolnić ${formattedSize}', other: 'Można je usunąć z urządzenia, aby zwolnić ${formattedSize}')}"; - static String m41(currentlyProcessing, totalCount) => + static String m43(currentlyProcessing, totalCount) => "Przetwarzanie ${currentlyProcessing} / ${totalCount}"; - static String m42(count) => + static String m44(count) => "${Intl.plural(count, one: '${count} element', few: '${count} elementy', many: '${count} elementów', other: '${count} elementu')}"; - static String m43(email) => + static String m45(email) => "${email} zaprosił Cię do zostania zaufanym kontaktem"; - static String m44(expiryTime) => "Link wygaśnie ${expiryTime}"; + static String m46(expiryTime) => "Link wygaśnie ${expiryTime}"; static String m5(count, formattedCount) => "${Intl.plural(count, zero: 'brak wspomnień', one: '${formattedCount} wspomnienie', few: '${formattedCount} wspomnienia', other: '${formattedCount} wspomnień')}"; - static String m45(count) => + static String m49(count) => "${Intl.plural(count, one: 'Przenieś element', few: 'Przenieś elementy', other: 'Przenieś elementów')}"; - static String m46(albumName) => "Pomyślnie przeniesiono do ${albumName}"; + static String m50(albumName) => "Pomyślnie przeniesiono do ${albumName}"; - static String m47(personName) => "Brak sugestii dla ${personName}"; + static String m51(personName) => "Brak sugestii dla ${personName}"; - static String m48(name) => "Nie ${name}?"; + static String m52(name) => "Nie ${name}?"; - static String m49(familyAdminEmail) => + static String m53(familyAdminEmail) => "Skontaktuj się z ${familyAdminEmail}, aby zmienić swój kod."; static String m0(passwordStrengthValue) => "Siła hasła: ${passwordStrengthValue}"; - static String m50(providerName) => + static String m54(providerName) => "Porozmawiaj ze wsparciem ${providerName} jeśli zostałeś obciążony"; - static String m51(count) => + static String m55(count) => "${Intl.plural(count, zero: '0 zdjęć', one: '1 zdjęcie', few: '${count} zdjęcia', other: '${count} zdjęć')}"; - static String m52(endDate) => + static String m56(endDate) => "Bezpłatny okres próbny ważny do ${endDate}.\nNastępnie możesz wybrać płatny plan."; - static String m53(toEmail) => + static String m57(toEmail) => "Prosimy o kontakt mailowy pod adresem ${toEmail}"; - static String m54(toEmail) => "Prosimy wysłać logi do ${toEmail}"; + static String m58(toEmail) => "Prosimy wysłać logi do ${toEmail}"; - static String m55(folderName) => "Przetwarzanie ${folderName}..."; + static String m59(folderName) => "Przetwarzanie ${folderName}..."; - static String m56(storeName) => "Oceń nas na ${storeName}"; + static String m60(storeName) => "Oceń nas na ${storeName}"; - static String m57(days, email) => + static String m62(days, email) => "Możesz uzyskać dostęp do konta po dniu ${days} dni. Powiadomienie zostanie wysłane na ${email}."; - static String m58(email) => + static String m63(email) => "Możesz teraz odzyskać konto ${email} poprzez ustawienie nowego hasła."; - static String m59(email) => "${email} próbuje odzyskać Twoje konto."; + static String m64(email) => "${email} próbuje odzyskać Twoje konto."; - static String m60(storageInGB) => + static String m65(storageInGB) => "3. Oboje otrzymujecie ${storageInGB} GB* za darmo"; - static String m61(userEmail) => + static String m66(userEmail) => "${userEmail} zostanie usunięty z tego udostępnionego albumu\n\nWszelkie dodane przez nich zdjęcia zostaną usunięte z albumu"; - static String m62(endDate) => "Subskrypcja odnowi się ${endDate}"; + static String m67(endDate) => "Subskrypcja odnowi się ${endDate}"; - static String m63(count) => + static String m68(count) => "${Intl.plural(count, one: 'Znaleziono ${count} wynik', few: 'Znaleziono ${count} wyniki', other: 'Znaleziono ${count} wyników')}"; - static String m64(snapshotLength, searchLength) => + static String m69(snapshotLength, searchLength) => "Niezgodność długości sekcji: ${snapshotLength} != ${searchLength}"; static String m6(count) => "Wybrano ${count}"; - static String m65(count, yourCount) => + static String m70(count, yourCount) => "Wybrano ${count} (twoich ${yourCount})"; - static String m66(verificationID) => + static String m71(verificationID) => "Oto mój identyfikator weryfikacyjny: ${verificationID} dla ente.io."; static String m7(verificationID) => "Hej, czy możesz potwierdzić, że to jest Twój identyfikator weryfikacyjny ente.io: ${verificationID}"; - static String m67(referralCode, referralStorageInGB) => + static String m72(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 m68(numberOfPeople) => + static String m73(numberOfPeople) => "${Intl.plural(numberOfPeople, zero: 'Udostępnione określonym osobom', one: 'Udostępnione 1 osobie', other: 'Udostępnione ${numberOfPeople} osobom')}"; - static String m69(emailIDs) => "Udostępnione z ${emailIDs}"; + static String m74(emailIDs) => "Udostępnione z ${emailIDs}"; - static String m70(fileType) => + static String m75(fileType) => "Ten ${fileType} zostanie usunięty z Twojego urządzenia."; - static String m71(fileType) => + static String m76(fileType) => "Ten ${fileType} jest zarówno w Ente, jak i na twoim urządzeniu."; - static String m72(fileType) => "Ten ${fileType} zostanie usunięty z Ente."; + static String m77(fileType) => "Ten ${fileType} zostanie usunięty z Ente."; static String m1(storageAmountInGB) => "${storageAmountInGB} GB"; - static String m73( + static String m78( usedAmount, usedStorageUnit, totalAmount, totalStorageUnit) => "Użyto ${usedAmount} ${usedStorageUnit} z ${totalAmount} ${totalStorageUnit}"; - static String m74(id) => + static String m79(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 m75(endDate) => + static String m80(endDate) => "Twoja subskrypcja zostanie anulowana dnia ${endDate}"; - static String m76(completed, total) => + static String m81(completed, total) => "Zachowano ${completed}/${total} wspomnień"; - static String m77(ignoreReason) => + static String m82(ignoreReason) => "Naciśnij, aby przesłać, przesyłanie jest obecnie ignorowane z powodu ${ignoreReason}"; static String m8(storageAmountInGB) => "Oni również otrzymują ${storageAmountInGB} GB"; - static String m78(email) => "To jest identyfikator weryfikacyjny ${email}"; + static String m83(email) => "To jest identyfikator weryfikacyjny ${email}"; - static String m79(count) => + static String m84(count) => "${Intl.plural(count, zero: 'Wkrótce', one: '1 dzień', few: '${count} dni', other: '${count} dni')}"; - static String m80(email) => + static String m85(email) => "Zostałeś zaproszony do bycia dziedzicznym kontaktem przez ${email}."; - static String m81(galleryType) => + static String m86(galleryType) => "Typ galerii ${galleryType} nie jest obsługiwany dla zmiany nazwy"; - static String m82(ignoreReason) => + static String m87(ignoreReason) => "Przesyłanie jest ignorowane z powodu ${ignoreReason}"; - static String m83(count) => "Zachowywanie ${count} wspomnień..."; + static String m88(count) => "Zachowywanie ${count} wspomnień..."; - static String m84(endDate) => "Ważne do ${endDate}"; + static String m89(endDate) => "Ważne do ${endDate}"; - static String m85(email) => "Zweryfikuj ${email}"; + static String m90(email) => "Zweryfikuj ${email}"; - static String m86(count) => + static String m91(count) => "${Intl.plural(count, zero: 'Dodano 0 widzów', one: 'Dodano 1 widza', other: 'Dodano ${count} widzów')}"; static String m2(email) => "Wysłaliśmy wiadomość na adres ${email}"; - static String m87(count) => + static String m92(count) => "${Intl.plural(count, one: '${count} rok temu', few: '${count} lata temu', many: '${count} lat temu', other: '${count} lata temu')}"; - static String m88(storageSaved) => "Pomyślnie zwolniłeś/aś ${storageSaved}!"; + static String m93(storageSaved) => "Pomyślnie zwolniłeś/aś ${storageSaved}!"; final messages = _notInlinedMessages(_notInlinedMessages); static Map _notInlinedMessages(_) => { @@ -287,11 +287,11 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Dodaj nowy adres e-mail"), "addCollaborator": MessageLookupByLibrary.simpleMessage("Dodaj współuczestnika"), - "addCollaborators": m9, + "addCollaborators": m10, "addFiles": MessageLookupByLibrary.simpleMessage("Dodaj Pliki"), "addFromDevice": MessageLookupByLibrary.simpleMessage("Dodaj z urządzenia"), - "addItem": m10, + "addItem": m11, "addLocation": MessageLookupByLibrary.simpleMessage("Dodaj lokalizację"), "addLocationButton": MessageLookupByLibrary.simpleMessage("Dodaj"), @@ -304,7 +304,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Dodaj nową osobę"), "addOnPageSubtitle": MessageLookupByLibrary.simpleMessage("Szczegóły dodatków"), - "addOnValidTill": m11, + "addOnValidTill": m12, "addOns": MessageLookupByLibrary.simpleMessage("Dodatki"), "addPhotos": MessageLookupByLibrary.simpleMessage("Dodaj zdjęcia"), "addSelected": MessageLookupByLibrary.simpleMessage("Dodaj zaznaczone"), @@ -315,12 +315,12 @@ class MessageLookup extends MessageLookupByLibrary { "addTrustedContact": MessageLookupByLibrary.simpleMessage("Dodaj Zaufany Kontakt"), "addViewer": MessageLookupByLibrary.simpleMessage("Dodaj widza"), - "addViewers": m12, + "addViewers": m13, "addYourPhotosNow": MessageLookupByLibrary.simpleMessage("Dodaj swoje zdjęcia teraz"), "addedAs": MessageLookupByLibrary.simpleMessage("Dodano jako"), - "addedBy": m13, - "addedSuccessfullyTo": m14, + "addedBy": m14, + "addedSuccessfullyTo": m15, "addingToFavorites": MessageLookupByLibrary.simpleMessage("Dodawanie do ulubionych..."), "advanced": MessageLookupByLibrary.simpleMessage("Zaawansowane"), @@ -332,7 +332,7 @@ class MessageLookup extends MessageLookupByLibrary { "after1Week": MessageLookupByLibrary.simpleMessage("Po 1 tygodniu"), "after1Year": MessageLookupByLibrary.simpleMessage("Po 1 roku"), "albumOwner": MessageLookupByLibrary.simpleMessage("Właściciel"), - "albumParticipantsCount": m15, + "albumParticipantsCount": m16, "albumTitle": MessageLookupByLibrary.simpleMessage("Tytuł albumu"), "albumUpdated": MessageLookupByLibrary.simpleMessage("Album został zaktualizowany"), @@ -383,7 +383,7 @@ class MessageLookup extends MessageLookupByLibrary { "Blokada dostępu do aplikacji"), "appLockDescriptions": MessageLookupByLibrary.simpleMessage( "Wybierz między domyślnym ekranem blokady urządzenia a niestandardowym ekranem blokady z kodem PIN lub hasłem."), - "appVersion": m16, + "appVersion": m17, "appleId": MessageLookupByLibrary.simpleMessage("Apple ID"), "apply": MessageLookupByLibrary.simpleMessage("Zastosuj"), "applyCodeTitle": MessageLookupByLibrary.simpleMessage("Użyj kodu"), @@ -435,6 +435,8 @@ class MessageLookup extends MessageLookupByLibrary { "Prosimy uwierzytelnić się, aby zarządzać zaufanymi kontaktami"), "authToViewPasskey": MessageLookupByLibrary.simpleMessage( "Prosimy uwierzytelnić się, aby wyświetlić swój klucz dostępu"), + "authToViewTrashedFiles": MessageLookupByLibrary.simpleMessage( + "Prosimy uwierzytelnić się, aby wyświetlić swoje pliki w koszu"), "authToViewYourActiveSessions": MessageLookupByLibrary.simpleMessage( "Prosimy uwierzytelnić się, aby wyświetlić swoje aktywne sesje"), "authToViewYourHiddenFiles": MessageLookupByLibrary.simpleMessage( @@ -465,7 +467,7 @@ class MessageLookup extends MessageLookupByLibrary { "autoPairDesc": MessageLookupByLibrary.simpleMessage( "Automatyczne parowanie działa tylko z urządzeniami obsługującymi Chromecast."), "available": MessageLookupByLibrary.simpleMessage("Dostępne"), - "availableStorageSpace": m17, + "availableStorageSpace": m18, "backedUpFolders": MessageLookupByLibrary.simpleMessage("Foldery kopii zapasowej"), "backup": MessageLookupByLibrary.simpleMessage("Kopia zapasowa"), @@ -490,6 +492,10 @@ class MessageLookup extends MessageLookupByLibrary { "cachedData": MessageLookupByLibrary.simpleMessage("Dane w pamięci podręcznej"), "calculating": MessageLookupByLibrary.simpleMessage("Obliczanie..."), + "canNotOpenBody": MessageLookupByLibrary.simpleMessage( + "Przepraszamy, ten album nie może zostać otwarty w aplikacji."), + "canNotOpenTitle": MessageLookupByLibrary.simpleMessage( + "Nie można otworzyć tego albumu"), "canNotUploadToAlbumsOwnedByOthers": MessageLookupByLibrary.simpleMessage( "Nie można przesłać do albumów należących do innych"), @@ -503,7 +509,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Anuluj odzyskiwanie"), "cancelAccountRecoveryBody": MessageLookupByLibrary.simpleMessage( "Czy na pewno chcesz anulować odzyskiwanie?"), - "cancelOtherSubscription": m18, + "cancelOtherSubscription": m19, "cancelSubscription": MessageLookupByLibrary.simpleMessage("Anuluj subskrypcję"), "cannotAddMorePhotosAfterBecomingViewer": m3, @@ -555,7 +561,7 @@ class MessageLookup extends MessageLookupByLibrary { "Odbierz bezpłatną przestrzeń dyskową"), "claimMore": MessageLookupByLibrary.simpleMessage("Zdobądź więcej!"), "claimed": MessageLookupByLibrary.simpleMessage("Odebrano"), - "claimedStorageSoFar": m19, + "claimedStorageSoFar": m20, "cleanUncategorized": MessageLookupByLibrary.simpleMessage("Wyczyść Nieskategoryzowane"), "cleanUncategorizedDescription": MessageLookupByLibrary.simpleMessage( @@ -585,12 +591,12 @@ class MessageLookup extends MessageLookupByLibrary { "Utwórz link, aby umożliwić innym dodawanie i przeglądanie zdjęć w udostępnionym albumie bez konieczności korzystania z aplikacji lub konta Ente. Świetne rozwiązanie do gromadzenia zdjęć ze wspólnych wydarzeń."), "collaborativeLink": MessageLookupByLibrary.simpleMessage("Link do współpracy"), - "collaborativeLinkCreatedFor": m20, + "collaborativeLinkCreatedFor": m21, "collaborator": MessageLookupByLibrary.simpleMessage("Współuczestnik"), "collaboratorsCanAddPhotosAndVideosToTheSharedAlbum": MessageLookupByLibrary.simpleMessage( "Współuczestnicy mogą dodawać zdjęcia i wideo do udostępnionego albumu."), - "collaboratorsSuccessfullyAdded": m21, + "collaboratorsSuccessfullyAdded": m22, "collageLayout": MessageLookupByLibrary.simpleMessage("Układ"), "collageSaved": MessageLookupByLibrary.simpleMessage("Kolaż zapisano w galerii"), @@ -607,7 +613,7 @@ class MessageLookup extends MessageLookupByLibrary { "Czy na pewno chcesz wyłączyć uwierzytelnianie dwustopniowe?"), "confirmAccountDeletion": MessageLookupByLibrary.simpleMessage("Potwierdź usunięcie konta"), - "confirmAddingTrustedContact": m22, + "confirmAddingTrustedContact": m23, "confirmDeletePrompt": MessageLookupByLibrary.simpleMessage( "Tak, chcę trwale usunąć to konto i jego dane ze wszystkich aplikacji."), "confirmPassword": @@ -620,10 +626,10 @@ class MessageLookup extends MessageLookupByLibrary { "Potwierdź klucz odzyskiwania"), "connectToDevice": MessageLookupByLibrary.simpleMessage("Połącz z urządzeniem"), - "contactFamilyAdmin": m23, + "contactFamilyAdmin": m24, "contactSupport": MessageLookupByLibrary.simpleMessage( "Skontaktuj się z pomocą techniczną"), - "contactToManageSubscription": m24, + "contactToManageSubscription": m25, "contacts": MessageLookupByLibrary.simpleMessage("Kontakty"), "contents": MessageLookupByLibrary.simpleMessage("Zawartość"), "continueLabel": MessageLookupByLibrary.simpleMessage("Kontynuuj"), @@ -669,7 +675,7 @@ class MessageLookup extends MessageLookupByLibrary { "currentlyRunning": MessageLookupByLibrary.simpleMessage("aktualnie uruchomiony"), "custom": MessageLookupByLibrary.simpleMessage("Niestandardowy"), - "customEndpoint": m25, + "customEndpoint": m26, "darkTheme": MessageLookupByLibrary.simpleMessage("Ciemny"), "dayToday": MessageLookupByLibrary.simpleMessage("Dzisiaj"), "dayYesterday": MessageLookupByLibrary.simpleMessage("Wczoraj"), @@ -704,11 +710,11 @@ class MessageLookup extends MessageLookupByLibrary { "deleteFromDevice": MessageLookupByLibrary.simpleMessage("Usuń z urządzenia"), "deleteFromEnte": MessageLookupByLibrary.simpleMessage("Usuń z Ente"), - "deleteItemCount": m26, + "deleteItemCount": m27, "deleteLocation": MessageLookupByLibrary.simpleMessage("Usuń lokalizację"), "deletePhotos": MessageLookupByLibrary.simpleMessage("Usuń zdjęcia"), - "deleteProgress": m27, + "deleteProgress": m28, "deleteReason1": MessageLookupByLibrary.simpleMessage( "Brakuje kluczowej funkcji, której potrzebuję"), "deleteReason2": MessageLookupByLibrary.simpleMessage( @@ -748,7 +754,7 @@ class MessageLookup extends MessageLookupByLibrary { "Widzowie mogą nadal robić zrzuty ekranu lub zapisywać kopie zdjęć za pomocą programów trzecich"), "disableDownloadWarningTitle": MessageLookupByLibrary.simpleMessage("Uwaga"), - "disableLinkMessage": m28, + "disableLinkMessage": m29, "disableTwofactor": MessageLookupByLibrary.simpleMessage( "Wyłącz uwierzytelnianie dwustopniowe"), "disablingTwofactorAuthentication": @@ -791,9 +797,9 @@ class MessageLookup extends MessageLookupByLibrary { "downloadFailed": MessageLookupByLibrary.simpleMessage("Pobieranie nie powiodło się"), "downloading": MessageLookupByLibrary.simpleMessage("Pobieranie..."), - "dropSupportEmail": m29, - "duplicateFileCountWithStorageSaved": m30, - "duplicateItemsGroup": m31, + "dropSupportEmail": m30, + "duplicateFileCountWithStorageSaved": m31, + "duplicateItemsGroup": m32, "edit": MessageLookupByLibrary.simpleMessage("Edytuj"), "editLocation": MessageLookupByLibrary.simpleMessage("Edytuj lokalizację"), @@ -806,8 +812,12 @@ class MessageLookup extends MessageLookupByLibrary { "Edycje lokalizacji będą widoczne tylko w Ente"), "eligible": MessageLookupByLibrary.simpleMessage("kwalifikujący się"), "email": MessageLookupByLibrary.simpleMessage("Adres e-mail"), - "emailChangedTo": m32, - "emailNoEnteAccount": m33, + "emailAlreadyRegistered": MessageLookupByLibrary.simpleMessage( + "Adres e-mail jest już zarejestrowany."), + "emailChangedTo": m33, + "emailNoEnteAccount": m35, + "emailNotRegistered": MessageLookupByLibrary.simpleMessage( + "Adres e-mail nie jest zarejestrowany."), "emailVerificationToggle": MessageLookupByLibrary.simpleMessage("Weryfikacja e-mail"), "emailYourLogs": @@ -888,7 +898,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Eksportuj swoje dane"), "extraPhotosFound": MessageLookupByLibrary.simpleMessage( "Znaleziono dodatkowe zdjęcia"), - "extraPhotosFoundFor": m34, + "extraPhotosFoundFor": m36, "faceNotClusteredYet": MessageLookupByLibrary.simpleMessage( "Twarz jeszcze nie zgrupowana, prosimy wrócić później"), "faceRecognition": @@ -940,8 +950,8 @@ class MessageLookup extends MessageLookupByLibrary { "fileTypes": MessageLookupByLibrary.simpleMessage("Rodzaje plików"), "fileTypesAndNames": MessageLookupByLibrary.simpleMessage("Typy plików i nazwy"), - "filesBackedUpFromDevice": m35, - "filesBackedUpInAlbum": m36, + "filesBackedUpFromDevice": m37, + "filesBackedUpInAlbum": m38, "filesDeleted": MessageLookupByLibrary.simpleMessage("Pliki usunięto"), "filesSavedToGallery": MessageLookupByLibrary.simpleMessage("Pliki zapisane do galerii"), @@ -962,21 +972,22 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Darmowa pamięć użyteczna"), "freeTrial": MessageLookupByLibrary.simpleMessage("Darmowy okres próbny"), - "freeTrialValidTill": m37, - "freeUpAccessPostDelete": m38, - "freeUpAmount": m39, + "freeTrialValidTill": m39, + "freeUpAccessPostDelete": m40, + "freeUpAmount": m41, "freeUpDeviceSpace": MessageLookupByLibrary.simpleMessage( "Zwolnij miejsce na urządzeniu"), "freeUpDeviceSpaceDesc": MessageLookupByLibrary.simpleMessage( "Oszczędzaj miejsce na urządzeniu poprzez wyczyszczenie plików, które zostały już przesłane."), "freeUpSpace": MessageLookupByLibrary.simpleMessage("Zwolnij miejsce"), - "freeUpSpaceSaving": m40, + "freeUpSpaceSaving": m42, + "gallery": MessageLookupByLibrary.simpleMessage("Galeria"), "galleryMemoryLimitInfo": MessageLookupByLibrary.simpleMessage( "W galerii wyświetlane jest do 1000 pamięci"), "general": MessageLookupByLibrary.simpleMessage("Ogólne"), "generatingEncryptionKeys": MessageLookupByLibrary.simpleMessage( "Generowanie kluczy szyfrujących..."), - "genericProgress": m41, + "genericProgress": m43, "goToSettings": MessageLookupByLibrary.simpleMessage("Przejdź do ustawień"), "googlePlayId": @@ -1059,12 +1070,18 @@ 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": m44, "itemsShowTheNumberOfDaysRemainingBeforePermanentDeletion": MessageLookupByLibrary.simpleMessage( "Elementy pokazują liczbę dni pozostałych przed trwałym usunięciem"), "itemsWillBeRemovedFromAlbum": MessageLookupByLibrary.simpleMessage( "Wybrane elementy zostaną usunięte z tego albumu"), + "join": MessageLookupByLibrary.simpleMessage("Dołącz"), + "joinAlbum": MessageLookupByLibrary.simpleMessage("Dołącz do albumu"), + "joinAlbumSubtext": MessageLookupByLibrary.simpleMessage( + "aby wyświetlić i dodać swoje zdjęcia"), + "joinAlbumSubtextViewer": MessageLookupByLibrary.simpleMessage( + "aby dodać to do udostępnionych albumów"), "joinDiscord": MessageLookupByLibrary.simpleMessage("Dołącz do serwera Discord"), "keepPhotos": MessageLookupByLibrary.simpleMessage("Zachowaj Zdjęcia"), @@ -1083,7 +1100,7 @@ class MessageLookup extends MessageLookupByLibrary { "legacy": MessageLookupByLibrary.simpleMessage("Dziedzictwo"), "legacyAccounts": MessageLookupByLibrary.simpleMessage("Odziedziczone konta"), - "legacyInvite": m43, + "legacyInvite": m45, "legacyPageDesc": MessageLookupByLibrary.simpleMessage( "Dziedzictwo pozwala zaufanym kontaktom na dostęp do Twojego konta w razie Twojej nieobecności."), "legacyPageDesc2": MessageLookupByLibrary.simpleMessage( @@ -1094,9 +1111,11 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Link skopiowany do schowka"), "linkDeviceLimit": MessageLookupByLibrary.simpleMessage("Limit urządzeń"), + "linkEmail": + MessageLookupByLibrary.simpleMessage("Połącz adres e-mail"), "linkEnabled": MessageLookupByLibrary.simpleMessage("Aktywny"), "linkExpired": MessageLookupByLibrary.simpleMessage("Wygasł"), - "linkExpiresOn": m44, + "linkExpiresOn": m46, "linkExpiry": MessageLookupByLibrary.simpleMessage("Wygaśnięcie linku"), "linkHasExpired": MessageLookupByLibrary.simpleMessage("Link wygasł"), "linkNeverExpires": MessageLookupByLibrary.simpleMessage("Nigdy"), @@ -1222,12 +1241,12 @@ class MessageLookup extends MessageLookupByLibrary { "mostRecent": MessageLookupByLibrary.simpleMessage("Od najnowszych"), "mostRelevant": MessageLookupByLibrary.simpleMessage("Najbardziej trafne"), - "moveItem": m45, + "moveItem": m49, "moveToAlbum": MessageLookupByLibrary.simpleMessage("Przenieś do albumu"), "moveToHiddenAlbum": MessageLookupByLibrary.simpleMessage("Przenieś do ukrytego albumu"), - "movedSuccessfullyTo": m46, + "movedSuccessfullyTo": m50, "movedToTrash": MessageLookupByLibrary.simpleMessage("Przeniesiono do kosza"), "movingFilesToAlbum": MessageLookupByLibrary.simpleMessage( @@ -1255,6 +1274,8 @@ class MessageLookup extends MessageLookupByLibrary { "Nie masz żadnych plików na tym urządzeniu, które można usunąć"), "noDuplicates": MessageLookupByLibrary.simpleMessage("✨ Brak duplikatów"), + "noEnteAccountExclamation": + MessageLookupByLibrary.simpleMessage("Brak konta Ente!"), "noExifData": MessageLookupByLibrary.simpleMessage("Brak danych EXIF"), "noFacesFound": MessageLookupByLibrary.simpleMessage("Nie znaleziono twarzy"), @@ -1278,10 +1299,10 @@ class MessageLookup extends MessageLookupByLibrary { "noResults": MessageLookupByLibrary.simpleMessage("Brak wyników"), "noResultsFound": MessageLookupByLibrary.simpleMessage("Nie znaleziono wyników"), - "noSuggestionsForPerson": m47, + "noSuggestionsForPerson": m51, "noSystemLockFound": MessageLookupByLibrary.simpleMessage( "Nie znaleziono blokady systemowej"), - "notPersonLabel": m48, + "notPersonLabel": m52, "nothingSharedWithYouYet": MessageLookupByLibrary.simpleMessage( "Nic Ci jeszcze nie udostępniono"), "nothingToSeeHere": MessageLookupByLibrary.simpleMessage( @@ -1291,7 +1312,7 @@ class MessageLookup extends MessageLookupByLibrary { "onDevice": MessageLookupByLibrary.simpleMessage("Na urządzeniu"), "onEnte": MessageLookupByLibrary.simpleMessage("W ente"), - "onlyFamilyAdminCanChangeCode": m49, + "onlyFamilyAdminCanChangeCode": m53, "onlyThem": MessageLookupByLibrary.simpleMessage("Tylko te"), "oops": MessageLookupByLibrary.simpleMessage("Ups"), "oopsCouldNotSaveEdits": MessageLookupByLibrary.simpleMessage( @@ -1314,8 +1335,6 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Lub złącz z istniejącymi"), "orPickAnExistingOne": MessageLookupByLibrary.simpleMessage("Lub wybierz istniejący"), - "orPickFromYourContacts": - MessageLookupByLibrary.simpleMessage("or pick from your contacts"), "pair": MessageLookupByLibrary.simpleMessage("Sparuj"), "pairWithPin": MessageLookupByLibrary.simpleMessage("Sparuj kodem PIN"), "pairingComplete": @@ -1341,7 +1360,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": m50, + "paymentFailedTalkToProvider": m54, "pendingItems": MessageLookupByLibrary.simpleMessage("Oczekujące elementy"), "pendingSync": @@ -1365,14 +1384,14 @@ class MessageLookup extends MessageLookupByLibrary { "photosAddedByYouWillBeRemovedFromTheAlbum": MessageLookupByLibrary.simpleMessage( "Zdjęcia dodane przez Ciebie zostaną usunięte z albumu"), - "photosCount": m51, + "photosCount": m55, "pickCenterPoint": MessageLookupByLibrary.simpleMessage("Wybierz punkt środkowy"), "pinAlbum": MessageLookupByLibrary.simpleMessage("Przypnij album"), "pinLock": MessageLookupByLibrary.simpleMessage("Blokada PIN"), "playOnTv": MessageLookupByLibrary.simpleMessage( "Odtwórz album na telewizorze"), - "playStoreFreeTrialValidTill": m52, + "playStoreFreeTrialValidTill": m56, "playstoreSubscription": MessageLookupByLibrary.simpleMessage("Subskrypcja PlayStore"), "pleaseCheckYourInternetConnectionAndTryAgain": @@ -1384,14 +1403,14 @@ class MessageLookup extends MessageLookupByLibrary { "pleaseContactSupportIfTheProblemPersists": MessageLookupByLibrary.simpleMessage( "Skontaktuj się z pomocą techniczną, jeśli problem będzie się powtarzał"), - "pleaseEmailUsAt": m53, + "pleaseEmailUsAt": m57, "pleaseGrantPermissions": MessageLookupByLibrary.simpleMessage( "Prosimy przyznać uprawnienia"), "pleaseLoginAgain": MessageLookupByLibrary.simpleMessage("Zaloguj się ponownie"), "pleaseSelectQuickLinksToRemove": MessageLookupByLibrary.simpleMessage( "Prosimy wybrać szybkie linki do usunięcia"), - "pleaseSendTheLogsTo": m54, + "pleaseSendTheLogsTo": m58, "pleaseTryAgain": MessageLookupByLibrary.simpleMessage("Spróbuj ponownie"), "pleaseVerifyTheCodeYouHaveEntered": @@ -1418,7 +1437,8 @@ class MessageLookup extends MessageLookupByLibrary { "privateSharing": MessageLookupByLibrary.simpleMessage("Udostępnianie prywatne"), "proceed": MessageLookupByLibrary.simpleMessage("Kontynuuj"), - "processingImport": m55, + "processed": MessageLookupByLibrary.simpleMessage("Przetworzone"), + "processingImport": m59, "publicLinkCreated": MessageLookupByLibrary.simpleMessage("Utworzono publiczny link"), "publicLinkEnabled": @@ -1428,7 +1448,7 @@ class MessageLookup extends MessageLookupByLibrary { "raiseTicket": MessageLookupByLibrary.simpleMessage("Zgłoś"), "rateTheApp": MessageLookupByLibrary.simpleMessage("Oceń aplikację"), "rateUs": MessageLookupByLibrary.simpleMessage("Oceń nas"), - "rateUsOnStore": m56, + "rateUsOnStore": m60, "recover": MessageLookupByLibrary.simpleMessage("Odzyskaj"), "recoverAccount": MessageLookupByLibrary.simpleMessage("Odzyskaj konto"), @@ -1437,7 +1457,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Odzyskaj konto"), "recoveryInitiated": MessageLookupByLibrary.simpleMessage("Odzyskiwanie rozpoczęte"), - "recoveryInitiatedDesc": m57, + "recoveryInitiatedDesc": m62, "recoveryKey": MessageLookupByLibrary.simpleMessage("Klucz odzyskiwania"), "recoveryKeyCopiedToClipboard": MessageLookupByLibrary.simpleMessage( @@ -1452,12 +1472,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": m58, + "recoveryReady": m63, "recoverySuccessful": MessageLookupByLibrary.simpleMessage("Odzyskano pomyślnie!"), "recoveryWarning": MessageLookupByLibrary.simpleMessage( "Zaufany kontakt próbuje uzyskać dostęp do Twojego konta"), - "recoveryWarningBody": m59, + "recoveryWarningBody": m64, "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": @@ -1473,7 +1493,7 @@ class MessageLookup extends MessageLookupByLibrary { "1. Przekaż ten kod swoim znajomym"), "referralStep2": MessageLookupByLibrary.simpleMessage("2. Wykupują płatny plan"), - "referralStep3": m60, + "referralStep3": m65, "referrals": MessageLookupByLibrary.simpleMessage("Polecenia"), "referralsAreCurrentlyPaused": MessageLookupByLibrary.simpleMessage( "Wysyłanie poleceń jest obecnie wstrzymane"), @@ -1503,7 +1523,7 @@ class MessageLookup extends MessageLookupByLibrary { "removeLink": MessageLookupByLibrary.simpleMessage("Usuń link"), "removeParticipant": MessageLookupByLibrary.simpleMessage("Usuń użytkownika"), - "removeParticipantBody": m61, + "removeParticipantBody": m66, "removePersonLabel": MessageLookupByLibrary.simpleMessage("Usuń etykietę osoby"), "removePublicLink": @@ -1524,7 +1544,7 @@ class MessageLookup extends MessageLookupByLibrary { "renameFile": MessageLookupByLibrary.simpleMessage("Zmień nazwę pliku"), "renewSubscription": MessageLookupByLibrary.simpleMessage("Odnów subskrypcję"), - "renewsOn": m62, + "renewsOn": m67, "reportABug": MessageLookupByLibrary.simpleMessage("Zgłoś błąd"), "reportBug": MessageLookupByLibrary.simpleMessage("Zgłoś błąd"), "resendEmail": @@ -1602,8 +1622,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": m63, - "searchSectionsLengthMismatch": m64, + "searchResultCount": m68, + "searchSectionsLengthMismatch": m69, "security": MessageLookupByLibrary.simpleMessage("Bezpieczeństwo"), "seePublicAlbumLinksInApp": MessageLookupByLibrary.simpleMessage( "Zobacz publiczne linki do albumów w aplikacji"), @@ -1637,7 +1657,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage( "Wybrane elementy zostaną usunięte ze wszystkich albumów i przeniesione do kosza."), "selectedPhotos": m6, - "selectedPhotosWithYours": m65, + "selectedPhotosWithYours": m70, "send": MessageLookupByLibrary.simpleMessage("Wyślij"), "sendEmail": MessageLookupByLibrary.simpleMessage("Wyślij e-mail"), "sendInvite": @@ -1666,16 +1686,16 @@ class MessageLookup extends MessageLookupByLibrary { "shareAnAlbumNow": MessageLookupByLibrary.simpleMessage("Udostępnij teraz album"), "shareLink": MessageLookupByLibrary.simpleMessage("Udostępnij link"), - "shareMyVerificationID": m66, + "shareMyVerificationID": m71, "shareOnlyWithThePeopleYouWant": MessageLookupByLibrary.simpleMessage( "Udostępnij tylko ludziom, którym chcesz"), "shareTextConfirmOthersVerificationID": m7, "shareTextRecommendUsingEnte": MessageLookupByLibrary.simpleMessage( "Pobierz Ente, abyśmy mogli łatwo udostępniać zdjęcia i wideo w oryginalnej jakości\n\nhttps://ente.io"), - "shareTextReferralCode": m67, + "shareTextReferralCode": m72, "shareWithNonenteUsers": MessageLookupByLibrary.simpleMessage( "Udostępnij użytkownikom bez konta Ente"), - "shareWithPeopleSectionTitle": m68, + "shareWithPeopleSectionTitle": m73, "shareYourFirstAlbum": MessageLookupByLibrary.simpleMessage( "Udostępnij swój pierwszy album"), "sharedAlbumSectionDescription": MessageLookupByLibrary.simpleMessage( @@ -1688,7 +1708,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": m69, + "sharedWith": m74, "sharedWithMe": MessageLookupByLibrary.simpleMessage("Udostępnione ze mną"), "sharedWithYou": @@ -1705,11 +1725,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": m70, + "singleFileDeleteFromDevice": m75, "singleFileDeleteHighlight": MessageLookupByLibrary.simpleMessage( "To zostanie usunięte ze wszystkich albumów."), - "singleFileInBothLocalAndRemote": m71, - "singleFileInRemoteOnly": m72, + "singleFileInBothLocalAndRemote": m76, + "singleFileInRemoteOnly": m77, "skip": MessageLookupByLibrary.simpleMessage("Pomiń"), "social": MessageLookupByLibrary.simpleMessage("Społeczność"), "someItemsAreInBothEnteAndYourDevice": @@ -1760,10 +1780,10 @@ class MessageLookup extends MessageLookupByLibrary { "storageInGB": m1, "storageLimitExceeded": MessageLookupByLibrary.simpleMessage("Przekroczono limit pamięci"), - "storageUsageInfo": m73, + "storageUsageInfo": m78, "strongStrength": MessageLookupByLibrary.simpleMessage("Silne"), - "subAlreadyLinkedErrMessage": m74, - "subWillBeCancelledOn": m75, + "subAlreadyLinkedErrMessage": m79, + "subWillBeCancelledOn": m80, "subscribe": MessageLookupByLibrary.simpleMessage("Subskrybuj"), "subscribeToEnableSharing": MessageLookupByLibrary.simpleMessage( "Potrzebujesz aktywnej płatnej subskrypcji, aby włączyć udostępnianie."), @@ -1780,7 +1800,7 @@ class MessageLookup extends MessageLookupByLibrary { "suggestFeatures": MessageLookupByLibrary.simpleMessage("Zaproponuj funkcje"), "support": MessageLookupByLibrary.simpleMessage("Wsparcie techniczne"), - "syncProgress": m76, + "syncProgress": m81, "syncStopped": MessageLookupByLibrary.simpleMessage("Synchronizacja zatrzymana"), "syncing": MessageLookupByLibrary.simpleMessage("Synchronizowanie..."), @@ -1793,7 +1813,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Naciśnij, aby odblokować"), "tapToUpload": MessageLookupByLibrary.simpleMessage("Naciśnij, aby przesłać"), - "tapToUploadIsIgnoredDue": m77, + "tapToUploadIsIgnoredDue": m82, "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"), @@ -1833,7 +1853,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Ten e-mail jest już używany"), "thisImageHasNoExifData": MessageLookupByLibrary.simpleMessage( "Ten obraz nie posiada danych exif"), - "thisIsPersonVerificationId": m78, + "thisIsPersonVerificationId": m83, "thisIsYourVerificationId": MessageLookupByLibrary.simpleMessage( "To jest Twój Identyfikator Weryfikacji"), "thisWillLogYouOutOfTheFollowingDevice": @@ -1857,11 +1877,11 @@ class MessageLookup extends MessageLookupByLibrary { "total": MessageLookupByLibrary.simpleMessage("ogółem"), "totalSize": MessageLookupByLibrary.simpleMessage("Całkowity rozmiar"), "trash": MessageLookupByLibrary.simpleMessage("Kosz"), - "trashDaysLeft": m79, + "trashDaysLeft": m84, "trim": MessageLookupByLibrary.simpleMessage("Przytnij"), "trustedContacts": MessageLookupByLibrary.simpleMessage("Zaufane kontakty"), - "trustedInviteBody": m80, + "trustedInviteBody": m85, "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."), @@ -1881,7 +1901,7 @@ class MessageLookup extends MessageLookupByLibrary { "Pomyślnie zresetowano uwierzytelnianie dwustopniowe"), "twofactorSetup": MessageLookupByLibrary.simpleMessage( "Uwierzytelnianie dwustopniowe"), - "typeOfGallerGallerytypeIsNotSupportedForRename": m81, + "typeOfGallerGallerytypeIsNotSupportedForRename": m86, "unarchive": MessageLookupByLibrary.simpleMessage("Przywróć z archiwum"), "unarchiveAlbum": @@ -1906,10 +1926,10 @@ class MessageLookup extends MessageLookupByLibrary { "updatingFolderSelection": MessageLookupByLibrary.simpleMessage( "Aktualizowanie wyboru folderu..."), "upgrade": MessageLookupByLibrary.simpleMessage("Ulepsz"), - "uploadIsIgnoredDueToIgnorereason": m82, + "uploadIsIgnoredDueToIgnorereason": m87, "uploadingFilesToAlbum": MessageLookupByLibrary.simpleMessage( "Przesyłanie plików do albumu..."), - "uploadingMultipleMemories": m83, + "uploadingMultipleMemories": m88, "uploadingSingleMemory": MessageLookupByLibrary.simpleMessage( "Zachowywanie 1 wspomnienia..."), "upto50OffUntil4thDec": MessageLookupByLibrary.simpleMessage( @@ -1927,7 +1947,7 @@ class MessageLookup extends MessageLookupByLibrary { "useSelectedPhoto": MessageLookupByLibrary.simpleMessage("Użyj zaznaczone zdjęcie"), "usedSpace": MessageLookupByLibrary.simpleMessage("Zajęta przestrzeń"), - "validTill": m84, + "validTill": m89, "verificationFailedPleaseTryAgain": MessageLookupByLibrary.simpleMessage( "Weryfikacja nie powiodła się, spróbuj ponownie"), @@ -1936,7 +1956,7 @@ class MessageLookup extends MessageLookupByLibrary { "verify": MessageLookupByLibrary.simpleMessage("Zweryfikuj"), "verifyEmail": MessageLookupByLibrary.simpleMessage("Zweryfikuj adres e-mail"), - "verifyEmailID": m85, + "verifyEmailID": m90, "verifyIDLabel": MessageLookupByLibrary.simpleMessage("Zweryfikuj"), "verifyPasskey": MessageLookupByLibrary.simpleMessage("Zweryfikuj klucz dostępu"), @@ -1962,7 +1982,7 @@ class MessageLookup extends MessageLookupByLibrary { "viewRecoveryKey": MessageLookupByLibrary.simpleMessage("Zobacz klucz odzyskiwania"), "viewer": MessageLookupByLibrary.simpleMessage("Widz"), - "viewersSuccessfullyAdded": m86, + "viewersSuccessfullyAdded": m91, "visitWebToManage": MessageLookupByLibrary.simpleMessage( "Odwiedź stronę web.ente.io, aby zarządzać subskrypcją"), "waitingForVerification": MessageLookupByLibrary.simpleMessage( @@ -1983,7 +2003,7 @@ class MessageLookup extends MessageLookupByLibrary { "Zaufany kontakt może pomóc w odzyskaniu Twoich danych."), "yearShort": MessageLookupByLibrary.simpleMessage("r"), "yearly": MessageLookupByLibrary.simpleMessage("Rocznie"), - "yearsAgo": m87, + "yearsAgo": m92, "yes": MessageLookupByLibrary.simpleMessage("Tak"), "yesCancel": MessageLookupByLibrary.simpleMessage("Tak, anuluj"), "yesConvertToViewer": @@ -2015,7 +2035,7 @@ class MessageLookup extends MessageLookupByLibrary { "Nie możesz udostępnić samemu sobie"), "youDontHaveAnyArchivedItems": MessageLookupByLibrary.simpleMessage( "Nie masz żadnych zarchiwizowanych elementów."), - "youHaveSuccessfullyFreedUp": m88, + "youHaveSuccessfullyFreedUp": m93, "yourAccountHasBeenDeleted": MessageLookupByLibrary.simpleMessage( "Twoje konto zostało usunięte"), "yourMap": MessageLookupByLibrary.simpleMessage("Twoja mapa"), @@ -2036,9 +2056,6 @@ class MessageLookup extends MessageLookupByLibrary { "Twoja subskrypcja została pomyślnie zaktualizowana"), "yourVerificationCodeHasExpired": MessageLookupByLibrary.simpleMessage( "Twój kod weryfikacyjny wygasł"), - "youveNoDuplicateFilesThatCanBeCleared": - MessageLookupByLibrary.simpleMessage( - "Nie masz duplikatów plików, które mogą być wyczyszczone"), "youveNoFilesInThisAlbumThatCanBeDeleted": MessageLookupByLibrary.simpleMessage( "Nie masz żadnych plików w tym albumie, które można usunąć"), diff --git a/mobile/lib/generated/intl/messages_pt.dart b/mobile/lib/generated/intl/messages_pt.dart index a5db557219..1f04fe0dc1 100644 --- a/mobile/lib/generated/intl/messages_pt.dart +++ b/mobile/lib/generated/intl/messages_pt.dart @@ -20,247 +20,258 @@ typedef String MessageIfAbsent(String messageStr, List args); class MessageLookup extends MessageLookupByLibrary { String get localeName => 'pt'; - static String m9(count) => - "${Intl.plural(count, one: 'Adicionar colaborador', other: 'Adicionar colaboradores')}"; + static String m9(title) => "${title} (Eu)"; static String m10(count) => + "${Intl.plural(count, one: 'Adicionar colaborador', other: 'Adicionar colaboradores')}"; + + static String m11(count) => "${Intl.plural(count, one: 'Adicionar item', other: 'Adicionar itens')}"; - static String m11(storageAmount, endDate) => + static String m12(storageAmount, endDate) => "Seu complemento ${storageAmount} é válido até ${endDate}"; - static String m12(count) => + static String m13(count) => "${Intl.plural(count, one: 'Adicionar visualizador', other: 'Adicionar visualizadores')}"; - static String m13(emailOrName) => "Adicionado por ${emailOrName}"; + static String m14(emailOrName) => "Adicionado por ${emailOrName}"; - static String m14(albumName) => "Adicionado com sucesso a ${albumName}"; + static String m15(albumName) => "Adicionado com sucesso a ${albumName}"; - static String m15(count) => + static String m16(count) => "${Intl.plural(count, zero: 'Nenhum participante', one: '1 participante', other: '${count} participantes')}"; - static String m16(versionValue) => "Versão: ${versionValue}"; + static String m17(versionValue) => "Versão: ${versionValue}"; - static String m17(freeAmount, storageUnit) => + static String m18(freeAmount, storageUnit) => "${freeAmount} ${storageUnit} livre"; - static String m18(paymentProvider) => + static String m19(paymentProvider) => "Primeiramente cancele sua assinatura existente do ${paymentProvider}"; static String m3(user) => "${user} Não poderá adicionar mais fotos a este álbum\n\nEles ainda conseguirão remover fotos existentes adicionadas por eles"; - static String m19(isFamilyMember, storageAmountInGb) => + static String m20(isFamilyMember, storageAmountInGb) => "${Intl.select(isFamilyMember, { '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!', })}"; - static String m20(albumName) => "Link colaborativo criado para ${albumName}"; + static String m21(albumName) => "Link colaborativo criado para ${albumName}"; - static String m21(count) => + static String m22(count) => "${Intl.plural(count, zero: 'Adicionado 0 colaboradores', one: 'Adicionado 1 colaborador', other: 'Adicionado ${count} colaboradores')}"; - static String m22(email, numOfDays) => + static String m23(email, numOfDays) => "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 m23(familyAdminEmail) => + static String m24(familyAdminEmail) => "Entre em contato com ${familyAdminEmail} para gerenciar sua assinatura"; - static String m24(provider) => + static String m25(provider) => "Entre em contato conosco em support@ente.io para gerenciar sua assinatura ${provider}."; - static String m25(endpoint) => "Conectado à ${endpoint}"; + static String m26(endpoint) => "Conectado à ${endpoint}"; - static String m26(count) => + static String m27(count) => "${Intl.plural(count, one: 'Excluir ${count} item', other: 'Excluir ${count} itens')}"; - static String m27(currentlyDeleting, totalCount) => + static String m28(currentlyDeleting, totalCount) => "Excluindo ${currentlyDeleting} / ${totalCount}"; - static String m28(albumName) => + static String m29(albumName) => "Isso removerá o link público para acessar \"${albumName}\"."; - static String m29(supportEmail) => + static String m30(supportEmail) => "Envie um e-mail para ${supportEmail} a partir do seu endereço de e-mail registrado"; - static String m30(count, storageSaved) => + static String m31(count, storageSaved) => "Você limpou ${Intl.plural(count, one: '${count} arquivo duplicado', other: '${count} arquivos duplicados')}, salvando (${storageSaved}!)"; - static String m31(count, formattedSize) => + static String m32(count, formattedSize) => "${count} arquivos, ${formattedSize} cada"; - static String m32(newEmail) => "E-mail alterado para ${newEmail}"; + static String m33(newEmail) => "E-mail alterado para ${newEmail}"; - static String m33(email) => + static String m34(email) => "${email} não possui uma conta Ente."; + + static String m35(email) => "${email} não tem uma conta Ente.\n\nEnvie-os um convite para compartilhar fotos."; - static String m34(text) => "Fotos adicionais encontradas para ${text}"; + static String m36(text) => "Fotos adicionais encontradas para ${text}"; - static String m35(count, formattedNumber) => + static String m37(count, formattedNumber) => "${Intl.plural(count, one: '1 arquivo', other: '${formattedNumber} arquivos')} deste dispositivo foi copiado com segurança"; - static String m36(count, formattedNumber) => + static String m38(count, formattedNumber) => "${Intl.plural(count, one: '1 arquivo', other: '${formattedNumber} arquivos')} deste álbum foi copiado com segurança"; static String m4(storageAmountInGB) => "${storageAmountInGB} GB cada vez que alguém se inscrever a um plano pago e aplicar seu código"; - static String m37(endDate) => "A avaliação grátis acaba em ${endDate}"; + static String m39(endDate) => "A avaliação grátis acaba em ${endDate}"; - static String m38(count) => + static String m40(count) => "Você ainda pode acessá-${Intl.plural(count, one: 'lo', other: 'los')} no Ente, contanto que você tenha uma assinatura ativa"; - static String m39(sizeInMBorGB) => "Liberar ${sizeInMBorGB}"; + static String m41(sizeInMBorGB) => "Liberar ${sizeInMBorGB}"; - static String m40(count, formattedSize) => + static String m42(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 m41(currentlyProcessing, totalCount) => + static String m43(currentlyProcessing, totalCount) => "Processando ${currentlyProcessing} / ${totalCount}"; - static String m42(count) => + static String m44(count) => "${Intl.plural(count, one: '${count} item', other: '${count} itens')}"; - static String m43(email) => + static String m45(email) => "${email} convidou você para ser um contato confiável"; - static String m44(expiryTime) => "O link expirará em ${expiryTime}"; + static String m46(expiryTime) => "O link expirará em ${expiryTime}"; + + static String m47(email) => "Vincular pessoa a ${email}"; + + static String m48(personName, email) => + "Isso vinculará ${personName} a ${email}"; static String m5(count, formattedCount) => "${Intl.plural(count, zero: 'sem memórias', one: '${formattedCount} memória', other: '${formattedCount} memórias')}"; - static String m45(count) => + static String m49(count) => "${Intl.plural(count, one: 'Mover item', other: 'Mover itens')}"; - static String m46(albumName) => "Movido com sucesso para ${albumName}"; + static String m50(albumName) => "Movido com sucesso para ${albumName}"; - static String m47(personName) => "Sem sugestões para ${personName}"; + static String m51(personName) => "Sem sugestões para ${personName}"; - static String m48(name) => "Não é ${name}?"; + static String m52(name) => "Não é ${name}?"; - static String m49(familyAdminEmail) => + static String m53(familyAdminEmail) => "Entre em contato com ${familyAdminEmail} para alterar o seu código."; static String m0(passwordStrengthValue) => "Força da senha: ${passwordStrengthValue}"; - static String m50(providerName) => + static String m54(providerName) => "Fale com o suporte ${providerName} se você foi cobrado"; - static String m51(count) => + static String m55(count) => "${Intl.plural(count, zero: '0 fotos', one: '1 foto', other: '${count} fotos')}"; - static String m52(endDate) => + static String m56(endDate) => "Avaliação grátis válida até ${endDate}.\nVocê pode alterar para um plano pago depois."; - static String m53(toEmail) => "Envie-nos um e-mail para ${toEmail}"; + static String m57(toEmail) => "Envie-nos um e-mail para ${toEmail}"; - static String m54(toEmail) => "Envie os registros para \n${toEmail}"; + static String m58(toEmail) => "Envie os registros para \n${toEmail}"; - static String m55(folderName) => "Processando ${folderName}..."; + static String m59(folderName) => "Processando ${folderName}..."; - static String m56(storeName) => "Avalie-nos no ${storeName}"; + static String m60(storeName) => "Avalie-nos no ${storeName}"; - static String m57(days, email) => + static String m61(name) => "Atribuído a ${name}"; + + static String m62(days, email) => "Você poderá acessar a conta após ${days} dias. Uma notificação será enviada para ${email}."; - static String m58(email) => + static String m63(email) => "Você pode recuperar a conta com e-mail ${email} por definir uma nova senha."; - static String m59(email) => "${email} está tentando recuperar sua conta."; + static String m64(email) => "${email} está tentando recuperar sua conta."; - static String m60(storageInGB) => + static String m65(storageInGB) => "3. Ambos os dois ganham ${storageInGB} GB* grátis"; - static String m61(userEmail) => + static String m66(userEmail) => "${userEmail} será removido deste álbum compartilhado\n\nQuaisquer fotos adicionadas por eles também serão removidas do álbum"; - static String m62(endDate) => "Renovação de assinatura em ${endDate}"; + static String m67(endDate) => "Renovação de assinatura em ${endDate}"; - static String m63(count) => + static String m68(count) => "${Intl.plural(count, one: '${count} resultado encontrado', other: '${count} resultados encontrados')}"; - static String m64(snapshotLength, searchLength) => + static String m69(snapshotLength, searchLength) => "Incompatibilidade de comprimento de seções: ${snapshotLength} != ${searchLength}"; static String m6(count) => "${count} selecionado(s)"; - static String m65(count, yourCount) => + static String m70(count, yourCount) => "${count} selecionado(s) (${yourCount} seus)"; - static String m66(verificationID) => + static String m71(verificationID) => "Aqui está meu ID de verificação para o ente.io: ${verificationID}"; static String m7(verificationID) => "Ei, você pode confirmar se este ID de verificação do ente.io é seu?: ${verificationID}"; - static String m67(referralCode, referralStorageInGB) => + static String m72(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 m68(numberOfPeople) => + static String m73(numberOfPeople) => "${Intl.plural(numberOfPeople, zero: 'Compartilhe com pessoas específicas', one: 'Compartilhado com 1 pessoa', other: 'Compartilhado com ${numberOfPeople} pessoas')}"; - static String m69(emailIDs) => "Compartilhado com ${emailIDs}"; + static String m74(emailIDs) => "Compartilhado com ${emailIDs}"; - static String m70(fileType) => + static String m75(fileType) => "Este ${fileType} será excluído do dispositivo."; - static String m71(fileType) => + static String m76(fileType) => "Este ${fileType} está no Ente e em seu dispositivo."; - static String m72(fileType) => "Este ${fileType} será excluído do Ente."; + static String m77(fileType) => "Este ${fileType} será excluído do Ente."; static String m1(storageAmountInGB) => "${storageAmountInGB} GB"; - static String m73( + static String m78( usedAmount, usedStorageUnit, totalAmount, totalStorageUnit) => "${usedAmount} ${usedStorageUnit} de ${totalAmount} ${totalStorageUnit} usado"; - static String m74(id) => + static String m79(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 m75(endDate) => "Sua assinatura será cancelada em ${endDate}"; + static String m80(endDate) => "Sua assinatura será cancelada em ${endDate}"; - static String m76(completed, total) => + static String m81(completed, total) => "${completed}/${total} memórias preservadas"; - static String m77(ignoreReason) => + static String m82(ignoreReason) => "Toque para enviar, atualmente o envio é ignorado devido a ${ignoreReason}"; static String m8(storageAmountInGB) => "Eles também recebem ${storageAmountInGB} GB"; - static String m78(email) => "Este é o ID de verificação de ${email}"; + static String m83(email) => "Este é o ID de verificação de ${email}"; - static String m79(count) => + static String m84(count) => "${Intl.plural(count, zero: 'Em breve', one: '1 dia', other: '${count} dias')}"; - static String m80(email) => + static String m85(email) => "Você foi convidado para ser um contato legado por ${email}."; - static String m81(galleryType) => + static String m86(galleryType) => "O tipo de galeria ${galleryType} não é suportado para renomear"; - static String m82(ignoreReason) => + static String m87(ignoreReason) => "O envio é ignorado devido a ${ignoreReason}"; - static String m83(count) => "Preservando ${count} memórias..."; + static String m88(count) => "Preservando ${count} memórias..."; - static String m84(endDate) => "Válido até ${endDate}"; + static String m89(endDate) => "Válido até ${endDate}"; - static String m85(email) => "Verificar ${email}"; + static String m90(email) => "Verificar ${email}"; - static String m86(count) => + static String m91(count) => "${Intl.plural(count, zero: 'Adicionado 0 visualizadores', one: 'Adicionado 1 visualizador', other: 'Adicionado ${count} visualizadores')}"; - static String m2(email) => "Nós enviamos um e-mail à ${email}"; + static String m2(email) => "Enviamos um e-mail à ${email}"; - static String m87(count) => + static String m92(count) => "${Intl.plural(count, one: '${count} ano atrás', other: '${count} anos atrás')}"; - static String m88(storageSaved) => + static String m93(storageSaved) => "Você liberou ${storageSaved} com sucesso!"; final messages = _notInlinedMessages(_notInlinedMessages); @@ -273,6 +284,7 @@ class MessageLookup extends MessageLookupByLibrary { "account": MessageLookupByLibrary.simpleMessage("Conta"), "accountIsAlreadyConfigured": MessageLookupByLibrary.simpleMessage( "A conta já está configurada."), + "accountOwnerPersonAppbarTitle": m9, "accountWelcomeBack": MessageLookupByLibrary.simpleMessage("Bem-vindo(a) de volta!"), "ackPasswordLostWarning": MessageLookupByLibrary.simpleMessage( @@ -285,11 +297,11 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Adicionar um novo e-mail"), "addCollaborator": MessageLookupByLibrary.simpleMessage("Adicionar colaborador"), - "addCollaborators": m9, + "addCollaborators": m10, "addFiles": MessageLookupByLibrary.simpleMessage("Adicionar arquivos"), "addFromDevice": MessageLookupByLibrary.simpleMessage("Adicionar do dispositivo"), - "addItem": m10, + "addItem": m11, "addLocation": MessageLookupByLibrary.simpleMessage("Adicionar localização"), "addLocationButton": MessageLookupByLibrary.simpleMessage("Adicionar"), @@ -302,7 +314,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Adicionar nova pessoa"), "addOnPageSubtitle": MessageLookupByLibrary.simpleMessage("Detalhes dos complementos"), - "addOnValidTill": m11, + "addOnValidTill": m12, "addOns": MessageLookupByLibrary.simpleMessage("Complementos"), "addPhotos": MessageLookupByLibrary.simpleMessage("Adicionar fotos"), "addSelected": @@ -316,12 +328,12 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Adicionar contato confiável"), "addViewer": MessageLookupByLibrary.simpleMessage("Adicionar visualizador"), - "addViewers": m12, + "addViewers": m13, "addYourPhotosNow": MessageLookupByLibrary.simpleMessage("Adicione suas fotos agora"), "addedAs": MessageLookupByLibrary.simpleMessage("Adicionado como"), - "addedBy": m13, - "addedSuccessfullyTo": m14, + "addedBy": m14, + "addedSuccessfullyTo": m15, "addingToFavorites": MessageLookupByLibrary.simpleMessage( "Adicionando aos favoritos..."), "advanced": MessageLookupByLibrary.simpleMessage("Avançado"), @@ -332,7 +344,7 @@ class MessageLookup extends MessageLookupByLibrary { "after1Week": MessageLookupByLibrary.simpleMessage("Após 1 semana"), "after1Year": MessageLookupByLibrary.simpleMessage("Após 1 ano"), "albumOwner": MessageLookupByLibrary.simpleMessage("Proprietário"), - "albumParticipantsCount": m15, + "albumParticipantsCount": m16, "albumTitle": MessageLookupByLibrary.simpleMessage("Título do álbum"), "albumUpdated": MessageLookupByLibrary.simpleMessage("Álbum atualizado"), @@ -380,7 +392,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Bloqueio do aplicativo"), "appLockDescriptions": MessageLookupByLibrary.simpleMessage( "Escolha entre a tela de bloqueio padrão do seu dispositivo e uma tela de bloqueio personalizada com PIN ou senha."), - "appVersion": m16, + "appVersion": m17, "appleId": MessageLookupByLibrary.simpleMessage("ID da Apple"), "apply": MessageLookupByLibrary.simpleMessage("Aplicar"), "applyCodeTitle": @@ -463,7 +475,7 @@ class MessageLookup extends MessageLookupByLibrary { "autoPairDesc": MessageLookupByLibrary.simpleMessage( "O pareamento automático só funciona com dispositivos que suportam o Chromecast."), "available": MessageLookupByLibrary.simpleMessage("Disponível"), - "availableStorageSpace": m17, + "availableStorageSpace": m18, "backedUpFolders": MessageLookupByLibrary.simpleMessage( "Pastas copiadas com segurança"), "backup": MessageLookupByLibrary.simpleMessage("Cópia de segurança"), @@ -505,7 +517,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Cancelar recuperação"), "cancelAccountRecoveryBody": MessageLookupByLibrary.simpleMessage( "Deseja mesmo cancelar a recuperação de conta?"), - "cancelOtherSubscription": m18, + "cancelOtherSubscription": m19, "cancelSubscription": MessageLookupByLibrary.simpleMessage("Cancelar assinatura"), "cannotAddMorePhotosAfterBecomingViewer": m3, @@ -555,7 +567,7 @@ class MessageLookup extends MessageLookupByLibrary { "Reivindicar armazenamento grátis"), "claimMore": MessageLookupByLibrary.simpleMessage("Reivindique mais!"), "claimed": MessageLookupByLibrary.simpleMessage("Reivindicado"), - "claimedStorageSoFar": m19, + "claimedStorageSoFar": m20, "cleanUncategorized": MessageLookupByLibrary.simpleMessage("Limpar não categorizado"), "cleanUncategorizedDescription": MessageLookupByLibrary.simpleMessage( @@ -584,12 +596,12 @@ class MessageLookup extends MessageLookupByLibrary { "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."), "collaborativeLink": MessageLookupByLibrary.simpleMessage("Link colaborativo"), - "collaborativeLinkCreatedFor": m20, + "collaborativeLinkCreatedFor": m21, "collaborator": MessageLookupByLibrary.simpleMessage("Colaborador"), "collaboratorsCanAddPhotosAndVideosToTheSharedAlbum": MessageLookupByLibrary.simpleMessage( "Os colaboradores podem adicionar fotos e vídeos ao álbum compartilhado."), - "collaboratorsSuccessfullyAdded": m21, + "collaboratorsSuccessfullyAdded": m22, "collageLayout": MessageLookupByLibrary.simpleMessage("Layout"), "collageSaved": MessageLookupByLibrary.simpleMessage("Colagem salva na galeria"), @@ -606,7 +618,7 @@ class MessageLookup extends MessageLookupByLibrary { "Você tem certeza que queira desativar a autenticação de dois fatores?"), "confirmAccountDeletion": MessageLookupByLibrary.simpleMessage("Confirmar exclusão da conta"), - "confirmAddingTrustedContact": m22, + "confirmAddingTrustedContact": m23, "confirmDeletePrompt": MessageLookupByLibrary.simpleMessage( "Sim, eu quero permanentemente excluir esta conta e os dados em todos os aplicativos."), "confirmPassword": @@ -619,10 +631,10 @@ class MessageLookup extends MessageLookupByLibrary { "Confirme sua chave de recuperação"), "connectToDevice": MessageLookupByLibrary.simpleMessage("Conectar ao dispositivo"), - "contactFamilyAdmin": m23, + "contactFamilyAdmin": m24, "contactSupport": MessageLookupByLibrary.simpleMessage("Contatar suporte"), - "contactToManageSubscription": m24, + "contactToManageSubscription": m25, "contacts": MessageLookupByLibrary.simpleMessage("Contatos"), "contents": MessageLookupByLibrary.simpleMessage("Conteúdos"), "continueLabel": MessageLookupByLibrary.simpleMessage("Continuar"), @@ -667,7 +679,7 @@ class MessageLookup extends MessageLookupByLibrary { "currentlyRunning": MessageLookupByLibrary.simpleMessage("Atualmente executando"), "custom": MessageLookupByLibrary.simpleMessage("Personalizado"), - "customEndpoint": m25, + "customEndpoint": m26, "darkTheme": MessageLookupByLibrary.simpleMessage("Escuro"), "dayToday": MessageLookupByLibrary.simpleMessage("Hoje"), "dayYesterday": MessageLookupByLibrary.simpleMessage("Ontem"), @@ -694,7 +706,7 @@ class MessageLookup extends MessageLookupByLibrary { "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."), "deleteEmailRequest": MessageLookupByLibrary.simpleMessage( - "Por favor, envie um e-mail à account-deletion@ente.io do seu endereço de e-mail registrado."), + "Por favor, envie um e-mail a account-deletion@ente.io do seu endereço de e-mail registrado."), "deleteEmptyAlbums": MessageLookupByLibrary.simpleMessage("Excluir álbuns vazios"), "deleteEmptyAlbumsWithQuestionMark": @@ -705,11 +717,11 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Excluir do dispositivo"), "deleteFromEnte": MessageLookupByLibrary.simpleMessage("Excluir do Ente"), - "deleteItemCount": m26, + "deleteItemCount": m27, "deleteLocation": MessageLookupByLibrary.simpleMessage("Excluir localização"), "deletePhotos": MessageLookupByLibrary.simpleMessage("Excluir fotos"), - "deleteProgress": m27, + "deleteProgress": m28, "deleteReason1": MessageLookupByLibrary.simpleMessage( "Está faltando um recurso-chave que eu preciso"), "deleteReason2": MessageLookupByLibrary.simpleMessage( @@ -750,7 +762,7 @@ class MessageLookup extends MessageLookupByLibrary { "Os visualizadores podem fazer capturas de tela ou salvar uma cópia de suas fotos usando ferramentas externas"), "disableDownloadWarningTitle": MessageLookupByLibrary.simpleMessage("Por favor, saiba que"), - "disableLinkMessage": m28, + "disableLinkMessage": m29, "disableTwofactor": MessageLookupByLibrary.simpleMessage( "Desativar autenticação de dois fatores"), "disablingTwofactorAuthentication": @@ -787,15 +799,16 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage( "Você quer descartar as edições que você fez?"), "done": MessageLookupByLibrary.simpleMessage("Concluído"), + "dontSave": MessageLookupByLibrary.simpleMessage("Não salvar"), "doubleYourStorage": MessageLookupByLibrary.simpleMessage("Duplique seu armazenamento"), "download": MessageLookupByLibrary.simpleMessage("Baixar"), "downloadFailed": MessageLookupByLibrary.simpleMessage("Falhou ao baixar"), "downloading": MessageLookupByLibrary.simpleMessage("Baixando..."), - "dropSupportEmail": m29, - "duplicateFileCountWithStorageSaved": m30, - "duplicateItemsGroup": m31, + "dropSupportEmail": m30, + "duplicateFileCountWithStorageSaved": m31, + "duplicateItemsGroup": m32, "edit": MessageLookupByLibrary.simpleMessage("Editar"), "editLocation": MessageLookupByLibrary.simpleMessage("Editar localização"), @@ -810,8 +823,9 @@ class MessageLookup extends MessageLookupByLibrary { "email": MessageLookupByLibrary.simpleMessage("E-mail"), "emailAlreadyRegistered": MessageLookupByLibrary.simpleMessage("E-mail já registrado."), - "emailChangedTo": m32, - "emailNoEnteAccount": m33, + "emailChangedTo": m33, + "emailDoesNotHaveEnteAccount": m34, + "emailNoEnteAccount": m35, "emailNotRegistered": MessageLookupByLibrary.simpleMessage("E-mail não registrado."), "emailVerificationToggle": @@ -845,7 +859,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage( "Ente pode criptografar e preservar arquivos apenas se você conceder acesso a eles"), "entePhotosPerm": MessageLookupByLibrary.simpleMessage( - "Ente precisa de sua permissão para preservar suas fotos"), + "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."), "enteSubscriptionShareWithFamily": MessageLookupByLibrary.simpleMessage( @@ -896,12 +910,13 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Exportar dados"), "extraPhotosFound": MessageLookupByLibrary.simpleMessage( "Fotos adicionais encontradas"), - "extraPhotosFoundFor": m34, + "extraPhotosFoundFor": m36, "faceNotClusteredYet": MessageLookupByLibrary.simpleMessage( "Rosto não agrupado ainda, volte aqui mais tarde"), "faceRecognition": MessageLookupByLibrary.simpleMessage("Reconhecimento facial"), "faces": MessageLookupByLibrary.simpleMessage("Rostos"), + "failed": MessageLookupByLibrary.simpleMessage("Falhou"), "failedToApplyCode": MessageLookupByLibrary.simpleMessage("Falhou ao aplicar código"), "failedToCancel": @@ -947,8 +962,8 @@ class MessageLookup extends MessageLookupByLibrary { "fileTypes": MessageLookupByLibrary.simpleMessage("Tipos de arquivo"), "fileTypesAndNames": MessageLookupByLibrary.simpleMessage("Tipos de arquivo e nomes"), - "filesBackedUpFromDevice": m35, - "filesBackedUpInAlbum": m36, + "filesBackedUpFromDevice": m37, + "filesBackedUpInAlbum": m38, "filesDeleted": MessageLookupByLibrary.simpleMessage("Arquivos excluídos"), "filesSavedToGallery": @@ -970,22 +985,22 @@ class MessageLookup extends MessageLookupByLibrary { "freeStorageUsable": MessageLookupByLibrary.simpleMessage("Armazenamento disponível"), "freeTrial": MessageLookupByLibrary.simpleMessage("Avaliação grátis"), - "freeTrialValidTill": m37, - "freeUpAccessPostDelete": m38, - "freeUpAmount": m39, + "freeTrialValidTill": m39, + "freeUpAccessPostDelete": m40, + "freeUpAmount": m41, "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": m40, + "freeUpSpaceSaving": m42, "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": m41, + "genericProgress": m43, "goToSettings": MessageLookupByLibrary.simpleMessage("Ir às opções"), "googlePlayId": MessageLookupByLibrary.simpleMessage("ID do Google Play"), @@ -1044,6 +1059,7 @@ class MessageLookup extends MessageLookupByLibrary { "indexedItems": MessageLookupByLibrary.simpleMessage("Itens indexados"), "indexingIsPaused": MessageLookupByLibrary.simpleMessage( "A indexação parou, ela será retomada automaticamente quando o dispositivo estiver pronto."), + "ineligible": MessageLookupByLibrary.simpleMessage("Inelegível"), "info": MessageLookupByLibrary.simpleMessage("Info"), "insecureDevice": MessageLookupByLibrary.simpleMessage("Dispositivo inseguro"), @@ -1068,7 +1084,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": m44, "itemsShowTheNumberOfDaysRemainingBeforePermanentDeletion": MessageLookupByLibrary.simpleMessage( "Os itens exibem o número de dias restantes antes da exclusão permanente"), @@ -1076,6 +1092,8 @@ class MessageLookup extends MessageLookupByLibrary { "Os itens selecionados serão removidos deste álbum"), "join": MessageLookupByLibrary.simpleMessage("Unir-se"), "joinAlbum": MessageLookupByLibrary.simpleMessage("Unir-se ao álbum"), + "joinAlbumConfirmationDialogBody": MessageLookupByLibrary.simpleMessage( + "Unir-se ao álbum fará que seu e-mail seja visível a todos do álbum."), "joinAlbumSubtext": MessageLookupByLibrary.simpleMessage( "para visualizar e adicionar suas fotos"), "joinAlbumSubtextViewer": MessageLookupByLibrary.simpleMessage( @@ -1097,12 +1115,13 @@ class MessageLookup extends MessageLookupByLibrary { "Sair do álbum compartilhado?"), "left": MessageLookupByLibrary.simpleMessage("Esquerda"), "legacy": MessageLookupByLibrary.simpleMessage("Legado"), - "legacyAccounts": MessageLookupByLibrary.simpleMessage("Contas legado"), - "legacyInvite": m43, + "legacyAccounts": + MessageLookupByLibrary.simpleMessage("Contas legadas"), + "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, e se não for cancelado dentro de 30 dias, redefina sua senha e acesse sua conta."), + "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"), "lightTheme": MessageLookupByLibrary.simpleMessage("Claro"), "link": MessageLookupByLibrary.simpleMessage("Vincular"), @@ -1111,13 +1130,20 @@ class MessageLookup extends MessageLookupByLibrary { "linkDeviceLimit": MessageLookupByLibrary.simpleMessage("Limite do dispositivo"), "linkEmail": MessageLookupByLibrary.simpleMessage("Vincular e-mail"), + "linkEmailToContactBannerCaption": + MessageLookupByLibrary.simpleMessage("para compartilhar rápido"), "linkEnabled": MessageLookupByLibrary.simpleMessage("Ativado"), "linkExpired": MessageLookupByLibrary.simpleMessage("Expirado"), - "linkExpiresOn": m44, + "linkExpiresOn": m46, "linkExpiry": MessageLookupByLibrary.simpleMessage("Expiração do link"), "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": m47, + "linkPersonToEmailConfirmation": m48, "livePhotos": MessageLookupByLibrary.simpleMessage("Fotos animadas"), "loadMessage1": MessageLookupByLibrary.simpleMessage( "Você pode compartilhar sua assinatura com seus familiares"), @@ -1207,6 +1233,7 @@ class MessageLookup extends MessageLookupByLibrary { "maps": MessageLookupByLibrary.simpleMessage("Mapas"), "mastodon": MessageLookupByLibrary.simpleMessage("Mastodon"), "matrix": MessageLookupByLibrary.simpleMessage("Matrix"), + "me": MessageLookupByLibrary.simpleMessage("Eu"), "memoryCount": m5, "merchandise": MessageLookupByLibrary.simpleMessage("Produtos"), "mergeWithExisting": @@ -1236,12 +1263,12 @@ class MessageLookup extends MessageLookupByLibrary { "moreDetails": MessageLookupByLibrary.simpleMessage("Mais detalhes"), "mostRecent": MessageLookupByLibrary.simpleMessage("Mais recente"), "mostRelevant": MessageLookupByLibrary.simpleMessage("Mais relevante"), - "moveItem": m45, + "moveItem": m49, "moveToAlbum": MessageLookupByLibrary.simpleMessage("Mover para o álbum"), "moveToHiddenAlbum": MessageLookupByLibrary.simpleMessage("Mover ao álbum oculto"), - "movedSuccessfullyTo": m46, + "movedSuccessfullyTo": m50, "movedToTrash": MessageLookupByLibrary.simpleMessage("Movido para a lixeira"), "movingFilesToAlbum": MessageLookupByLibrary.simpleMessage( @@ -1294,10 +1321,10 @@ class MessageLookup extends MessageLookupByLibrary { "noResults": MessageLookupByLibrary.simpleMessage("Nenhum resultado"), "noResultsFound": MessageLookupByLibrary.simpleMessage("Nenhum resultado encontrado"), - "noSuggestionsForPerson": m47, + "noSuggestionsForPerson": m51, "noSystemLockFound": MessageLookupByLibrary.simpleMessage( "Nenhum bloqueio do sistema encontrado"), - "notPersonLabel": m48, + "notPersonLabel": m52, "nothingSharedWithYouYet": MessageLookupByLibrary.simpleMessage( "Nada compartilhado com você ainda"), "nothingToSeeHere": @@ -1307,7 +1334,7 @@ class MessageLookup extends MessageLookupByLibrary { "onDevice": MessageLookupByLibrary.simpleMessage("No dispositivo"), "onEnte": MessageLookupByLibrary.simpleMessage( "No ente"), - "onlyFamilyAdminCanChangeCode": m49, + "onlyFamilyAdminCanChangeCode": m53, "onlyThem": MessageLookupByLibrary.simpleMessage("Apenas eles"), "oops": MessageLookupByLibrary.simpleMessage("Ops"), "oopsCouldNotSaveEdits": MessageLookupByLibrary.simpleMessage( @@ -1330,8 +1357,8 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Ou mesclar com existente"), "orPickAnExistingOne": MessageLookupByLibrary.simpleMessage("Ou escolha um existente"), - "orPickFromYourContacts": - MessageLookupByLibrary.simpleMessage("or pick from your contacts"), + "orPickFromYourContacts": MessageLookupByLibrary.simpleMessage( + "ou escolher dos seus contatos"), "pair": MessageLookupByLibrary.simpleMessage("Parear"), "pairWithPin": MessageLookupByLibrary.simpleMessage("Parear com PIN"), "pairingComplete": @@ -1358,7 +1385,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": m50, + "paymentFailedTalkToProvider": m54, "pendingItems": MessageLookupByLibrary.simpleMessage("Itens pendentes"), "pendingSync": MessageLookupByLibrary.simpleMessage("Sincronização pendente"), @@ -1381,14 +1408,18 @@ class MessageLookup extends MessageLookupByLibrary { "photosAddedByYouWillBeRemovedFromTheAlbum": MessageLookupByLibrary.simpleMessage( "Suas fotos adicionadas serão removidas do álbum"), - "photosCount": m51, + "photosCount": m55, "pickCenterPoint": MessageLookupByLibrary.simpleMessage("Escolha o ponto central"), "pinAlbum": MessageLookupByLibrary.simpleMessage("Fixar álbum"), "pinLock": MessageLookupByLibrary.simpleMessage("Bloqueio por PIN"), "playOnTv": MessageLookupByLibrary.simpleMessage("Reproduzir álbum na TV"), - "playStoreFreeTrialValidTill": m52, + "playOriginal": + MessageLookupByLibrary.simpleMessage("Reproduzir original"), + "playStoreFreeTrialValidTill": m56, + "playStream": + MessageLookupByLibrary.simpleMessage("Reproduzir transmissão"), "playstoreSubscription": MessageLookupByLibrary.simpleMessage("Assinatura da PlayStore"), "pleaseCheckYourInternetConnectionAndTryAgain": @@ -1400,14 +1431,14 @@ class MessageLookup extends MessageLookupByLibrary { "pleaseContactSupportIfTheProblemPersists": MessageLookupByLibrary.simpleMessage( "Por favor, contate o suporte se o problema persistir"), - "pleaseEmailUsAt": m53, + "pleaseEmailUsAt": m57, "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": m54, + "pleaseSendTheLogsTo": m58, "pleaseTryAgain": MessageLookupByLibrary.simpleMessage("Tente novamente"), "pleaseVerifyTheCodeYouHaveEntered": @@ -1434,18 +1465,26 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Compartilhamento privado"), "proceed": MessageLookupByLibrary.simpleMessage("Continuar"), "processed": MessageLookupByLibrary.simpleMessage("Processado"), - "processingImport": m55, + "processing": MessageLookupByLibrary.simpleMessage("Processando"), + "processingImport": m59, + "processingVideos": + MessageLookupByLibrary.simpleMessage("Processando vídeos"), "publicLinkCreated": MessageLookupByLibrary.simpleMessage("Link público criado"), "publicLinkEnabled": MessageLookupByLibrary.simpleMessage("Link público ativo"), + "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": m56, + "rateUsOnStore": m60, + "reassignMe": MessageLookupByLibrary.simpleMessage("Reatribuir \"Eu\""), + "reassignedToName": m61, + "reassigningLoading": + MessageLookupByLibrary.simpleMessage("Reatribuindo..."), "recover": MessageLookupByLibrary.simpleMessage("Recuperar"), "recoverAccount": MessageLookupByLibrary.simpleMessage("Recuperar conta"), @@ -1454,7 +1493,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Recuperar conta"), "recoveryInitiated": MessageLookupByLibrary.simpleMessage("A recuperação iniciou"), - "recoveryInitiatedDesc": m57, + "recoveryInitiatedDesc": m62, "recoveryKey": MessageLookupByLibrary.simpleMessage("Chave de recuperação"), "recoveryKeyCopiedToClipboard": MessageLookupByLibrary.simpleMessage( @@ -1469,12 +1508,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": m58, + "recoveryReady": m63, "recoverySuccessful": MessageLookupByLibrary.simpleMessage("Recuperação com sucesso!"), "recoveryWarning": MessageLookupByLibrary.simpleMessage( "Um contato confiável está tentando acessar sua conta"), - "recoveryWarningBody": m59, + "recoveryWarningBody": m64, "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": @@ -1489,7 +1528,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": m60, + "referralStep3": m65, "referrals": MessageLookupByLibrary.simpleMessage("Referências"), "referralsAreCurrentlyPaused": MessageLookupByLibrary.simpleMessage( "As referências estão atualmente pausadas"), @@ -1518,7 +1557,7 @@ class MessageLookup extends MessageLookupByLibrary { "removeLink": MessageLookupByLibrary.simpleMessage("Remover link"), "removeParticipant": MessageLookupByLibrary.simpleMessage("Remover participante"), - "removeParticipantBody": m61, + "removeParticipantBody": m66, "removePersonLabel": MessageLookupByLibrary.simpleMessage("Remover etiqueta da pessoa"), "removePublicLink": @@ -1538,7 +1577,7 @@ class MessageLookup extends MessageLookupByLibrary { "renameFile": MessageLookupByLibrary.simpleMessage("Renomear arquivo"), "renewSubscription": MessageLookupByLibrary.simpleMessage("Renovar assinatura"), - "renewsOn": m62, + "renewsOn": m67, "reportABug": MessageLookupByLibrary.simpleMessage("Informar um erro"), "reportBug": MessageLookupByLibrary.simpleMessage("Informar erro"), "resendEmail": MessageLookupByLibrary.simpleMessage("Reenviar e-mail"), @@ -1571,6 +1610,9 @@ class MessageLookup extends MessageLookupByLibrary { "safelyStored": MessageLookupByLibrary.simpleMessage("Armazenado com segurança"), "save": MessageLookupByLibrary.simpleMessage("Salvar"), + "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"), @@ -1617,8 +1659,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": m63, - "searchSectionsLengthMismatch": m64, + "searchResultCount": m68, + "searchSectionsLengthMismatch": m69, "security": MessageLookupByLibrary.simpleMessage("Segurança"), "seePublicAlbumLinksInApp": MessageLookupByLibrary.simpleMessage( "Ver links de álbum compartilhado no aplicativo"), @@ -1641,7 +1683,11 @@ class MessageLookup extends MessageLookupByLibrary { "Selecionar aplicativo de e-mail"), "selectMorePhotos": MessageLookupByLibrary.simpleMessage("Selecionar mais fotos"), + "selectPersonToLink": MessageLookupByLibrary.simpleMessage( + "Selecione a pessoa para vincular"), "selectReason": MessageLookupByLibrary.simpleMessage("Diga o motivo"), + "selectYourFace": + MessageLookupByLibrary.simpleMessage("Selecione seu rosto"), "selectYourPlan": MessageLookupByLibrary.simpleMessage("Selecione seu plano"), "selectedFilesAreNotOnEnte": MessageLookupByLibrary.simpleMessage( @@ -1653,7 +1699,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage( "Os itens selecionados serão excluídos de todos os álbuns e movidos para a lixeira."), "selectedPhotos": m6, - "selectedPhotosWithYours": m65, + "selectedPhotosWithYours": m70, "send": MessageLookupByLibrary.simpleMessage("Enviar"), "sendEmail": MessageLookupByLibrary.simpleMessage("Enviar e-mail"), "sendInvite": MessageLookupByLibrary.simpleMessage("Enviar convite"), @@ -1683,16 +1729,16 @@ class MessageLookup extends MessageLookupByLibrary { "shareAnAlbumNow": MessageLookupByLibrary.simpleMessage("Compartilhar um álbum agora"), "shareLink": MessageLookupByLibrary.simpleMessage("Compartilhar link"), - "shareMyVerificationID": m66, + "shareMyVerificationID": m71, "shareOnlyWithThePeopleYouWant": MessageLookupByLibrary.simpleMessage( "Compartilhar apenas com as pessoas que você quiser"), "shareTextConfirmOthersVerificationID": m7, "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": m67, + "shareTextReferralCode": m72, "shareWithNonenteUsers": MessageLookupByLibrary.simpleMessage( "Compartilhar com usuários não ente"), - "shareWithPeopleSectionTitle": m68, + "shareWithPeopleSectionTitle": m73, "shareYourFirstAlbum": MessageLookupByLibrary.simpleMessage( "Compartilhar seu primeiro álbum"), "sharedAlbumSectionDescription": MessageLookupByLibrary.simpleMessage( @@ -1705,7 +1751,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": m69, + "sharedWith": m74, "sharedWithMe": MessageLookupByLibrary.simpleMessage("Compartilhado comigo"), "sharedWithYou": @@ -1722,11 +1768,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": m70, + "singleFileDeleteFromDevice": m75, "singleFileDeleteHighlight": MessageLookupByLibrary.simpleMessage( "Ele será excluído de todos os álbuns."), - "singleFileInBothLocalAndRemote": m71, - "singleFileInRemoteOnly": m72, + "singleFileInBothLocalAndRemote": m76, + "singleFileInRemoteOnly": m77, "skip": MessageLookupByLibrary.simpleMessage("Pular"), "social": MessageLookupByLibrary.simpleMessage("Redes sociais"), "someItemsAreInBothEnteAndYourDevice": @@ -1777,10 +1823,12 @@ class MessageLookup extends MessageLookupByLibrary { "storageInGB": m1, "storageLimitExceeded": MessageLookupByLibrary.simpleMessage( "Limite de armazenamento excedido"), - "storageUsageInfo": m73, + "storageUsageInfo": m78, + "streamDetails": + MessageLookupByLibrary.simpleMessage("Detalhes da transmissão"), "strongStrength": MessageLookupByLibrary.simpleMessage("Forte"), - "subAlreadyLinkedErrMessage": m74, - "subWillBeCancelledOn": m75, + "subAlreadyLinkedErrMessage": m79, + "subWillBeCancelledOn": m80, "subscribe": MessageLookupByLibrary.simpleMessage("Inscrever-se"), "subscribeToEnableSharing": MessageLookupByLibrary.simpleMessage( "Você precisa de uma inscrição paga ativa para ativar o compartilhamento."), @@ -1797,7 +1845,7 @@ class MessageLookup extends MessageLookupByLibrary { "suggestFeatures": MessageLookupByLibrary.simpleMessage("Sugerir recurso"), "support": MessageLookupByLibrary.simpleMessage("Suporte"), - "syncProgress": m76, + "syncProgress": m81, "syncStopped": MessageLookupByLibrary.simpleMessage("Sincronização interrompida"), "syncing": MessageLookupByLibrary.simpleMessage("Sincronizando..."), @@ -1809,7 +1857,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Toque para desbloquear"), "tapToUpload": MessageLookupByLibrary.simpleMessage("Toque para enviar"), - "tapToUploadIsIgnoredDue": m77, + "tapToUploadIsIgnoredDue": m82, "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"), @@ -1847,7 +1895,9 @@ class MessageLookup extends MessageLookupByLibrary { "Este e-mail já está sendo usado"), "thisImageHasNoExifData": MessageLookupByLibrary.simpleMessage( "Esta imagem não possui dados EXIF"), - "thisIsPersonVerificationId": m78, + "thisIsMeExclamation": + MessageLookupByLibrary.simpleMessage("Este é você!"), + "thisIsPersonVerificationId": m83, "thisIsYourVerificationId": MessageLookupByLibrary.simpleMessage( "Este é o seu ID de verificação"), "thisWillLogYouOutOfTheFollowingDevice": @@ -1871,11 +1921,11 @@ class MessageLookup extends MessageLookupByLibrary { "total": MessageLookupByLibrary.simpleMessage("total"), "totalSize": MessageLookupByLibrary.simpleMessage("Tamanho total"), "trash": MessageLookupByLibrary.simpleMessage("Lixeira"), - "trashDaysLeft": m79, + "trashDaysLeft": m84, "trim": MessageLookupByLibrary.simpleMessage("Recortar"), "trustedContacts": MessageLookupByLibrary.simpleMessage("Contatos confiáveis"), - "trustedInviteBody": m80, + "trustedInviteBody": m85, "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."), @@ -1894,7 +1944,7 @@ class MessageLookup extends MessageLookupByLibrary { "Autenticação de dois fatores redefinida com sucesso"), "twofactorSetup": MessageLookupByLibrary.simpleMessage( "Configuração de dois fatores"), - "typeOfGallerGallerytypeIsNotSupportedForRename": m81, + "typeOfGallerGallerytypeIsNotSupportedForRename": m86, "unarchive": MessageLookupByLibrary.simpleMessage("Desarquivar"), "unarchiveAlbum": MessageLookupByLibrary.simpleMessage("Desarquivar álbum"), @@ -1917,10 +1967,10 @@ class MessageLookup extends MessageLookupByLibrary { "updatingFolderSelection": MessageLookupByLibrary.simpleMessage( "Atualizando seleção de pasta..."), "upgrade": MessageLookupByLibrary.simpleMessage("Atualizar"), - "uploadIsIgnoredDueToIgnorereason": m82, + "uploadIsIgnoredDueToIgnorereason": m87, "uploadingFilesToAlbum": MessageLookupByLibrary.simpleMessage( "Enviando arquivos para o álbum..."), - "uploadingMultipleMemories": m83, + "uploadingMultipleMemories": m88, "uploadingSingleMemory": MessageLookupByLibrary.simpleMessage("Preservando 1 memória..."), "upto50OffUntil4thDec": MessageLookupByLibrary.simpleMessage( @@ -1938,7 +1988,7 @@ class MessageLookup extends MessageLookupByLibrary { "useSelectedPhoto": MessageLookupByLibrary.simpleMessage("Usar foto selecionada"), "usedSpace": MessageLookupByLibrary.simpleMessage("Espaço usado"), - "validTill": m84, + "validTill": m89, "verificationFailedPleaseTryAgain": MessageLookupByLibrary.simpleMessage( "Falha na verificação. Tente novamente"), @@ -1946,7 +1996,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("ID de verificação"), "verify": MessageLookupByLibrary.simpleMessage("Verificar"), "verifyEmail": MessageLookupByLibrary.simpleMessage("Verificar e-mail"), - "verifyEmailID": m85, + "verifyEmailID": m90, "verifyIDLabel": MessageLookupByLibrary.simpleMessage("Verificar"), "verifyPasskey": MessageLookupByLibrary.simpleMessage("Verificar chave de acesso"), @@ -1958,6 +2008,8 @@ class MessageLookup extends MessageLookupByLibrary { "videoInfo": MessageLookupByLibrary.simpleMessage("Informações do 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"), @@ -1974,7 +2026,7 @@ class MessageLookup extends MessageLookupByLibrary { "viewRecoveryKey": MessageLookupByLibrary.simpleMessage("Ver chave de recuperação"), "viewer": MessageLookupByLibrary.simpleMessage("Visualizador"), - "viewersSuccessfullyAdded": m86, + "viewersSuccessfullyAdded": m91, "visitWebToManage": MessageLookupByLibrary.simpleMessage( "Visite o web.ente.io para gerenciar sua assinatura"), "waitingForVerification": @@ -1996,7 +2048,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": m87, + "yearsAgo": m92, "yes": MessageLookupByLibrary.simpleMessage("Sim"), "yesCancel": MessageLookupByLibrary.simpleMessage("Sim"), "yesConvertToViewer": MessageLookupByLibrary.simpleMessage( @@ -2029,7 +2081,7 @@ class MessageLookup extends MessageLookupByLibrary { "Você não pode compartilhar consigo mesmo"), "youDontHaveAnyArchivedItems": MessageLookupByLibrary.simpleMessage( "Você não tem nenhum item arquivado."), - "youHaveSuccessfullyFreedUp": m88, + "youHaveSuccessfullyFreedUp": m93, "yourAccountHasBeenDeleted": MessageLookupByLibrary.simpleMessage("Sua conta foi excluída"), "yourMap": MessageLookupByLibrary.simpleMessage("Seu mapa"), @@ -2052,7 +2104,7 @@ class MessageLookup extends MessageLookupByLibrary { "O código de verificação expirou"), "youveNoDuplicateFilesThatCanBeCleared": MessageLookupByLibrary.simpleMessage( - "Você não tem arquivos duplicados que possam ser limpos"), + "Você não possui nenhum arquivo duplicado que possa ser excluído"), "youveNoFilesInThisAlbumThatCanBeDeleted": MessageLookupByLibrary.simpleMessage( "Você não tem arquivos neste álbum que possam ser excluídos"), diff --git a/mobile/lib/generated/intl/messages_ro.dart b/mobile/lib/generated/intl/messages_ro.dart index 8b0e700e7f..e0c2131c2c 100644 --- a/mobile/lib/generated/intl/messages_ro.dart +++ b/mobile/lib/generated/intl/messages_ro.dart @@ -20,37 +20,37 @@ typedef String MessageIfAbsent(String messageStr, List args); class MessageLookup extends MessageLookupByLibrary { String get localeName => 'ro'; - static String m9(count) => + static String m10(count) => "${Intl.plural(count, one: 'Adăugați un colaborator', few: 'Adăugați colaboratori', other: 'Adăugați colaboratori')}"; - static String m10(count) => + static String m11(count) => "${Intl.plural(count, one: 'Adăugați articolul', few: 'Adăugați articolele', other: 'Adăugați articolele')}"; - static String m11(storageAmount, endDate) => + static String m12(storageAmount, endDate) => "Suplimentul de ${storageAmount} este valabil până pe ${endDate}"; - static String m12(count) => + static String m13(count) => "${Intl.plural(count, one: 'Adăugați observator', few: 'Adăugați observatori', other: 'Adăugați observatori')}"; - static String m13(emailOrName) => "Adăugat de ${emailOrName}"; + static String m14(emailOrName) => "Adăugat de ${emailOrName}"; - static String m14(albumName) => "S-au adăugat cu succes la ${albumName}"; + static String m15(albumName) => "S-au adăugat cu succes la ${albumName}"; - static String m15(count) => + static String m16(count) => "${Intl.plural(count, zero: 'Fără participanți', one: '1 participant', other: '${count} de participanți')}"; - static String m16(versionValue) => "Versiune: ${versionValue}"; + static String m17(versionValue) => "Versiune: ${versionValue}"; - static String m17(freeAmount, storageUnit) => + static String m18(freeAmount, storageUnit) => "${freeAmount} ${storageUnit} liber"; - static String m18(paymentProvider) => + static String m19(paymentProvider) => "Vă rugăm să vă anulați mai întâi abonamentul existent de la ${paymentProvider}"; static String m3(user) => "${user} nu va putea să mai adauge fotografii la acest album\n\nVa putea să elimine fotografii existente adăugate de el/ea"; - static String m19(isFamilyMember, storageAmountInGb) => + static String m20(isFamilyMember, storageAmountInGb) => "${Intl.select(isFamilyMember, { 'true': 'Familia dvs. a revendicat ${storageAmountInGb} GB până acum', @@ -58,214 +58,208 @@ class MessageLookup extends MessageLookupByLibrary { 'other': 'Ați revendicat ${storageAmountInGb} de GB până acum!', })}"; - static String m20(albumName) => "Link colaborativ creat pentru ${albumName}"; + static String m21(albumName) => "Link colaborativ creat pentru ${albumName}"; - static String m21(count) => - "${Intl.plural(count, zero: 'S-au adăugat 0 colaboratori', one: 'S-a adăugat 1 colaborator', few: 'S-au adăugat ${count} colaboratori', other: 'S-au adăugat ${count} de colaboratori')}"; - - static String m22(email, numOfDays) => + static String m23(email, numOfDays) => "Sunteți pe cale să adăugați ${email} ca persoană de contact de încredere. Acesta va putea să vă recupereze contul dacă lipsiți timp de ${numOfDays} de zile."; - static String m23(familyAdminEmail) => + static String m24(familyAdminEmail) => "Vă rugăm să contactați ${familyAdminEmail} pentru a gestiona abonamentul"; - static String m24(provider) => + static String m25(provider) => "Vă rugăm să ne contactați la support@ente.io pentru a vă gestiona abonamentul ${provider}."; - static String m25(endpoint) => "Conectat la ${endpoint}"; + static String m26(endpoint) => "Conectat la ${endpoint}"; - static String m26(count) => + static String m27(count) => "${Intl.plural(count, one: 'Ștergeți ${count} articol', other: 'Ștergeți ${count} de articole')}"; - static String m27(currentlyDeleting, totalCount) => + static String m28(currentlyDeleting, totalCount) => "Se șterg ${currentlyDeleting} / ${totalCount}"; - static String m28(albumName) => + static String m29(albumName) => "Urmează să eliminați linkul public pentru accesarea „${albumName}”."; - static String m29(supportEmail) => + static String m30(supportEmail) => "Vă rugăm să trimiteți un e-mail la ${supportEmail} de pe adresa de e-mail înregistrată"; - static String m30(count, storageSaved) => + static String m31(count, storageSaved) => "Ați curățat ${Intl.plural(count, one: '${count} dublură', few: '${count} dubluri', other: '${count} de dubluri')}, economisind (${storageSaved}!)"; - static String m31(count, formattedSize) => + static String m32(count, formattedSize) => "${count} fișiere, ${formattedSize} fiecare"; - static String m32(newEmail) => "E-mail modificat în ${newEmail}"; + static String m33(newEmail) => "E-mail modificat în ${newEmail}"; - static String m33(email) => + static String m35(email) => "${email} nu are un cont Ente.\n\nTrimiteți-le o invitație pentru a distribui fotografii."; - static String m34(text) => "S-au găsit fotografii extra pentru ${text}"; + static String m36(text) => "S-au găsit fotografii extra pentru ${text}"; - static String m35(count, formattedNumber) => + static String m37(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 m36(count, formattedNumber) => + static String m38(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 m4(storageAmountInGB) => "${storageAmountInGB} GB de fiecare dată când cineva se înscrie pentru un plan plătit și aplică codul dvs."; - static String m37(endDate) => + static String m39(endDate) => "Perioadă de încercare valabilă până pe ${endDate}"; - static String m38(count) => + static String m40(count) => "Încă ${Intl.plural(count, one: 'îl puteți', few: 'le puteți', other: 'le puteți')} accesa pe Ente cât timp aveți un abonament activ"; - static String m39(sizeInMBorGB) => "Eliberați ${sizeInMBorGB}"; + static String m41(sizeInMBorGB) => "Eliberați ${sizeInMBorGB}"; - static String m40(count, formattedSize) => + static String m42(count, formattedSize) => "${Intl.plural(count, one: 'Poate fi șters de pe dispozitiv pentru a elibera ${formattedSize}', few: 'Pot fi șterse de pe dispozitiv pentru a elibera ${formattedSize}', other: 'Pot fi șterse de pe dispozitiv pentru a elibera ${formattedSize}')}"; - static String m41(currentlyProcessing, totalCount) => + static String m43(currentlyProcessing, totalCount) => "Se procesează ${currentlyProcessing} / ${totalCount}"; - static String m42(count) => + static String m44(count) => "${Intl.plural(count, one: '${count} articol', few: '${count} articole', other: '${count} de articole')}"; - static String m43(email) => + static String m45(email) => "${email} v-a invitat să fiți un contact de încredere"; - static String m44(expiryTime) => "Linkul va expira pe ${expiryTime}"; + static String m46(expiryTime) => "Linkul va expira pe ${expiryTime}"; static String m5(count, formattedCount) => "${Intl.plural(count, one: '${formattedCount} amintire', few: '${formattedCount} amintiri', other: '${formattedCount} de amintiri')}"; - static String m45(count) => + static String m49(count) => "${Intl.plural(count, one: 'Mutați articolul', few: 'Mutați articole', other: 'Mutați articolele')}"; - static String m46(albumName) => "S-au mutat cu succes în ${albumName}"; + static String m50(albumName) => "S-au mutat cu succes în ${albumName}"; - static String m47(personName) => "Nicio sugestie pentru ${personName}"; + static String m51(personName) => "Nicio sugestie pentru ${personName}"; - static String m48(name) => "Nu este ${name}?"; + static String m52(name) => "Nu este ${name}?"; - static String m49(familyAdminEmail) => + static String m53(familyAdminEmail) => "Vă rugăm să contactați ${familyAdminEmail} pentru a vă schimba codul."; static String m0(passwordStrengthValue) => "Complexitatea parolei: ${passwordStrengthValue}"; - static String m50(providerName) => + static String m54(providerName) => "Vă rugăm să vorbiți cu asistența ${providerName} dacă ați fost taxat"; - static String m51(count) => - "${Intl.plural(count, zero: '0 fotografii', one: '1 fotografie', few: '${count} fotografii', other: '${count} de fotografii')}"; + static String m55(count) => + "${Intl.plural(count, zero: '0 Fotografii', one: 'O Fotografie', other: '${count} Fotografii')}"; - static String m52(endDate) => + static String m56(endDate) => "Perioada de încercare gratuită valabilă până pe ${endDate}.\nUlterior, puteți opta pentru un plan plătit."; - static String m53(toEmail) => + static String m57(toEmail) => "Vă rugăm să ne trimiteți un e-mail la ${toEmail}"; - static String m54(toEmail) => + static String m58(toEmail) => "Vă rugăm să trimiteți jurnalele la \n${toEmail}"; - static String m55(folderName) => "Se procesează ${folderName}..."; + static String m59(folderName) => "Se procesează ${folderName}..."; - static String m56(storeName) => "Evaluați-ne pe ${storeName}"; + static String m60(storeName) => "Evaluați-ne pe ${storeName}"; - static String m57(days, email) => + static String m62(days, email) => "Puteți accesa contul după ${days} zile. O notificare va fi trimisă la ${email}."; - static String m58(email) => + static String m63(email) => "Acum puteți recupera contul ${email} setând o nouă parolă."; - static String m59(email) => "${email} încearcă să vă recupereze contul."; + static String m64(email) => "${email} încearcă să vă recupereze contul."; - static String m60(storageInGB) => + static String m65(storageInGB) => "3. Amândoi primiți ${storageInGB} GB* gratuit"; - static String m61(userEmail) => + static String m66(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 m62(endDate) => "Abonamentul se reînnoiește pe ${endDate}"; + static String m67(endDate) => "Abonamentul se reînnoiește pe ${endDate}"; - static String m63(count) => + static String m68(count) => "${Intl.plural(count, one: '${count} rezultat găsit', few: '${count} rezultate găsite', other: '${count} de rezultate găsite')}"; - static String m64(snapshotLength, searchLength) => + static String m69(snapshotLength, searchLength) => "Lungimea secțiunilor nu se potrivesc: ${snapshotLength} != ${searchLength}"; static String m6(count) => "${count} selectate"; - static String m65(count, yourCount) => + static String m70(count, yourCount) => "${count} selectate (${yourCount} ale dvs.)"; - static String m66(verificationID) => + static String m71(verificationID) => "Acesta este ID-ul meu de verificare: ${verificationID} pentru ente.io."; static String m7(verificationID) => "Poți confirma că acesta este ID-ul tău de verificare ente.io: ${verificationID}"; - static String m67(referralCode, referralStorageInGB) => + static String m72(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 m68(numberOfPeople) => + static String m73(numberOfPeople) => "${Intl.plural(numberOfPeople, zero: 'Distribuiți cu anumite persoane', one: 'Distribuit cu o persoană', other: 'Distribuit cu ${numberOfPeople} de persoane')}"; - static String m69(emailIDs) => "Distribuit cu ${emailIDs}"; + static String m74(emailIDs) => "Distribuit cu ${emailIDs}"; - static String m70(fileType) => + static String m75(fileType) => "Fișierul de tip ${fileType} va fi șters din dispozitivul dvs."; - static String m71(fileType) => + static String m76(fileType) => "Fișierul de tip ${fileType} este atât în Ente, cât și în dispozitivul dvs."; - static String m72(fileType) => + static String m77(fileType) => "Fișierul de tip ${fileType} va fi șters din Ente."; static String m1(storageAmountInGB) => "${storageAmountInGB} GB"; - static String m73( + static String m78( usedAmount, usedStorageUnit, totalAmount, totalStorageUnit) => "${usedAmount} ${usedStorageUnit} din ${totalAmount} ${totalStorageUnit} utilizat"; - static String m74(id) => + static String m79(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 m75(endDate) => "Abonamentul dvs. va fi anulat pe ${endDate}"; + static String m80(endDate) => "Abonamentul dvs. va fi anulat pe ${endDate}"; - static String m76(completed, total) => + static String m81(completed, total) => "${completed}/${total} amintiri salvate"; - static String m77(ignoreReason) => + static String m82(ignoreReason) => "Atingeți pentru a încărca, încărcarea este ignorată în prezent datorită ${ignoreReason}"; static String m8(storageAmountInGB) => "De asemenea, va primii ${storageAmountInGB} GB"; - static String m78(email) => "Acesta este ID-ul de verificare al ${email}"; + static String m83(email) => "Acesta este ID-ul de verificare al ${email}"; - static String m79(count) => + static String m84(count) => "${Intl.plural(count, zero: 'Curând', one: 'O zi', other: '${count} de zile')}"; - static String m80(email) => + static String m85(email) => "Ați fost învitat să fiți un contact de moștenire de către ${email}."; - static String m81(galleryType) => + static String m86(galleryType) => "Tipul de galerie ${galleryType} nu este acceptat pentru redenumire"; - static String m82(ignoreReason) => + static String m87(ignoreReason) => "Încărcare ignorată din motivul ${ignoreReason}"; - static String m83(count) => "Se salvează ${count} amintiri..."; + static String m88(count) => "Se salvează ${count} amintiri..."; - static String m84(endDate) => "Valabil până pe ${endDate}"; + static String m89(endDate) => "Valabil până pe ${endDate}"; - static String m85(email) => "Verificare ${email}"; - - static String m86(count) => - "${Intl.plural(count, zero: 'S-au adăugat 0 observatori', one: 'S-a adăugat 1 observator', few: 'S-au adăugat ${count} observatori', other: 'S-au adăugat ${count} de observatori')}"; + static String m90(email) => "Verificare ${email}"; static String m2(email) => "Am trimis un e-mail la ${email}"; - static String m87(count) => + static String m92(count) => "${Intl.plural(count, one: 'acum ${count} an', few: 'acum ${count} ani', other: 'acum ${count} de ani')}"; - static String m88(storageSaved) => "Ați eliberat cu succes ${storageSaved}!"; + static String m93(storageSaved) => "Ați eliberat cu succes ${storageSaved}!"; final messages = _notInlinedMessages(_notInlinedMessages); static Map _notInlinedMessages(_) => { @@ -289,11 +283,11 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Adăugați un e-mail nou"), "addCollaborator": MessageLookupByLibrary.simpleMessage("Adăugare colaborator"), - "addCollaborators": m9, + "addCollaborators": m10, "addFiles": MessageLookupByLibrary.simpleMessage("Adăugați fișiere"), "addFromDevice": MessageLookupByLibrary.simpleMessage("Adăugați de pe dispozitiv"), - "addItem": m10, + "addItem": m11, "addLocation": MessageLookupByLibrary.simpleMessage("Adăugare locație"), "addLocationButton": MessageLookupByLibrary.simpleMessage("Adăugare"), "addMore": MessageLookupByLibrary.simpleMessage("Adăugați mai mulți"), @@ -305,7 +299,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Adăugare persoană nouă"), "addOnPageSubtitle": MessageLookupByLibrary.simpleMessage("Detaliile suplimentelor"), - "addOnValidTill": m11, + "addOnValidTill": m12, "addOns": MessageLookupByLibrary.simpleMessage("Suplimente"), "addPhotos": MessageLookupByLibrary.simpleMessage("Adăugați fotografii"), @@ -319,12 +313,12 @@ class MessageLookup extends MessageLookupByLibrary { "Adăugare contact de încredere"), "addViewer": MessageLookupByLibrary.simpleMessage("Adăugare observator"), - "addViewers": m12, + "addViewers": m13, "addYourPhotosNow": MessageLookupByLibrary.simpleMessage( "Adăugați-vă fotografiile acum"), "addedAs": MessageLookupByLibrary.simpleMessage("Adăugat ca"), - "addedBy": m13, - "addedSuccessfullyTo": m14, + "addedBy": m14, + "addedSuccessfullyTo": m15, "addingToFavorites": MessageLookupByLibrary.simpleMessage("Se adaugă la favorite..."), "advanced": MessageLookupByLibrary.simpleMessage("Avansat"), @@ -335,7 +329,7 @@ class MessageLookup extends MessageLookupByLibrary { "after1Week": MessageLookupByLibrary.simpleMessage("După o săptămâna"), "after1Year": MessageLookupByLibrary.simpleMessage("După un an"), "albumOwner": MessageLookupByLibrary.simpleMessage("Proprietar"), - "albumParticipantsCount": m15, + "albumParticipantsCount": m16, "albumTitle": MessageLookupByLibrary.simpleMessage("Titlu album"), "albumUpdated": MessageLookupByLibrary.simpleMessage("Album actualizat"), @@ -384,7 +378,7 @@ class MessageLookup extends MessageLookupByLibrary { "appLock": MessageLookupByLibrary.simpleMessage("Blocare aplicație"), "appLockDescriptions": MessageLookupByLibrary.simpleMessage( "Alegeți între ecranul de blocare implicit al dispozitivului dvs. și un ecran de blocare personalizat cu PIN sau parolă."), - "appVersion": m16, + "appVersion": m17, "appleId": MessageLookupByLibrary.simpleMessage("Apple ID"), "apply": MessageLookupByLibrary.simpleMessage("Aplicare"), "applyCodeTitle": @@ -467,7 +461,7 @@ class MessageLookup extends MessageLookupByLibrary { "autoPairDesc": MessageLookupByLibrary.simpleMessage( "Asocierea automată funcționează numai cu dispozitive care acceptă Chromecast."), "available": MessageLookupByLibrary.simpleMessage("Disponibil"), - "availableStorageSpace": m17, + "availableStorageSpace": m18, "backedUpFolders": MessageLookupByLibrary.simpleMessage("Foldere salvate"), "backup": MessageLookupByLibrary.simpleMessage("Copie de rezervă"), @@ -508,7 +502,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Anulare recuperare"), "cancelAccountRecoveryBody": MessageLookupByLibrary.simpleMessage( "Sunteți sigur că doriți să anulați recuperarea?"), - "cancelOtherSubscription": m18, + "cancelOtherSubscription": m19, "cancelSubscription": MessageLookupByLibrary.simpleMessage("Anulare abonament"), "cannotAddMorePhotosAfterBecomingViewer": m3, @@ -563,7 +557,7 @@ class MessageLookup extends MessageLookupByLibrary { "claimMore": MessageLookupByLibrary.simpleMessage("Revendicați mai multe!"), "claimed": MessageLookupByLibrary.simpleMessage("Revendicat"), - "claimedStorageSoFar": m19, + "claimedStorageSoFar": m20, "cleanUncategorized": MessageLookupByLibrary.simpleMessage("Curățare Necategorisite"), "cleanUncategorizedDescription": MessageLookupByLibrary.simpleMessage( @@ -594,12 +588,11 @@ class MessageLookup extends MessageLookupByLibrary { "Creați un link pentru a permite oamenilor să adauge și să vizualizeze fotografii în albumul dvs. distribuit, fără a avea nevoie de o aplicație sau un cont Ente. Excelent pentru colectarea fotografiilor de la evenimente."), "collaborativeLink": MessageLookupByLibrary.simpleMessage("Link colaborativ"), - "collaborativeLinkCreatedFor": m20, + "collaborativeLinkCreatedFor": m21, "collaborator": MessageLookupByLibrary.simpleMessage("Colaborator"), "collaboratorsCanAddPhotosAndVideosToTheSharedAlbum": MessageLookupByLibrary.simpleMessage( "Colaboratorii pot adăuga fotografii și videoclipuri la albumul distribuit."), - "collaboratorsSuccessfullyAdded": m21, "collageLayout": MessageLookupByLibrary.simpleMessage("Aspect"), "collageSaved": MessageLookupByLibrary.simpleMessage("Colaj salvat în galerie"), @@ -617,7 +610,7 @@ class MessageLookup extends MessageLookupByLibrary { "Sigur doriți dezactivarea autentificării cu doi factori?"), "confirmAccountDeletion": MessageLookupByLibrary.simpleMessage( "Confirmați ștergerea contului"), - "confirmAddingTrustedContact": m22, + "confirmAddingTrustedContact": m23, "confirmDeletePrompt": MessageLookupByLibrary.simpleMessage( "Da, doresc să șterg definitiv acest cont și toate datele sale din toate aplicațiile."), "confirmPassword": @@ -630,10 +623,10 @@ class MessageLookup extends MessageLookupByLibrary { "Confirmați cheia de recuperare"), "connectToDevice": MessageLookupByLibrary.simpleMessage("Conectați-vă la dispozitiv"), - "contactFamilyAdmin": m23, + "contactFamilyAdmin": m24, "contactSupport": MessageLookupByLibrary.simpleMessage( "Contactați serviciul de asistență"), - "contactToManageSubscription": m24, + "contactToManageSubscription": m25, "contacts": MessageLookupByLibrary.simpleMessage("Contacte"), "contents": MessageLookupByLibrary.simpleMessage("Conținuturi"), "continueLabel": MessageLookupByLibrary.simpleMessage("Continuare"), @@ -679,7 +672,7 @@ class MessageLookup extends MessageLookupByLibrary { "currentlyRunning": MessageLookupByLibrary.simpleMessage("rulează în prezent"), "custom": MessageLookupByLibrary.simpleMessage("Particularizat"), - "customEndpoint": m25, + "customEndpoint": m26, "darkTheme": MessageLookupByLibrary.simpleMessage("Întunecată"), "dayToday": MessageLookupByLibrary.simpleMessage("Astăzi"), "dayYesterday": MessageLookupByLibrary.simpleMessage("Ieri"), @@ -716,12 +709,12 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Ștergeți de pe dispozitiv"), "deleteFromEnte": MessageLookupByLibrary.simpleMessage("Ștergeți din Ente"), - "deleteItemCount": m26, + "deleteItemCount": m27, "deleteLocation": MessageLookupByLibrary.simpleMessage("Ștergeți locația"), "deletePhotos": MessageLookupByLibrary.simpleMessage("Ștergeți fotografiile"), - "deleteProgress": m27, + "deleteProgress": m28, "deleteReason1": MessageLookupByLibrary.simpleMessage( "Lipsește o funcție cheie de care am nevoie"), "deleteReason2": MessageLookupByLibrary.simpleMessage( @@ -729,7 +722,7 @@ class MessageLookup extends MessageLookupByLibrary { "deleteReason3": MessageLookupByLibrary.simpleMessage( "Am găsit un alt serviciu care îmi place mai mult"), "deleteReason4": - MessageLookupByLibrary.simpleMessage("Motivul meu nu este listat"), + MessageLookupByLibrary.simpleMessage("Motivul meu nu apare"), "deleteRequestSLAText": MessageLookupByLibrary.simpleMessage( "Solicitarea dvs. va fi procesată în 72 de ore."), "deleteSharedAlbum": MessageLookupByLibrary.simpleMessage( @@ -762,7 +755,7 @@ class MessageLookup extends MessageLookupByLibrary { "Observatorii pot să facă capturi de ecran sau să salveze o copie a fotografiilor dvs. folosind instrumente externe"), "disableDownloadWarningTitle": MessageLookupByLibrary.simpleMessage("Rețineți"), - "disableLinkMessage": m28, + "disableLinkMessage": m29, "disableTwofactor": MessageLookupByLibrary.simpleMessage( "Dezactivați al doilea factor"), "disablingTwofactorAuthentication": @@ -803,9 +796,9 @@ class MessageLookup extends MessageLookupByLibrary { "downloadFailed": MessageLookupByLibrary.simpleMessage("Descărcarea nu a reușit"), "downloading": MessageLookupByLibrary.simpleMessage("Se descarcă..."), - "dropSupportEmail": m29, - "duplicateFileCountWithStorageSaved": m30, - "duplicateItemsGroup": m31, + "dropSupportEmail": m30, + "duplicateFileCountWithStorageSaved": m31, + "duplicateItemsGroup": m32, "edit": MessageLookupByLibrary.simpleMessage("Editare"), "editLocation": MessageLookupByLibrary.simpleMessage("Editare locaţie"), "editLocationTagTitle": @@ -819,8 +812,8 @@ class MessageLookup extends MessageLookupByLibrary { "email": MessageLookupByLibrary.simpleMessage("E-mail"), "emailAlreadyRegistered": MessageLookupByLibrary.simpleMessage("E-mail deja înregistrat."), - "emailChangedTo": m32, - "emailNoEnteAccount": m33, + "emailChangedTo": m33, + "emailNoEnteAccount": m35, "emailNotRegistered": MessageLookupByLibrary.simpleMessage( "E-mailul nu este înregistrat."), "emailVerificationToggle": MessageLookupByLibrary.simpleMessage( @@ -907,7 +900,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Export de date"), "extraPhotosFound": MessageLookupByLibrary.simpleMessage("S-au găsit fotografii extra"), - "extraPhotosFoundFor": m34, + "extraPhotosFoundFor": m36, "faceNotClusteredYet": MessageLookupByLibrary.simpleMessage( "Fața nu este încă grupată, vă rugăm să reveniți mai târziu"), "faceRecognition": @@ -958,8 +951,8 @@ class MessageLookup extends MessageLookupByLibrary { "fileTypes": MessageLookupByLibrary.simpleMessage("Tipuri de fișiere"), "fileTypesAndNames": MessageLookupByLibrary.simpleMessage( "Tipuri de fișiere și denumiri"), - "filesBackedUpFromDevice": m35, - "filesBackedUpInAlbum": m36, + "filesBackedUpFromDevice": m37, + "filesBackedUpInAlbum": m38, "filesDeleted": MessageLookupByLibrary.simpleMessage("Fișiere șterse"), "filesSavedToGallery": MessageLookupByLibrary.simpleMessage("Fișiere salvate în galerie"), @@ -979,22 +972,22 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Spațiu gratuit utilizabil"), "freeTrial": MessageLookupByLibrary.simpleMessage( "Perioadă de încercare gratuită"), - "freeTrialValidTill": m37, - "freeUpAccessPostDelete": m38, - "freeUpAmount": m39, + "freeTrialValidTill": m39, + "freeUpAccessPostDelete": m40, + "freeUpAmount": m41, "freeUpDeviceSpace": MessageLookupByLibrary.simpleMessage( "Eliberați spațiu pe dispozitiv"), "freeUpDeviceSpaceDesc": MessageLookupByLibrary.simpleMessage( "Economisiți spațiu pe dispozitivul dvs. prin ștergerea fișierelor cărora li s-a făcut copie de rezervă."), "freeUpSpace": MessageLookupByLibrary.simpleMessage("Eliberați spațiu"), - "freeUpSpaceSaving": m40, + "freeUpSpaceSaving": m42, "gallery": MessageLookupByLibrary.simpleMessage("Galerie"), "galleryMemoryLimitInfo": MessageLookupByLibrary.simpleMessage( "Până la 1000 de amintiri afișate în galerie"), "general": MessageLookupByLibrary.simpleMessage("General"), "generatingEncryptionKeys": MessageLookupByLibrary.simpleMessage( "Se generează cheile de criptare..."), - "genericProgress": m41, + "genericProgress": m43, "goToSettings": MessageLookupByLibrary.simpleMessage("Mergeți la setări"), "googlePlayId": MessageLookupByLibrary.simpleMessage("ID Google Play"), @@ -1078,7 +1071,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": m44, "itemsShowTheNumberOfDaysRemainingBeforePermanentDeletion": MessageLookupByLibrary.simpleMessage( "Articolele afișează numărul de zile rămase până la ștergerea definitivă"), @@ -1110,7 +1103,7 @@ class MessageLookup extends MessageLookupByLibrary { "legacy": MessageLookupByLibrary.simpleMessage("Moștenire"), "legacyAccounts": MessageLookupByLibrary.simpleMessage("Conturi de moștenire"), - "legacyInvite": m43, + "legacyInvite": m45, "legacyPageDesc": MessageLookupByLibrary.simpleMessage( "Moștenirea permite contactelor de încredere să vă acceseze contul în absența dvs."), "legacyPageDesc2": MessageLookupByLibrary.simpleMessage( @@ -1123,7 +1116,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Limită de dispozitive"), "linkEnabled": MessageLookupByLibrary.simpleMessage("Activat"), "linkExpired": MessageLookupByLibrary.simpleMessage("Expirat"), - "linkExpiresOn": m44, + "linkExpiresOn": m46, "linkExpiry": MessageLookupByLibrary.simpleMessage("Expirarea linkului"), "linkHasExpired": @@ -1251,11 +1244,11 @@ class MessageLookup extends MessageLookupByLibrary { "mostRecent": MessageLookupByLibrary.simpleMessage("Cele mai recente"), "mostRelevant": MessageLookupByLibrary.simpleMessage("Cele mai relevante"), - "moveItem": m45, + "moveItem": m49, "moveToAlbum": MessageLookupByLibrary.simpleMessage("Mutare în album"), "moveToHiddenAlbum": MessageLookupByLibrary.simpleMessage("Mutați în albumul ascuns"), - "movedSuccessfullyTo": m46, + "movedSuccessfullyTo": m50, "movedToTrash": MessageLookupByLibrary.simpleMessage("S-a mutat în coșul de gunoi"), "movingFilesToAlbum": MessageLookupByLibrary.simpleMessage( @@ -1307,10 +1300,10 @@ class MessageLookup extends MessageLookupByLibrary { "noResults": MessageLookupByLibrary.simpleMessage("Niciun rezultat"), "noResultsFound": MessageLookupByLibrary.simpleMessage("Nu s-au găsit rezultate"), - "noSuggestionsForPerson": m47, + "noSuggestionsForPerson": m51, "noSystemLockFound": MessageLookupByLibrary.simpleMessage( "Nu s-a găsit nicio blocare de sistem"), - "notPersonLabel": m48, + "notPersonLabel": m52, "nothingSharedWithYouYet": MessageLookupByLibrary.simpleMessage( "Nimic distribuit cu dvs. încă"), "nothingToSeeHere": @@ -1320,7 +1313,7 @@ class MessageLookup extends MessageLookupByLibrary { "onDevice": MessageLookupByLibrary.simpleMessage("Pe dispozitiv"), "onEnte": MessageLookupByLibrary.simpleMessage( "Pe ente"), - "onlyFamilyAdminCanChangeCode": m49, + "onlyFamilyAdminCanChangeCode": m53, "onlyThem": MessageLookupByLibrary.simpleMessage("Numai el/ea"), "oops": MessageLookupByLibrary.simpleMessage("Ups"), "oopsCouldNotSaveEdits": MessageLookupByLibrary.simpleMessage( @@ -1344,8 +1337,6 @@ class MessageLookup extends MessageLookupByLibrary { "Sau îmbinați cu cele existente"), "orPickAnExistingOne": MessageLookupByLibrary.simpleMessage("Sau alegeți unul existent"), - "orPickFromYourContacts": - MessageLookupByLibrary.simpleMessage("or pick from your contacts"), "pair": MessageLookupByLibrary.simpleMessage("Asociere"), "pairWithPin": MessageLookupByLibrary.simpleMessage("Asociere cu PIN"), "pairingComplete": @@ -1372,7 +1363,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": m50, + "paymentFailedTalkToProvider": m54, "pendingItems": MessageLookupByLibrary.simpleMessage("Elemente în așteptare"), "pendingSync": @@ -1396,13 +1387,13 @@ class MessageLookup extends MessageLookupByLibrary { "photosAddedByYouWillBeRemovedFromTheAlbum": MessageLookupByLibrary.simpleMessage( "Fotografiile adăugate de dvs. vor fi eliminate din album"), - "photosCount": m51, + "photosCount": m55, "pickCenterPoint": MessageLookupByLibrary.simpleMessage("Alegeți punctul central"), "pinAlbum": MessageLookupByLibrary.simpleMessage("Fixați albumul"), "pinLock": MessageLookupByLibrary.simpleMessage("Blocare PIN"), "playOnTv": MessageLookupByLibrary.simpleMessage("Redare album pe TV"), - "playStoreFreeTrialValidTill": m52, + "playStoreFreeTrialValidTill": m56, "playstoreSubscription": MessageLookupByLibrary.simpleMessage("Abonament PlayStore"), "pleaseCheckYourInternetConnectionAndTryAgain": @@ -1414,14 +1405,14 @@ class MessageLookup extends MessageLookupByLibrary { "pleaseContactSupportIfTheProblemPersists": MessageLookupByLibrary.simpleMessage( "Vă rugăm să contactați asistența dacă problema persistă"), - "pleaseEmailUsAt": m53, + "pleaseEmailUsAt": m57, "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": m54, + "pleaseSendTheLogsTo": m58, "pleaseTryAgain": MessageLookupByLibrary.simpleMessage( "Vă rugăm să încercați din nou"), "pleaseVerifyTheCodeYouHaveEntered": @@ -1451,7 +1442,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Distribuire privată"), "proceed": MessageLookupByLibrary.simpleMessage("Continuați"), "processed": MessageLookupByLibrary.simpleMessage("Procesate"), - "processingImport": m55, + "processingImport": m59, "publicLinkCreated": MessageLookupByLibrary.simpleMessage("Link public creat"), "publicLinkEnabled": @@ -1463,7 +1454,7 @@ class MessageLookup extends MessageLookupByLibrary { "rateTheApp": MessageLookupByLibrary.simpleMessage("Evaluați aplicația"), "rateUs": MessageLookupByLibrary.simpleMessage("Evaluați-ne"), - "rateUsOnStore": m56, + "rateUsOnStore": m60, "recover": MessageLookupByLibrary.simpleMessage("Recuperare"), "recoverAccount": MessageLookupByLibrary.simpleMessage("Recuperare cont"), @@ -1472,7 +1463,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Recuperare cont"), "recoveryInitiated": MessageLookupByLibrary.simpleMessage("Recuperare inițiată"), - "recoveryInitiatedDesc": m57, + "recoveryInitiatedDesc": m62, "recoveryKey": MessageLookupByLibrary.simpleMessage("Cheie de recuperare"), "recoveryKeyCopiedToClipboard": MessageLookupByLibrary.simpleMessage( @@ -1487,12 +1478,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": m58, + "recoveryReady": m63, "recoverySuccessful": MessageLookupByLibrary.simpleMessage("Recuperare reușită!"), "recoveryWarning": MessageLookupByLibrary.simpleMessage( "Un contact de încredere încearcă să vă acceseze contul"), - "recoveryWarningBody": m59, + "recoveryWarningBody": m64, "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": @@ -1508,7 +1499,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": m60, + "referralStep3": m65, "referrals": MessageLookupByLibrary.simpleMessage("Recomandări"), "referralsAreCurrentlyPaused": MessageLookupByLibrary.simpleMessage( "Recomandările sunt momentan întrerupte"), @@ -1540,7 +1531,7 @@ class MessageLookup extends MessageLookupByLibrary { "removeLink": MessageLookupByLibrary.simpleMessage("Eliminați linkul"), "removeParticipant": MessageLookupByLibrary.simpleMessage("Eliminați participantul"), - "removeParticipantBody": m61, + "removeParticipantBody": m66, "removePersonLabel": MessageLookupByLibrary.simpleMessage( "Eliminați eticheta persoanei"), "removePublicLink": @@ -1561,7 +1552,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Redenumiți fișierul"), "renewSubscription": MessageLookupByLibrary.simpleMessage("Reînnoire abonament"), - "renewsOn": m62, + "renewsOn": m67, "reportABug": MessageLookupByLibrary.simpleMessage("Raportați o eroare"), "reportBug": MessageLookupByLibrary.simpleMessage("Raportare eroare"), @@ -1642,8 +1633,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": m63, - "searchSectionsLengthMismatch": m64, + "searchResultCount": m68, + "searchSectionsLengthMismatch": m69, "security": MessageLookupByLibrary.simpleMessage("Securitate"), "seePublicAlbumLinksInApp": MessageLookupByLibrary.simpleMessage( "Vedeți linkurile albumelor publice în aplicație"), @@ -1679,7 +1670,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage( "Articolele selectate vor fi șterse din toate albumele și mutate în coșul de gunoi."), "selectedPhotos": m6, - "selectedPhotosWithYours": m65, + "selectedPhotosWithYours": m70, "send": MessageLookupByLibrary.simpleMessage("Trimitere"), "sendEmail": MessageLookupByLibrary.simpleMessage("Trimiteți e-mail"), "sendInvite": @@ -1712,16 +1703,16 @@ class MessageLookup extends MessageLookupByLibrary { "shareAnAlbumNow": MessageLookupByLibrary.simpleMessage("Distribuiți un album acum"), "shareLink": MessageLookupByLibrary.simpleMessage("Distribuiți linkul"), - "shareMyVerificationID": m66, + "shareMyVerificationID": m71, "shareOnlyWithThePeopleYouWant": MessageLookupByLibrary.simpleMessage( "Distribuiți numai cu persoanele pe care le doriți"), "shareTextConfirmOthersVerificationID": m7, "shareTextRecommendUsingEnte": MessageLookupByLibrary.simpleMessage( "Descarcă Ente pentru a putea distribui cu ușurință fotografii și videoclipuri în calitate originală\n\nhttps://ente.io"), - "shareTextReferralCode": m67, + "shareTextReferralCode": m72, "shareWithNonenteUsers": MessageLookupByLibrary.simpleMessage( "Distribuiți cu utilizatori din afara Ente"), - "shareWithPeopleSectionTitle": m68, + "shareWithPeopleSectionTitle": m73, "shareYourFirstAlbum": MessageLookupByLibrary.simpleMessage("Distribuiți primul album"), "sharedAlbumSectionDescription": MessageLookupByLibrary.simpleMessage( @@ -1734,7 +1725,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": m69, + "sharedWith": m74, "sharedWithMe": MessageLookupByLibrary.simpleMessage("Distribuit mie"), "sharedWithYou": MessageLookupByLibrary.simpleMessage("Distribuite cu dvs."), @@ -1750,11 +1741,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": m70, + "singleFileDeleteFromDevice": m75, "singleFileDeleteHighlight": MessageLookupByLibrary.simpleMessage( "Acesta va fi șters din toate albumele."), - "singleFileInBothLocalAndRemote": m71, - "singleFileInRemoteOnly": m72, + "singleFileInBothLocalAndRemote": m76, + "singleFileInRemoteOnly": m77, "skip": MessageLookupByLibrary.simpleMessage("Omiteți"), "social": MessageLookupByLibrary.simpleMessage("Rețele socializare"), "someItemsAreInBothEnteAndYourDevice": @@ -1805,10 +1796,10 @@ class MessageLookup extends MessageLookupByLibrary { "storageInGB": m1, "storageLimitExceeded": MessageLookupByLibrary.simpleMessage("Limita de spațiu depășită"), - "storageUsageInfo": m73, + "storageUsageInfo": m78, "strongStrength": MessageLookupByLibrary.simpleMessage("Puternică"), - "subAlreadyLinkedErrMessage": m74, - "subWillBeCancelledOn": m75, + "subAlreadyLinkedErrMessage": m79, + "subWillBeCancelledOn": m80, "subscribe": MessageLookupByLibrary.simpleMessage("Abonare"), "subscribeToEnableSharing": MessageLookupByLibrary.simpleMessage( "Aveți nevoie de un abonament plătit activ pentru a activa distribuirea."), @@ -1825,7 +1816,7 @@ class MessageLookup extends MessageLookupByLibrary { "suggestFeatures": MessageLookupByLibrary.simpleMessage("Sugerați funcționalități"), "support": MessageLookupByLibrary.simpleMessage("Asistență"), - "syncProgress": m76, + "syncProgress": m81, "syncStopped": MessageLookupByLibrary.simpleMessage("Sincronizare oprită"), "syncing": MessageLookupByLibrary.simpleMessage("Sincronizare..."), @@ -1838,7 +1829,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Atingeți pentru a debloca"), "tapToUpload": MessageLookupByLibrary.simpleMessage("Atingeți pentru a încărca"), - "tapToUploadIsIgnoredDue": m77, + "tapToUploadIsIgnoredDue": m82, "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"), @@ -1877,7 +1868,7 @@ class MessageLookup extends MessageLookupByLibrary { "Această adresă de e-mail este deja folosită"), "thisImageHasNoExifData": MessageLookupByLibrary.simpleMessage( "Această imagine nu are date exif"), - "thisIsPersonVerificationId": m78, + "thisIsPersonVerificationId": m83, "thisIsYourVerificationId": MessageLookupByLibrary.simpleMessage( "Acesta este ID-ul dvs. de verificare"), "thisWillLogYouOutOfTheFollowingDevice": @@ -1902,11 +1893,11 @@ class MessageLookup extends MessageLookupByLibrary { "total": MessageLookupByLibrary.simpleMessage("total"), "totalSize": MessageLookupByLibrary.simpleMessage("Dimensiune totală"), "trash": MessageLookupByLibrary.simpleMessage("Coș de gunoi"), - "trashDaysLeft": m79, + "trashDaysLeft": m84, "trim": MessageLookupByLibrary.simpleMessage("Decupare"), "trustedContacts": MessageLookupByLibrary.simpleMessage("Contacte de încredere"), - "trustedInviteBody": m80, + "trustedInviteBody": m85, "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."), @@ -1925,7 +1916,7 @@ class MessageLookup extends MessageLookupByLibrary { "Autentificarea cu doi factori a fost resetată cu succes"), "twofactorSetup": MessageLookupByLibrary.simpleMessage("Configurare doi factori"), - "typeOfGallerGallerytypeIsNotSupportedForRename": m81, + "typeOfGallerGallerytypeIsNotSupportedForRename": m86, "unarchive": MessageLookupByLibrary.simpleMessage("Dezarhivare"), "unarchiveAlbum": MessageLookupByLibrary.simpleMessage("Dezarhivare album"), @@ -1951,10 +1942,10 @@ class MessageLookup extends MessageLookupByLibrary { "updatingFolderSelection": MessageLookupByLibrary.simpleMessage( "Se actualizează selecția dosarelor..."), "upgrade": MessageLookupByLibrary.simpleMessage("Îmbunătățire"), - "uploadIsIgnoredDueToIgnorereason": m82, + "uploadIsIgnoredDueToIgnorereason": m87, "uploadingFilesToAlbum": MessageLookupByLibrary.simpleMessage( "Se încarcă fișiere în album..."), - "uploadingMultipleMemories": m83, + "uploadingMultipleMemories": m88, "uploadingSingleMemory": MessageLookupByLibrary.simpleMessage("Se salvează o amintire..."), "upto50OffUntil4thDec": MessageLookupByLibrary.simpleMessage( @@ -1972,7 +1963,7 @@ class MessageLookup extends MessageLookupByLibrary { "useSelectedPhoto": MessageLookupByLibrary.simpleMessage( "Folosiți fotografia selectată"), "usedSpace": MessageLookupByLibrary.simpleMessage("Spațiu utilizat"), - "validTill": m84, + "validTill": m89, "verificationFailedPleaseTryAgain": MessageLookupByLibrary.simpleMessage( "Verificare eșuată, încercați din nou"), @@ -1981,7 +1972,7 @@ class MessageLookup extends MessageLookupByLibrary { "verify": MessageLookupByLibrary.simpleMessage("Verificare"), "verifyEmail": MessageLookupByLibrary.simpleMessage("Verificare e-mail"), - "verifyEmailID": m85, + "verifyEmailID": m90, "verifyIDLabel": MessageLookupByLibrary.simpleMessage("Verificare"), "verifyPasskey": MessageLookupByLibrary.simpleMessage("Verificați cheia de acces"), @@ -2007,7 +1998,6 @@ class MessageLookup extends MessageLookupByLibrary { "viewRecoveryKey": MessageLookupByLibrary.simpleMessage( "Vizualizați cheia de recuperare"), "viewer": MessageLookupByLibrary.simpleMessage("Observator"), - "viewersSuccessfullyAdded": m86, "visitWebToManage": MessageLookupByLibrary.simpleMessage( "Vă rugăm să vizitați web.ente.io pentru a vă gestiona abonamentul"), "waitingForVerification": @@ -2029,7 +2019,7 @@ class MessageLookup extends MessageLookupByLibrary { "Contactul de încredere vă poate ajuta la recuperarea datelor."), "yearShort": MessageLookupByLibrary.simpleMessage("an"), "yearly": MessageLookupByLibrary.simpleMessage("Anual"), - "yearsAgo": m87, + "yearsAgo": m92, "yes": MessageLookupByLibrary.simpleMessage("Da"), "yesCancel": MessageLookupByLibrary.simpleMessage("Da, anulează"), "yesConvertToViewer": @@ -2061,7 +2051,7 @@ class MessageLookup extends MessageLookupByLibrary { "Nu poți distribui cu tine însuți"), "youDontHaveAnyArchivedItems": MessageLookupByLibrary.simpleMessage("Nu aveți articole arhivate."), - "youHaveSuccessfullyFreedUp": m88, + "youHaveSuccessfullyFreedUp": m93, "yourAccountHasBeenDeleted": MessageLookupByLibrary.simpleMessage("Contul dvs. a fost șters"), "yourMap": MessageLookupByLibrary.simpleMessage("Harta dvs."), @@ -2082,9 +2072,6 @@ class MessageLookup extends MessageLookupByLibrary { "Abonamentul dvs. a fost actualizat cu succes"), "yourVerificationCodeHasExpired": MessageLookupByLibrary.simpleMessage( "Codul dvs. de verificare a expirat"), - "youveNoDuplicateFilesThatCanBeCleared": - MessageLookupByLibrary.simpleMessage( - "Nu aveți dubluri care pot fi șterse"), "youveNoFilesInThisAlbumThatCanBeDeleted": MessageLookupByLibrary.simpleMessage( "Nu aveți fișiere în acest album care pot fi șterse"), diff --git a/mobile/lib/generated/intl/messages_ru.dart b/mobile/lib/generated/intl/messages_ru.dart index ed0f726786..09ac42bdcc 100644 --- a/mobile/lib/generated/intl/messages_ru.dart +++ b/mobile/lib/generated/intl/messages_ru.dart @@ -20,217 +20,217 @@ typedef String MessageIfAbsent(String messageStr, List args); class MessageLookup extends MessageLookupByLibrary { String get localeName => 'ru'; - static String m9(count) => + static String m10(count) => "${Intl.plural(count, one: 'Добавьте соавтора', few: 'Добавьте соавторов', many: 'Добавьте соавторов', other: 'Добавьте соавторов')}"; - static String m10(count) => + static String m11(count) => "${Intl.plural(count, one: 'Добавить элемент', other: 'Добавить элементы')}"; - static String m11(storageAmount, endDate) => + static String m12(storageAmount, endDate) => "Ваше дополнение ${storageAmount} действительно по ${endDate}"; - static String m12(count) => + static String m13(count) => "${Intl.plural(count, one: 'Добавьте зрителя', few: 'Добавьте зрителей', many: 'Добавьте зрителей', other: 'Добавьте зрителей')}"; - static String m13(emailOrName) => "Добавлено ${emailOrName}"; + static String m14(emailOrName) => "Добавлено ${emailOrName}"; - static String m14(albumName) => "Успешно добавлено в ${albumName}"; + static String m15(albumName) => "Успешно добавлено в ${albumName}"; - static String m15(count) => + static String m16(count) => "${Intl.plural(count, zero: 'Нет Участников', one: '1 Участник', other: '${count} Участника')}"; - static String m16(versionValue) => "Версия: ${versionValue}"; + static String m17(versionValue) => "Версия: ${versionValue}"; - static String m17(freeAmount, storageUnit) => + static String m18(freeAmount, storageUnit) => "${freeAmount} ${storageUnit} свободно"; - static String m18(paymentProvider) => + static String m19(paymentProvider) => "Пожалуйста, сначала отмените вашу существующую подписку от ${paymentProvider}"; static String m3(user) => "${user} больше не сможет добавлять фотографии в этот альбом\n\nОни все еще смогут удалять существующие фотографии, добавленные ими"; - static String m19(isFamilyMember, storageAmountInGb) => + static String m20(isFamilyMember, storageAmountInGb) => "${Intl.select(isFamilyMember, { 'true': 'Ваша семья получила ${storageAmountInGb} ГБ', 'false': 'Вы уже получили ${storageAmountInGb} ГБ', 'other': 'Вы уже получили ${storageAmountInGb} ГБ!', })}"; - static String m20(albumName) => "Совместная ссылка создана для ${albumName}"; + static String m21(albumName) => "Совместная ссылка создана для ${albumName}"; - static String m21(count) => + static String m22(count) => "${Intl.plural(count, zero: 'Добавлено 0 соавторов', one: 'Добавлен 1 соавтор', other: 'Добавлено ${count} соавторов')}"; - static String m23(familyAdminEmail) => + static String m24(familyAdminEmail) => "Пожалуйста, свяжитесь с ${familyAdminEmail} для управления подпиской"; - static String m24(provider) => + static String m25(provider) => "Пожалуйста, свяжитесь с нами по адресу support@ente.io для управления подпиской ${provider}."; - static String m25(endpoint) => "Подключено к ${endpoint}"; + static String m26(endpoint) => "Подключено к ${endpoint}"; - static String m26(count) => + static String m27(count) => "${Intl.plural(count, one: 'Удалена ${count} штука', other: 'Удалено ${count} штук')}"; - static String m27(currentlyDeleting, totalCount) => + static String m28(currentlyDeleting, totalCount) => "Удаление ${currentlyDeleting} / ${totalCount}"; - static String m28(albumName) => + static String m29(albumName) => "Это удалит публичную ссылку для доступа к \"${albumName}\"."; - static String m29(supportEmail) => + static String m30(supportEmail) => "Пожалуйста, отправьте электронное письмо на адрес ${supportEmail} с вашего зарегистрированного адреса электронной почты"; - static String m30(count, storageSaved) => + static String m31(count, storageSaved) => "Вы привели себя в порядок ${Intl.plural(count, one: '${count} duplicate file', other: '${count} duplicate files')}, экономия (${storageSaved}!)\n"; - static String m31(count, formattedSize) => + static String m32(count, formattedSize) => "${count} файлов, ${formattedSize}"; - static String m32(newEmail) => + static String m33(newEmail) => "Адрес электронной почты изменен на ${newEmail}"; - static String m33(email) => + static String m35(email) => "У ${email} нет учетной записи Ente.\n\nОтправьте им приглашение для обмена фотографиями."; - static String m34(text) => "Дополнительные фотографии найдены для ${text}"; + static String m36(text) => "Дополнительные фотографии найдены для ${text}"; - static String m35(count, formattedNumber) => + static String m37(count, formattedNumber) => "${Intl.plural(count, one: 'для 1 файла было создан бекап', other: 'для ${formattedNumber} файлов были созданы бекапы')}"; - static String m36(count, formattedNumber) => + static String m38(count, formattedNumber) => "${Intl.plural(count, one: 'для 1 файла было создан бекап', other: 'для ${formattedNumber} файлов были созданы бекапы')}"; static String m4(storageAmountInGB) => "${storageAmountInGB} Гигабайт каждый раз когда кто-то подписывается на платный план и применяет ваш код"; - static String m37(endDate) => + static String m39(endDate) => "Бесплатная пробная версия действительна до ${endDate}"; - static String m38(count) => + static String m40(count) => "Вы все еще можете получить доступ к ${Intl.plural(count, one: 'ниму', other: 'ним')} на Ente, пока у вас есть активная подписка"; - static String m39(sizeInMBorGB) => "Освободите ${sizeInMBorGB}"; + static String m41(sizeInMBorGB) => "Освободите ${sizeInMBorGB}"; - static String m40(count, formattedSize) => + static String m42(count, formattedSize) => "${Intl.plural(count, one: 'Это можно удалить с устройства, чтобы освободить ${formattedSize}', other: 'Их можно удалить с устройства, чтобы освободить ${formattedSize}')}"; - static String m41(currentlyProcessing, totalCount) => + static String m43(currentlyProcessing, totalCount) => "Обработка ${currentlyProcessing} / ${totalCount}"; - static String m42(count) => + static String m44(count) => "${Intl.plural(count, one: '${count} штука', other: '${count} штук')}"; - static String m44(expiryTime) => "Ссылка истечёт через ${expiryTime}"; + static String m46(expiryTime) => "Ссылка истечёт через ${expiryTime}"; static String m5(count, formattedCount) => "${Intl.plural(count, zero: 'нет воспоминаний', one: '${formattedCount} воспоминание', other: '${formattedCount} воспоминаний')}"; - static String m45(count) => + static String m49(count) => "${Intl.plural(count, one: 'Переместить элемент', other: 'Переместить элементы')}"; - static String m46(albumName) => "Успешно перемещено в ${albumName}"; + static String m50(albumName) => "Успешно перемещено в ${albumName}"; - static String m47(personName) => "Нет предложений для ${personName}"; + static String m51(personName) => "Нет предложений для ${personName}"; - static String m48(name) => "Не ${name}?"; + static String m52(name) => "Не ${name}?"; static String m0(passwordStrengthValue) => "Мощность пароля: ${passwordStrengthValue}"; - static String m50(providerName) => + static String m54(providerName) => "Если с вас сняли оплату, обратитесь в службу поддержки ${providerName}"; - static String m51(count) => + static String m55(count) => "${Intl.plural(count, zero: '0 photo', one: '1 photo', other: '${count} photos')}"; - static String m52(endDate) => + static String m56(endDate) => "Бесплатный пробный период до ${endDate}.\nПосле, вы сможете выбрать платный план."; - static String m53(toEmail) => "Пожалуйста, напишите нам на ${toEmail}"; + static String m57(toEmail) => "Пожалуйста, напишите нам на ${toEmail}"; - static String m54(toEmail) => "Пожалуйста, отправьте логи на \n${toEmail}"; + static String m58(toEmail) => "Пожалуйста, отправьте логи на \n${toEmail}"; - static String m55(folderName) => "Обработка ${folderName}..."; + static String m59(folderName) => "Обработка ${folderName}..."; - static String m56(storeName) => "Оцените нас в ${storeName}"; + static String m60(storeName) => "Оцените нас в ${storeName}"; - static String m60(storageInGB) => + static String m65(storageInGB) => "3. Вы оба получаете ${storageInGB} Гигабайт* бесплатно"; - static String m61(userEmail) => + static String m66(userEmail) => "${userEmail} будет удален из этого общего альбома\n\nВсе добавленные им фотографии также будут удалены из альбома"; - static String m62(endDate) => "Обновление подписки на ${endDate}"; + static String m67(endDate) => "Обновление подписки на ${endDate}"; - static String m63(count) => + static String m68(count) => "${Intl.plural(count, one: '${count} результат найден', other: '${count} результатов найдено')}"; static String m6(count) => "${count} выбрано"; - static String m65(count, yourCount) => "${count} выбрано (${yourCount} ваши)"; + static String m70(count, yourCount) => "${count} выбрано (${yourCount} ваши)"; - static String m66(verificationID) => + static String m71(verificationID) => "Вот мой проверочный ID: ${verificationID} для ente.io."; static String m7(verificationID) => "Эй, вы можете подтвердить, что это ваш идентификатор подтверждения ente.io: ${verificationID}"; - static String m67(referralCode, referralStorageInGB) => + static String m72(referralCode, referralStorageInGB) => "Реферальный код Ente: ${referralCode} \n\nПримените его в разделе «Настройки» → «Основные» → «Рефералы», чтобы получить ${referralStorageInGB} Гигабайт бесплатно после того как вы подпишетесь на платный план"; - static String m68(numberOfPeople) => + static String m73(numberOfPeople) => "${Intl.plural(numberOfPeople, zero: 'Поделится с конкретными людьми', one: 'Поделено с 1 человеком', other: 'Поделено с ${numberOfPeople} людьми')}"; - static String m69(emailIDs) => "Поделиться с ${emailIDs}"; + static String m74(emailIDs) => "Поделиться с ${emailIDs}"; - static String m70(fileType) => + static String m75(fileType) => "Это ${fileType} будет удалено с вашего устройства."; - static String m71(fileType) => + static String m76(fileType) => "Этот ${fileType} есть и в Ente, и на вашем устройстве."; - static String m72(fileType) => "Этот ${fileType} будет удалён из Ente."; + static String m77(fileType) => "Этот ${fileType} будет удалён из Ente."; static String m1(storageAmountInGB) => "${storageAmountInGB} Гигабайт"; - static String m73( + static String m78( usedAmount, usedStorageUnit, totalAmount, totalStorageUnit) => "${usedAmount} ${usedStorageUnit} из ${totalAmount} ${totalStorageUnit} использовано"; - static String m74(id) => + static String m79(id) => "Ваш ${id} уже связан с другой учетной записью Ente.\nЕсли вы хотите использовать ${id} с этой учетной записью, пожалуйста, свяжитесь с нашей службой поддержки"; - static String m75(endDate) => "Ваша подписка будет отменена ${endDate}"; + static String m80(endDate) => "Ваша подписка будет отменена ${endDate}"; - static String m76(completed, total) => "${completed}/${total} сохранено"; + static String m81(completed, total) => "${completed}/${total} сохранено"; static String m8(storageAmountInGB) => "Они тоже получат ${storageAmountInGB} Гигабайт"; - static String m78(email) => + static String m83(email) => "Этот идентификатор подтверждения пользователя ${email}"; - static String m81(galleryType) => + static String m86(galleryType) => "Тип галереи ${galleryType} не поддерживается для переименования"; - static String m82(ignoreReason) => + static String m87(ignoreReason) => "Загрузка игнорируется из-за ${ignoreReason}"; - static String m84(endDate) => "Действителен по ${endDate}"; + static String m89(endDate) => "Действителен по ${endDate}"; - static String m85(email) => "Подтвердить ${email}"; + static String m90(email) => "Подтвердить ${email}"; - static String m86(count) => + static String m91(count) => "${Intl.plural(count, zero: 'Добавлено 0 зрителей', one: 'Добавлен 1 зритель', other: 'Добавлено ${count} зрителей')}"; static String m2(email) => "Мы отправили письмо на ${email}"; - static String m87(count) => + static String m92(count) => "${Intl.plural(count, one: '${count} год назад', other: '${count} лет назад')}"; - static String m88(storageSaved) => "Вы успешно освободили ${storageSaved}!"; + static String m93(storageSaved) => "Вы успешно освободили ${storageSaved}!"; final messages = _notInlinedMessages(_notInlinedMessages); static Map _notInlinedMessages(_) => { @@ -254,11 +254,11 @@ class MessageLookup extends MessageLookupByLibrary { "Добавить новый адрес эл. почты"), "addCollaborator": MessageLookupByLibrary.simpleMessage("Добавить соавтора"), - "addCollaborators": m9, + "addCollaborators": m10, "addFiles": MessageLookupByLibrary.simpleMessage("Добавить файлы"), "addFromDevice": MessageLookupByLibrary.simpleMessage("Добавить с устройства"), - "addItem": m10, + "addItem": m11, "addLocation": MessageLookupByLibrary.simpleMessage("Добавить место"), "addLocationButton": MessageLookupByLibrary.simpleMessage("Добавить"), "addMore": MessageLookupByLibrary.simpleMessage("Добавить еще"), @@ -270,7 +270,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Добавить новую персону"), "addOnPageSubtitle": MessageLookupByLibrary.simpleMessage("Подробнее о расширениях"), - "addOnValidTill": m11, + "addOnValidTill": m12, "addOns": MessageLookupByLibrary.simpleMessage("Расширения"), "addPhotos": MessageLookupByLibrary.simpleMessage("Добавить фотографии"), @@ -284,12 +284,12 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Добавить доверенный контакт"), "addViewer": MessageLookupByLibrary.simpleMessage("Добавить наблюдателя"), - "addViewers": m12, + "addViewers": m13, "addYourPhotosNow": MessageLookupByLibrary.simpleMessage("Добавьте ваши фотографии"), "addedAs": MessageLookupByLibrary.simpleMessage("Добавлено как"), - "addedBy": m13, - "addedSuccessfullyTo": m14, + "addedBy": m14, + "addedSuccessfullyTo": m15, "addingToFavorites": MessageLookupByLibrary.simpleMessage("Добавление в избранное..."), "advanced": MessageLookupByLibrary.simpleMessage("Дополнительно"), @@ -301,7 +301,7 @@ class MessageLookup extends MessageLookupByLibrary { "after1Week": MessageLookupByLibrary.simpleMessage("Через неделю"), "after1Year": MessageLookupByLibrary.simpleMessage("Через 1 год"), "albumOwner": MessageLookupByLibrary.simpleMessage("Владелец"), - "albumParticipantsCount": m15, + "albumParticipantsCount": m16, "albumTitle": MessageLookupByLibrary.simpleMessage("Название альбома"), "albumUpdated": MessageLookupByLibrary.simpleMessage("Альбом обновлен"), "albums": MessageLookupByLibrary.simpleMessage("Альбомы"), @@ -346,7 +346,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Блокировка приложения"), "appLockDescriptions": MessageLookupByLibrary.simpleMessage( "Выберите между экраном блокировки вашего устройства и пользовательским экраном блокировки с PIN-кодом или паролем."), - "appVersion": m16, + "appVersion": m17, "appleId": MessageLookupByLibrary.simpleMessage("Apple ID"), "apply": MessageLookupByLibrary.simpleMessage("Применить"), "applyCodeTitle": MessageLookupByLibrary.simpleMessage("Применить код"), @@ -425,7 +425,7 @@ class MessageLookup extends MessageLookupByLibrary { "autoPairDesc": MessageLookupByLibrary.simpleMessage( "Автоматическое подключение работает только с устройствами, поддерживающими Chromecast."), "available": MessageLookupByLibrary.simpleMessage("Доступно"), - "availableStorageSpace": m17, + "availableStorageSpace": m18, "backedUpFolders": MessageLookupByLibrary.simpleMessage("Резервное копирование папок"), "backup": MessageLookupByLibrary.simpleMessage("Резервное копирование"), @@ -456,7 +456,7 @@ class MessageLookup extends MessageLookupByLibrary { "canOnlyRemoveFilesOwnedByYou": MessageLookupByLibrary.simpleMessage( "Можно удалять только файлы, принадлежащие вам"), "cancel": MessageLookupByLibrary.simpleMessage("Отменить"), - "cancelOtherSubscription": m18, + "cancelOtherSubscription": m19, "cancelSubscription": MessageLookupByLibrary.simpleMessage("Отменить подписку"), "cannotAddMorePhotosAfterBecomingViewer": m3, @@ -510,7 +510,7 @@ class MessageLookup extends MessageLookupByLibrary { "Получить бесплатное хранилище"), "claimMore": MessageLookupByLibrary.simpleMessage("Получите больше!"), "claimed": MessageLookupByLibrary.simpleMessage("Получено"), - "claimedStorageSoFar": m19, + "claimedStorageSoFar": m20, "cleanUncategorized": MessageLookupByLibrary.simpleMessage("Очистить \"Без Категории\""), "cleanUncategorizedDescription": MessageLookupByLibrary.simpleMessage( @@ -538,12 +538,12 @@ class MessageLookup extends MessageLookupByLibrary { "Создайте ссылку, чтобы позволить людям добавлять и просматривать фотографии в вашем общем альбоме без приложения или учетной записи Ente. Отлично подходит для сбора фотографий событий."), "collaborativeLink": MessageLookupByLibrary.simpleMessage("Совместная ссылка"), - "collaborativeLinkCreatedFor": m20, + "collaborativeLinkCreatedFor": m21, "collaborator": MessageLookupByLibrary.simpleMessage("Соавтор"), "collaboratorsCanAddPhotosAndVideosToTheSharedAlbum": MessageLookupByLibrary.simpleMessage( "Соавторы могут добавлять фотографии и видео в общий альбом."), - "collaboratorsSuccessfullyAdded": m21, + "collaboratorsSuccessfullyAdded": m22, "collageLayout": MessageLookupByLibrary.simpleMessage("Разметка"), "collageSaved": MessageLookupByLibrary.simpleMessage("Коллаж сохранен в галерее"), @@ -573,10 +573,10 @@ class MessageLookup extends MessageLookupByLibrary { "Подтвердите ваш ключ восстановления"), "connectToDevice": MessageLookupByLibrary.simpleMessage("Подключиться к устройству"), - "contactFamilyAdmin": m23, + "contactFamilyAdmin": m24, "contactSupport": MessageLookupByLibrary.simpleMessage("Связаться с поддержкой"), - "contactToManageSubscription": m24, + "contactToManageSubscription": m25, "contacts": MessageLookupByLibrary.simpleMessage("Контакты"), "contents": MessageLookupByLibrary.simpleMessage("Содержимое"), "continueLabel": MessageLookupByLibrary.simpleMessage("Далее"), @@ -623,7 +623,7 @@ class MessageLookup extends MessageLookupByLibrary { "currentlyRunning": MessageLookupByLibrary.simpleMessage("сейчас запущено"), "custom": MessageLookupByLibrary.simpleMessage("Свой"), - "customEndpoint": m25, + "customEndpoint": m26, "darkTheme": MessageLookupByLibrary.simpleMessage("Темная тема"), "dayToday": MessageLookupByLibrary.simpleMessage("Сегодня"), "dayYesterday": MessageLookupByLibrary.simpleMessage("Вчера"), @@ -661,11 +661,11 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Удалить с устройства"), "deleteFromEnte": MessageLookupByLibrary.simpleMessage("Удалить из Ente"), - "deleteItemCount": m26, + "deleteItemCount": m27, "deleteLocation": MessageLookupByLibrary.simpleMessage("Удалить местоположение"), "deletePhotos": MessageLookupByLibrary.simpleMessage("Удалить фото"), - "deleteProgress": m27, + "deleteProgress": m28, "deleteReason1": MessageLookupByLibrary.simpleMessage( "У вас отсутствует важная функция, которая мне нужна"), "deleteReason2": MessageLookupByLibrary.simpleMessage( @@ -704,7 +704,7 @@ class MessageLookup extends MessageLookupByLibrary { "Наблюдатели все еще могут делать скриншоты или копировать ваши фотографии с помощью других инструментов"), "disableDownloadWarningTitle": MessageLookupByLibrary.simpleMessage("Обратите внимание"), - "disableLinkMessage": m28, + "disableLinkMessage": m29, "disableTwofactor": MessageLookupByLibrary.simpleMessage( "Отключить двухфакторную аутентификацию"), "disablingTwofactorAuthentication": @@ -737,9 +737,9 @@ class MessageLookup extends MessageLookupByLibrary { "downloadFailed": MessageLookupByLibrary.simpleMessage("Загрузка не удалась"), "downloading": MessageLookupByLibrary.simpleMessage("Скачивание..."), - "dropSupportEmail": m29, - "duplicateFileCountWithStorageSaved": m30, - "duplicateItemsGroup": m31, + "dropSupportEmail": m30, + "duplicateFileCountWithStorageSaved": m31, + "duplicateItemsGroup": m32, "edit": MessageLookupByLibrary.simpleMessage("Редактировать"), "editLocation": MessageLookupByLibrary.simpleMessage("Изменить местоположение"), @@ -754,8 +754,8 @@ class MessageLookup extends MessageLookupByLibrary { "Редактирования в местоположении будут видны только внутри Ente"), "eligible": MessageLookupByLibrary.simpleMessage("подходящий"), "email": MessageLookupByLibrary.simpleMessage("Электронная почта"), - "emailChangedTo": m32, - "emailNoEnteAccount": m33, + "emailChangedTo": m33, + "emailNoEnteAccount": m35, "emailVerificationToggle": MessageLookupByLibrary.simpleMessage("Вход с кодом на почту"), "emailYourLogs": MessageLookupByLibrary.simpleMessage( @@ -835,7 +835,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Экспорт данных"), "extraPhotosFound": MessageLookupByLibrary.simpleMessage( "Найдены дополнительные фотографии"), - "extraPhotosFoundFor": m34, + "extraPhotosFoundFor": m36, "faceNotClusteredYet": MessageLookupByLibrary.simpleMessage( "Лицо еще не кластеризовано, пожалуйста, вернитесь позже"), "faceRecognition": @@ -885,8 +885,8 @@ class MessageLookup extends MessageLookupByLibrary { "fileTypes": MessageLookupByLibrary.simpleMessage("Типы файлов"), "fileTypesAndNames": MessageLookupByLibrary.simpleMessage("Типы файлов и имена"), - "filesBackedUpFromDevice": m35, - "filesBackedUpInAlbum": m36, + "filesBackedUpFromDevice": m37, + "filesBackedUpInAlbum": m38, "filesDeleted": MessageLookupByLibrary.simpleMessage("Файлы удалены"), "filesSavedToGallery": MessageLookupByLibrary.simpleMessage("Файлы сохранены в галерею"), @@ -904,21 +904,21 @@ class MessageLookup extends MessageLookupByLibrary { "Бесплатного хранилища можно использовать"), "freeTrial": MessageLookupByLibrary.simpleMessage("Бесплатный пробный период"), - "freeTrialValidTill": m37, - "freeUpAccessPostDelete": m38, - "freeUpAmount": m39, + "freeTrialValidTill": m39, + "freeUpAccessPostDelete": m40, + "freeUpAmount": m41, "freeUpDeviceSpace": MessageLookupByLibrary.simpleMessage( "Освободите место на устройстве"), "freeUpDeviceSpaceDesc": MessageLookupByLibrary.simpleMessage( "Сохраните место на вашем устройстве, очистив уже сохраненные файлы."), "freeUpSpace": MessageLookupByLibrary.simpleMessage("Освободить место"), - "freeUpSpaceSaving": m40, + "freeUpSpaceSaving": m42, "galleryMemoryLimitInfo": MessageLookupByLibrary.simpleMessage( "До 1000 воспоминаний, отображаемых в галерее"), "general": MessageLookupByLibrary.simpleMessage("Общее"), "generatingEncryptionKeys": MessageLookupByLibrary.simpleMessage( "Генерируем ключи шифрования..."), - "genericProgress": m41, + "genericProgress": m43, "goToSettings": MessageLookupByLibrary.simpleMessage("Перейти в настройки"), "googlePlayId": MessageLookupByLibrary.simpleMessage("Google Play ID"), @@ -1001,7 +1001,7 @@ class MessageLookup extends MessageLookupByLibrary { "itLooksLikeSomethingWentWrongPleaseRetryAfterSome": MessageLookupByLibrary.simpleMessage( "Похоже, что-то пошло не так. Пожалуйста, повторите попытку через некоторое время. Если ошибка повторится, обратитесь в нашу службу поддержки."), - "itemCount": m42, + "itemCount": m44, "itemsShowTheNumberOfDaysRemainingBeforePermanentDeletion": MessageLookupByLibrary.simpleMessage( "Элементы показывают количество дней, оставшихся до окончательного удаления"), @@ -1032,7 +1032,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Лимит устройств"), "linkEnabled": MessageLookupByLibrary.simpleMessage("Разрешён"), "linkExpired": MessageLookupByLibrary.simpleMessage("Истекшая"), - "linkExpiresOn": m44, + "linkExpiresOn": m46, "linkExpiry": MessageLookupByLibrary.simpleMessage("Срок действия ссылки истек"), "linkHasExpired": @@ -1154,12 +1154,12 @@ class MessageLookup extends MessageLookupByLibrary { "mostRecent": MessageLookupByLibrary.simpleMessage("Недавние"), "mostRelevant": MessageLookupByLibrary.simpleMessage("Сначала актуальные"), - "moveItem": m45, + "moveItem": m49, "moveToAlbum": MessageLookupByLibrary.simpleMessage("Переместить в альбом"), "moveToHiddenAlbum": MessageLookupByLibrary.simpleMessage( "Переместить в скрытый альбом"), - "movedSuccessfullyTo": m46, + "movedSuccessfullyTo": m50, "movedToTrash": MessageLookupByLibrary.simpleMessage("Перемещено в корзину"), "movingFilesToAlbum": MessageLookupByLibrary.simpleMessage( @@ -1211,10 +1211,10 @@ class MessageLookup extends MessageLookupByLibrary { "noResults": MessageLookupByLibrary.simpleMessage("Ничего не найденo"), "noResultsFound": MessageLookupByLibrary.simpleMessage("Ничего не найдено"), - "noSuggestionsForPerson": m47, + "noSuggestionsForPerson": m51, "noSystemLockFound": MessageLookupByLibrary.simpleMessage( "Системная блокировка не найдена"), - "notPersonLabel": m48, + "notPersonLabel": m52, "nothingSharedWithYouYet": MessageLookupByLibrary.simpleMessage( "Пока никто не поделился с вами"), "nothingToSeeHere": @@ -1247,8 +1247,6 @@ class MessageLookup extends MessageLookupByLibrary { "Или объединить с существующими"), "orPickAnExistingOne": MessageLookupByLibrary.simpleMessage( "Или выберите уже существующий"), - "orPickFromYourContacts": - MessageLookupByLibrary.simpleMessage("or pick from your contacts"), "pair": MessageLookupByLibrary.simpleMessage("Спарить"), "pairWithPin": MessageLookupByLibrary.simpleMessage("Соединить с PIN"), "pairingComplete": @@ -1274,7 +1272,7 @@ class MessageLookup extends MessageLookupByLibrary { "paymentFailed": MessageLookupByLibrary.simpleMessage("Сбой платежа"), "paymentFailedMessage": MessageLookupByLibrary.simpleMessage( "К сожалению, ваш платеж не был выполнен. Пожалуйста, свяжитесь со службой поддержки, и мы вам поможем!"), - "paymentFailedTalkToProvider": m50, + "paymentFailedTalkToProvider": m54, "pendingItems": MessageLookupByLibrary.simpleMessage("Отложенные элементы"), "pendingSync": @@ -1298,14 +1296,14 @@ class MessageLookup extends MessageLookupByLibrary { "photosAddedByYouWillBeRemovedFromTheAlbum": MessageLookupByLibrary.simpleMessage( "Добавленные вами фотографии будут удалены из альбома"), - "photosCount": m51, + "photosCount": m55, "pickCenterPoint": MessageLookupByLibrary.simpleMessage("Указать центральную точку"), "pinAlbum": MessageLookupByLibrary.simpleMessage("Закрепить альбом"), "pinLock": MessageLookupByLibrary.simpleMessage("Блокировка PIN-кодом"), "playOnTv": MessageLookupByLibrary.simpleMessage("Воспроизвести альбом на ТВ"), - "playStoreFreeTrialValidTill": m52, + "playStoreFreeTrialValidTill": m56, "playstoreSubscription": MessageLookupByLibrary.simpleMessage("Подписка на PlayStore"), "pleaseCheckYourInternetConnectionAndTryAgain": @@ -1317,14 +1315,14 @@ class MessageLookup extends MessageLookupByLibrary { "pleaseContactSupportIfTheProblemPersists": MessageLookupByLibrary.simpleMessage( "Если проблема не устранена, обратитесь в службу поддержки"), - "pleaseEmailUsAt": m53, + "pleaseEmailUsAt": m57, "pleaseGrantPermissions": MessageLookupByLibrary.simpleMessage("Предоставьте разрешение"), "pleaseLoginAgain": MessageLookupByLibrary.simpleMessage("Пожалуйста, войдите снова"), "pleaseSelectQuickLinksToRemove": MessageLookupByLibrary.simpleMessage( "Пожалуйста, выберите быстрые ссылки для удаления"), - "pleaseSendTheLogsTo": m54, + "pleaseSendTheLogsTo": m58, "pleaseTryAgain": MessageLookupByLibrary.simpleMessage( "Пожалуйста, попробуйте ещё раз"), "pleaseVerifyTheCodeYouHaveEntered": @@ -1351,7 +1349,7 @@ class MessageLookup extends MessageLookupByLibrary { "privateBackups": MessageLookupByLibrary.simpleMessage("Приватные резервные копии"), "privateSharing": MessageLookupByLibrary.simpleMessage("Личный доступ"), - "processingImport": m55, + "processingImport": m59, "publicLinkCreated": MessageLookupByLibrary.simpleMessage("Публичная ссылка создана"), "publicLinkEnabled": @@ -1362,7 +1360,7 @@ class MessageLookup extends MessageLookupByLibrary { "rateTheApp": MessageLookupByLibrary.simpleMessage("Оценить приложение"), "rateUs": MessageLookupByLibrary.simpleMessage("Оцените нас"), - "rateUsOnStore": m56, + "rateUsOnStore": m60, "recover": MessageLookupByLibrary.simpleMessage("Восстановить"), "recoverAccount": MessageLookupByLibrary.simpleMessage("Восстановить аккаунт"), @@ -1400,7 +1398,7 @@ class MessageLookup extends MessageLookupByLibrary { "1. Дайте этот код своим друзьям"), "referralStep2": MessageLookupByLibrary.simpleMessage( "2. Они подписываются на платный план"), - "referralStep3": m60, + "referralStep3": m65, "referrals": MessageLookupByLibrary.simpleMessage("Рефералы"), "referralsAreCurrentlyPaused": MessageLookupByLibrary.simpleMessage( "Рефералы в настоящее время приостановлены"), @@ -1431,7 +1429,7 @@ class MessageLookup extends MessageLookupByLibrary { "removeLink": MessageLookupByLibrary.simpleMessage("Удалить ссылку"), "removeParticipant": MessageLookupByLibrary.simpleMessage("Исключить участника"), - "removeParticipantBody": m61, + "removeParticipantBody": m66, "removePersonLabel": MessageLookupByLibrary.simpleMessage("Удалить метку человека"), "removePublicLink": @@ -1453,7 +1451,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Переименовать файл"), "renewSubscription": MessageLookupByLibrary.simpleMessage("Продлить подписку"), - "renewsOn": m62, + "renewsOn": m67, "reportABug": MessageLookupByLibrary.simpleMessage("Сообщить об ошибке"), "reportBug": MessageLookupByLibrary.simpleMessage("Сообщить об ошибке"), @@ -1529,7 +1527,7 @@ class MessageLookup extends MessageLookupByLibrary { "Групповые фотографии, сделанные в некотором радиусе от фотографии"), "searchPeopleEmptySection": MessageLookupByLibrary.simpleMessage( "Пригласите людей, и вы увидите все фотографии, которыми они поделились здесь"), - "searchResultCount": m63, + "searchResultCount": m68, "security": MessageLookupByLibrary.simpleMessage("Безопасность"), "seePublicAlbumLinksInApp": MessageLookupByLibrary.simpleMessage( "Смотреть ссылки публичного альбома в приложении"), @@ -1563,7 +1561,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage( "Выбранные элементы будут удалены из всех альбомов и перемещены в корзину."), "selectedPhotos": m6, - "selectedPhotosWithYours": m65, + "selectedPhotosWithYours": m70, "send": MessageLookupByLibrary.simpleMessage("Отправить"), "sendEmail": MessageLookupByLibrary.simpleMessage( "Отправить электронное письмо"), @@ -1598,16 +1596,16 @@ class MessageLookup extends MessageLookupByLibrary { "shareAnAlbumNow": MessageLookupByLibrary.simpleMessage("Поделиться альбомом сейчас"), "shareLink": MessageLookupByLibrary.simpleMessage("Поделиться ссылкой"), - "shareMyVerificationID": m66, + "shareMyVerificationID": m71, "shareOnlyWithThePeopleYouWant": MessageLookupByLibrary.simpleMessage( "Поделитесь только с теми людьми, с которыми вы хотите"), "shareTextConfirmOthersVerificationID": m7, "shareTextRecommendUsingEnte": MessageLookupByLibrary.simpleMessage( "Скачай Ente, чтобы мы могли легко поделиться фотографиями и видео без сжатия\n\nhttps://ente.io"), - "shareTextReferralCode": m67, + "shareTextReferralCode": m72, "shareWithNonenteUsers": MessageLookupByLibrary.simpleMessage( "Поделится с пользователями без Ente"), - "shareWithPeopleSectionTitle": m68, + "shareWithPeopleSectionTitle": m73, "shareYourFirstAlbum": MessageLookupByLibrary.simpleMessage("Поделиться первым альбомом"), "sharedAlbumSectionDescription": MessageLookupByLibrary.simpleMessage( @@ -1618,7 +1616,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Новые общие фотографии"), "sharedPhotoNotificationsExplanation": MessageLookupByLibrary.simpleMessage( "Получать уведомления, когда кто-то добавляет фото в общий альбом, в котором вы состоите"), - "sharedWith": m69, + "sharedWith": m74, "sharedWithMe": MessageLookupByLibrary.simpleMessage("Поделиться со мной"), "sharedWithYou": @@ -1635,11 +1633,11 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Выйти из других устройств"), "signUpTerms": MessageLookupByLibrary.simpleMessage( "Я согласен с условиями предоставления услуг и политикой конфиденциальности"), - "singleFileDeleteFromDevice": m70, + "singleFileDeleteFromDevice": m75, "singleFileDeleteHighlight": MessageLookupByLibrary.simpleMessage( "Он будет удален из всех альбомов."), - "singleFileInBothLocalAndRemote": m71, - "singleFileInRemoteOnly": m72, + "singleFileInBothLocalAndRemote": m76, + "singleFileInRemoteOnly": m77, "skip": MessageLookupByLibrary.simpleMessage("Пропустить"), "social": MessageLookupByLibrary.simpleMessage("Соцсети"), "someItemsAreInBothEnteAndYourDevice": MessageLookupByLibrary.simpleMessage( @@ -1687,10 +1685,10 @@ class MessageLookup extends MessageLookupByLibrary { "storageInGB": m1, "storageLimitExceeded": MessageLookupByLibrary.simpleMessage("Превышен предел хранения"), - "storageUsageInfo": m73, + "storageUsageInfo": m78, "strongStrength": MessageLookupByLibrary.simpleMessage("Сильный"), - "subAlreadyLinkedErrMessage": m74, - "subWillBeCancelledOn": m75, + "subAlreadyLinkedErrMessage": m79, + "subWillBeCancelledOn": m80, "subscribe": MessageLookupByLibrary.simpleMessage("Подписаться"), "subscription": MessageLookupByLibrary.simpleMessage("Подписка"), "success": MessageLookupByLibrary.simpleMessage("Успешно"), @@ -1705,7 +1703,7 @@ class MessageLookup extends MessageLookupByLibrary { "suggestFeatures": MessageLookupByLibrary.simpleMessage("Предложить идею"), "support": MessageLookupByLibrary.simpleMessage("Поддержка"), - "syncProgress": m76, + "syncProgress": m81, "syncStopped": MessageLookupByLibrary.simpleMessage("Синхронизация остановлена"), "syncing": MessageLookupByLibrary.simpleMessage("Синхронизация..."), @@ -1755,7 +1753,7 @@ class MessageLookup extends MessageLookupByLibrary { "Этот адрес электронной почты уже используется"), "thisImageHasNoExifData": MessageLookupByLibrary.simpleMessage( "Это изображение не имеет exif данных"), - "thisIsPersonVerificationId": m78, + "thisIsPersonVerificationId": m83, "thisIsYourVerificationId": MessageLookupByLibrary.simpleMessage( "Это ваш идентификатор подтверждения"), "thisWillLogYouOutOfTheFollowingDevice": @@ -1799,7 +1797,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage( "Двухфакторная аутентификация успешно сброшена"), "twofactorSetup": MessageLookupByLibrary.simpleMessage("Вход с 2FA"), - "typeOfGallerGallerytypeIsNotSupportedForRename": m81, + "typeOfGallerGallerytypeIsNotSupportedForRename": m86, "unarchive": MessageLookupByLibrary.simpleMessage("Разархивировать"), "unarchiveAlbum": MessageLookupByLibrary.simpleMessage("Разархивировать альбом"), @@ -1823,7 +1821,7 @@ class MessageLookup extends MessageLookupByLibrary { "updatingFolderSelection": MessageLookupByLibrary.simpleMessage("Обновление выбора папки..."), "upgrade": MessageLookupByLibrary.simpleMessage("Обновить"), - "uploadIsIgnoredDueToIgnorereason": m82, + "uploadIsIgnoredDueToIgnorereason": m87, "uploadingFilesToAlbum": MessageLookupByLibrary.simpleMessage("Загрузка файлов в альбом..."), "upto50OffUntil4thDec": MessageLookupByLibrary.simpleMessage( @@ -1840,7 +1838,7 @@ class MessageLookup extends MessageLookupByLibrary { "useSelectedPhoto": MessageLookupByLibrary.simpleMessage("Использовать выбранное фото"), "usedSpace": MessageLookupByLibrary.simpleMessage("Использовано места"), - "validTill": m84, + "validTill": m89, "verificationFailedPleaseTryAgain": MessageLookupByLibrary.simpleMessage( "Проверка не удалась, попробуйте еще раз"), @@ -1849,7 +1847,7 @@ class MessageLookup extends MessageLookupByLibrary { "verify": MessageLookupByLibrary.simpleMessage("Подтвердить"), "verifyEmail": MessageLookupByLibrary.simpleMessage( "Подтвердить электронную почту"), - "verifyEmailID": m85, + "verifyEmailID": m90, "verifyIDLabel": MessageLookupByLibrary.simpleMessage("Подтверждение"), "verifyPasskey": MessageLookupByLibrary.simpleMessage("Подтвердить ключ"), @@ -1873,7 +1871,7 @@ class MessageLookup extends MessageLookupByLibrary { "viewRecoveryKey": MessageLookupByLibrary.simpleMessage( "Просмотреть ключ восстановления"), "viewer": MessageLookupByLibrary.simpleMessage("Наблюдатель"), - "viewersSuccessfullyAdded": m86, + "viewersSuccessfullyAdded": m91, "visitWebToManage": MessageLookupByLibrary.simpleMessage( "Пожалуйста, посетите web.ente.io для управления вашей подпиской"), "waitingForVerification": @@ -1891,7 +1889,7 @@ class MessageLookup extends MessageLookupByLibrary { "whatsNew": MessageLookupByLibrary.simpleMessage("Что нового"), "yearShort": MessageLookupByLibrary.simpleMessage("г."), "yearly": MessageLookupByLibrary.simpleMessage("Ежегодно"), - "yearsAgo": m87, + "yearsAgo": m92, "yes": MessageLookupByLibrary.simpleMessage("Да"), "yesCancel": MessageLookupByLibrary.simpleMessage("Да, отменить"), "yesConvertToViewer": @@ -1923,7 +1921,7 @@ class MessageLookup extends MessageLookupByLibrary { "Вы не можете поделиться с самим собой"), "youDontHaveAnyArchivedItems": MessageLookupByLibrary.simpleMessage( "У вас нет архивных элементов."), - "youHaveSuccessfullyFreedUp": m88, + "youHaveSuccessfullyFreedUp": m93, "yourAccountHasBeenDeleted": MessageLookupByLibrary.simpleMessage("Ваш аккаунт был удален"), "yourMap": MessageLookupByLibrary.simpleMessage("Ваша карта"), @@ -1944,9 +1942,6 @@ class MessageLookup extends MessageLookupByLibrary { "Ваша подписка успешно обновлена"), "yourVerificationCodeHasExpired": MessageLookupByLibrary.simpleMessage( "Срок действия вашего проверочного кода истек"), - "youveNoDuplicateFilesThatCanBeCleared": - MessageLookupByLibrary.simpleMessage( - "У вас нет дубликатов файлов, которые можно очистить"), "youveNoFilesInThisAlbumThatCanBeDeleted": MessageLookupByLibrary.simpleMessage( "В этом альбоме нет файлов, которые можно удалить"), diff --git a/mobile/lib/generated/intl/messages_sl.dart b/mobile/lib/generated/intl/messages_sl.dart index 41b9782537..d41d848b0f 100644 --- a/mobile/lib/generated/intl/messages_sl.dart +++ b/mobile/lib/generated/intl/messages_sl.dart @@ -21,8 +21,5 @@ class MessageLookup extends MessageLookupByLibrary { String get localeName => 'sl'; final messages = _notInlinedMessages(_notInlinedMessages); - static Map _notInlinedMessages(_) => { - "orPickFromYourContacts": - MessageLookupByLibrary.simpleMessage("or pick from your contacts") - }; + static Map _notInlinedMessages(_) => {}; } diff --git a/mobile/lib/generated/intl/messages_sv.dart b/mobile/lib/generated/intl/messages_sv.dart index 61e22c9a5b..25fbbccdd6 100644 --- a/mobile/lib/generated/intl/messages_sv.dart +++ b/mobile/lib/generated/intl/messages_sv.dart @@ -20,71 +20,94 @@ typedef String MessageIfAbsent(String messageStr, List args); class MessageLookup extends MessageLookupByLibrary { String get localeName => 'sv'; - static String m10(count) => + static String m11(count) => "${Intl.plural(count, one: 'Lägg till objekt', other: 'Lägg till objekt')}"; - static String m15(count) => + static String m16(count) => "${Intl.plural(count, zero: 'Inga deltagare', one: '1 deltagare', other: '${count} deltagare')}"; - static String m16(versionValue) => "Version: ${versionValue}"; + static String m17(versionValue) => "Version: ${versionValue}"; - static String m17(freeAmount, storageUnit) => + static String m18(freeAmount, storageUnit) => "${freeAmount} ${storageUnit} gratis"; static String m3(user) => "${user} kommer inte att kunna lägga till fler foton till detta album\n\nDe kommer fortfarande att kunna ta bort befintliga foton som lagts till av dem"; - static String m26(count) => + static String m20(isFamilyMember, storageAmountInGb) => + "${Intl.select(isFamilyMember, { + 'true': 'Din familj har begärt ${storageAmountInGb} GB', + 'false': '${storageAmountInGb}', + 'other': 'Du har begärt ${storageAmountInGb} GB!', + })}"; + + static String m27(count) => "${Intl.plural(count, one: 'Radera ${count} objekt', other: 'Radera ${count} objekt')}"; - static String m29(supportEmail) => + static String m30(supportEmail) => "Vänligen skicka ett e-postmeddelande till ${supportEmail} från din registrerade e-postadress"; - static String m33(email) => + static String m32(count, formattedSize) => + "${count} filer, ${formattedSize} vardera"; + + static String m35(email) => "${email} har inte ett Ente-konto.\n\nSkicka dem en inbjudan för att dela bilder."; - static String m42(count) => + static String m4(storageAmountInGB) => + "${storageAmountInGB} GB varje gång någon registrerar sig för en betalplan och tillämpar din kod"; + + static String m44(count) => "${Intl.plural(count, one: '${count} objekt', other: '${count} objekt')}"; - static String m44(expiryTime) => "Länken upphör att gälla ${expiryTime}"; + static String m46(expiryTime) => "Länken upphör att gälla ${expiryTime}"; - static String m45(count) => + static String m49(count) => "${Intl.plural(count, one: 'Flytta objekt', other: 'Flytta objekt')}"; - static String m48(name) => "Inte ${name}?"; + static String m52(name) => "Inte ${name}?"; - static String m49(familyAdminEmail) => + static String m53(familyAdminEmail) => "Kontakta ${familyAdminEmail} för att ändra din kod."; static String m0(passwordStrengthValue) => "Lösenordsstyrka: ${passwordStrengthValue}"; - static String m56(storeName) => "Betygsätt oss på ${storeName}"; + static String m60(storeName) => "Betygsätt oss på ${storeName}"; - static String m63(count) => + static String m65(storageInGB) => "3. Ni får båda ${storageInGB} GB* gratis"; + + static String m66(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 m68(count) => "${Intl.plural(count, one: '${count} resultat hittades', other: '${count} resultat hittades')}"; - static String m66(verificationID) => + static String m71(verificationID) => "Här är mitt verifierings-ID: ${verificationID} för ente.io."; static String m7(verificationID) => "Hallå, kan du bekräfta att detta är ditt ente.io verifierings-ID: ${verificationID}"; - static String m68(numberOfPeople) => + static String m72(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 m73(numberOfPeople) => "${Intl.plural(numberOfPeople, zero: 'Dela med specifika personer', one: 'Delad med en person', other: 'Delad med ${numberOfPeople} personer')}"; static String m1(storageAmountInGB) => "${storageAmountInGB} GB"; - static String m78(email) => "Detta är ${email}s verifierings-ID"; + static String m8(storageAmountInGB) => "De får också ${storageAmountInGB} GB"; - static String m83(count) => "Bevarar ${count} minnen..."; + static String m83(email) => "Detta är ${email}s verifierings-ID"; - static String m85(email) => "Bekräfta ${email}"; + static String m88(count) => "Bevarar ${count} minnen..."; + + static String m90(email) => "Bekräfta ${email}"; static String m2(email) => "Vi har skickat ett e-postmeddelande till ${email}"; - static String m87(count) => + static String m92(count) => "${Intl.plural(count, one: '${count} år sedan', other: '${count} år sedan')}"; final messages = _notInlinedMessages(_notInlinedMessages); @@ -106,20 +129,22 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Lägg till samarbetspartner"), "addFromDevice": MessageLookupByLibrary.simpleMessage("Lägg till från enhet"), - "addItem": m10, + "addItem": m11, "addLocationButton": MessageLookupByLibrary.simpleMessage("Lägg till"), "addMore": MessageLookupByLibrary.simpleMessage("Lägg till fler"), "addName": MessageLookupByLibrary.simpleMessage("Lägg till namn"), "addPhotos": MessageLookupByLibrary.simpleMessage("Lägg till foton"), "addViewer": MessageLookupByLibrary.simpleMessage("Lägg till bildvy"), "addedAs": MessageLookupByLibrary.simpleMessage("Lades till som"), + "addingToFavorites": MessageLookupByLibrary.simpleMessage( + "Lägger till bland favoriter..."), "after1Day": MessageLookupByLibrary.simpleMessage("Om en dag"), "after1Hour": MessageLookupByLibrary.simpleMessage("Om en timme"), "after1Month": MessageLookupByLibrary.simpleMessage("Om en månad"), "after1Week": MessageLookupByLibrary.simpleMessage("Om en vecka"), "after1Year": MessageLookupByLibrary.simpleMessage("Om ett år"), "albumOwner": MessageLookupByLibrary.simpleMessage("Ägare"), - "albumParticipantsCount": m15, + "albumParticipantsCount": m16, "albumUpdated": MessageLookupByLibrary.simpleMessage("Album uppdaterat"), "albums": MessageLookupByLibrary.simpleMessage("Album"), @@ -130,7 +155,7 @@ class MessageLookup extends MessageLookupByLibrary { "allowDownloads": MessageLookupByLibrary.simpleMessage("Tillåt nedladdningar"), "androidCancelButton": MessageLookupByLibrary.simpleMessage("Avbryt"), - "appVersion": m16, + "appVersion": m17, "apply": MessageLookupByLibrary.simpleMessage("Verkställ"), "applyCodeTitle": MessageLookupByLibrary.simpleMessage("Använd kod"), "areYouSureYouWantToLogout": MessageLookupByLibrary.simpleMessage( @@ -140,8 +165,18 @@ class MessageLookup extends MessageLookupByLibrary { "authenticationFailedPleaseTryAgain": MessageLookupByLibrary.simpleMessage( "Autentisering misslyckades, försök igen"), - "availableStorageSpace": m17, + "availableStorageSpace": m18, + "backupSettings": MessageLookupByLibrary.simpleMessage( + "Säkerhetskopieringsinställningar"), + "backupStatus": + MessageLookupByLibrary.simpleMessage("Säkerhetskopieringsstatus"), "blog": MessageLookupByLibrary.simpleMessage("Blogg"), + "canNotOpenBody": MessageLookupByLibrary.simpleMessage( + "Tyvärr kan detta album inte öppnas i appen."), + "canNotOpenTitle": MessageLookupByLibrary.simpleMessage( + "Kan inte öppna det här albumet"), + "canOnlyRemoveFilesOwnedByYou": MessageLookupByLibrary.simpleMessage( + "Kan endast ta bort filer som ägs av dig"), "cancel": MessageLookupByLibrary.simpleMessage("Avbryt"), "cannotAddMorePhotosAfterBecomingViewer": m3, "change": MessageLookupByLibrary.simpleMessage("Ändra"), @@ -153,14 +188,25 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Ändra lösenord"), "changePermissions": MessageLookupByLibrary.simpleMessage("Ändra behörighet?"), + "changeYourReferralCode": + MessageLookupByLibrary.simpleMessage("Ändra din värvningskod"), "checkInboxAndSpamFolder": MessageLookupByLibrary.simpleMessage( "Kontrollera din inkorg (och skräppost) för att slutföra verifieringen"), + "claimFreeStorage": + MessageLookupByLibrary.simpleMessage("Hämta kostnadsfri lagring"), + "claimMore": MessageLookupByLibrary.simpleMessage("Begär mer!"), "claimed": MessageLookupByLibrary.simpleMessage("Nyttjad"), + "claimedStorageSoFar": m20, + "clearIndexes": MessageLookupByLibrary.simpleMessage("Rensa index"), "close": MessageLookupByLibrary.simpleMessage("Stäng"), "codeAppliedPageTitle": MessageLookupByLibrary.simpleMessage("Kod tillämpad"), + "codeChangeLimitReached": MessageLookupByLibrary.simpleMessage( + "Tyvärr, du har nått gränsen för kodändringar."), "codeCopiedToClipboard": MessageLookupByLibrary.simpleMessage( "Koden har kopierats till urklipp"), + "codeUsedByYou": + MessageLookupByLibrary.simpleMessage("Kod som används av dig"), "collabLinkSectionDescription": MessageLookupByLibrary.simpleMessage( "Skapa en länk så att personer kan lägga till och visa foton i ditt delade album utan att behöva en Ente app eller konto. Perfekt för att samla in bilder från evenemang."), "collaborativeLink": @@ -175,6 +221,8 @@ class MessageLookup extends MessageLookupByLibrary { "confirm": MessageLookupByLibrary.simpleMessage("Bekräfta"), "confirmAccountDeletion": MessageLookupByLibrary.simpleMessage("Bekräfta radering av konto"), + "confirmDeletePrompt": MessageLookupByLibrary.simpleMessage( + "Ja, jag vill permanent ta bort detta konto och data i alla appar."), "confirmPassword": MessageLookupByLibrary.simpleMessage("Bekräfta lösenord"), "confirmRecoveryKey": MessageLookupByLibrary.simpleMessage( @@ -199,6 +247,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Skapa eller välj album"), "createPublicLink": MessageLookupByLibrary.simpleMessage("Skapa offentlig länk"), + "creatingLink": MessageLookupByLibrary.simpleMessage("Skapar länk..."), "custom": MessageLookupByLibrary.simpleMessage("Anpassad"), "darkTheme": MessageLookupByLibrary.simpleMessage("Mörkt"), "decrypting": MessageLookupByLibrary.simpleMessage("Dekrypterar..."), @@ -209,12 +258,14 @@ class MessageLookup extends MessageLookupByLibrary { "deleteAccountPermanentlyButton": MessageLookupByLibrary.simpleMessage("Radera kontot permanent"), "deleteAlbum": MessageLookupByLibrary.simpleMessage("Radera album"), + "deleteAlbumDialog": MessageLookupByLibrary.simpleMessage( + "Ta också bort foton (och videor) som finns i detta album från alla andra album som de är en del av?"), "deleteAll": MessageLookupByLibrary.simpleMessage("Radera alla"), "deleteEmailRequest": MessageLookupByLibrary.simpleMessage( "Vänligen skicka ett e-postmeddelande till account-deletion@ente.io från din registrerade e-postadress."), "deleteFromDevice": MessageLookupByLibrary.simpleMessage("Radera från enhet"), - "deleteItemCount": m26, + "deleteItemCount": m27, "deletePhotos": MessageLookupByLibrary.simpleMessage("Radera foton"), "deleteReason1": MessageLookupByLibrary.simpleMessage( "Det saknas en viktig funktion som jag behöver"), @@ -226,6 +277,11 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Min orsak finns inte med"), "deleteRequestSLAText": MessageLookupByLibrary.simpleMessage( "Din begäran kommer att hanteras inom 72 timmar."), + "deleteSharedAlbum": + MessageLookupByLibrary.simpleMessage("Radera delat album?"), + "deleteSharedAlbumDialogBody": MessageLookupByLibrary.simpleMessage( + "Albumet kommer att raderas för alla\n\nDu kommer att förlora åtkomst till delade foton i detta album som ägs av andra"), + "details": MessageLookupByLibrary.simpleMessage("Uppgifter"), "disableDownloadWarningBody": MessageLookupByLibrary.simpleMessage( "Besökare kan fortfarande ta skärmdumpar eller spara en kopia av dina foton med hjälp av externa verktyg"), "disableDownloadWarningTitle": @@ -235,10 +291,16 @@ class MessageLookup extends MessageLookupByLibrary { "discover_receipts": MessageLookupByLibrary.simpleMessage("Kvitton"), "doThisLater": MessageLookupByLibrary.simpleMessage("Gör detta senare"), "done": MessageLookupByLibrary.simpleMessage("Klar"), - "dropSupportEmail": m29, + "dropSupportEmail": m30, + "duplicateItemsGroup": m32, "edit": MessageLookupByLibrary.simpleMessage("Redigera"), + "eligible": MessageLookupByLibrary.simpleMessage("berättigad"), "email": MessageLookupByLibrary.simpleMessage("E-post"), - "emailNoEnteAccount": m33, + "emailAlreadyRegistered": MessageLookupByLibrary.simpleMessage( + "E-postadress redan registrerad."), + "emailNoEnteAccount": m35, + "emailNotRegistered": MessageLookupByLibrary.simpleMessage( + "E-postadressen är inte registrerad."), "encryption": MessageLookupByLibrary.simpleMessage("Kryptering"), "encryptionKeys": MessageLookupByLibrary.simpleMessage("Krypteringsnycklar"), @@ -275,12 +337,20 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Exportera din data"), "failedToApplyCode": MessageLookupByLibrary.simpleMessage( "Det gick inte att använda koden"), + "failedToFetchReferralDetails": MessageLookupByLibrary.simpleMessage( + "Det gick inte att hämta hänvisningsdetaljer. Försök igen senare."), + "faq": MessageLookupByLibrary.simpleMessage("Vanliga frågor och svar"), "feedback": MessageLookupByLibrary.simpleMessage("Feedback"), "fileInfoAddDescHint": MessageLookupByLibrary.simpleMessage("Lägg till en beskrivning..."), "fileTypes": MessageLookupByLibrary.simpleMessage("Filtyper"), "forgotPassword": MessageLookupByLibrary.simpleMessage("Glömt lösenord"), + "freeStorageClaimed": + MessageLookupByLibrary.simpleMessage("Gratis lagring begärd"), + "freeStorageOnReferralSuccess": m4, + "freeStorageUsable": MessageLookupByLibrary.simpleMessage( + "Gratis lagringsutrymme som kan användas"), "freeTrial": MessageLookupByLibrary.simpleMessage("Gratis provperiod"), "generatingEncryptionKeys": MessageLookupByLibrary.simpleMessage( "Skapar krypteringsnycklar..."), @@ -309,11 +379,16 @@ class MessageLookup extends MessageLookupByLibrary { "invalidKey": MessageLookupByLibrary.simpleMessage("Ogiltig nyckel"), "invalidRecoveryKey": MessageLookupByLibrary.simpleMessage( "Återställningsnyckeln du angav är inte giltig. Kontrollera att den innehåller 24 ord och kontrollera stavningen av varje ord.\n\nOm du har angett en äldre återställnings kod, se till att den är 64 tecken lång, och kontrollera var och en av bokstäverna."), + "inviteToEnte": + MessageLookupByLibrary.simpleMessage("Bjud in till Ente"), "inviteYourFriends": MessageLookupByLibrary.simpleMessage("Bjud in dina vänner"), "inviteYourFriendsToEnte": MessageLookupByLibrary.simpleMessage( "Bjud in dina vänner till Ente"), - "itemCount": m42, + "itemCount": m44, + "itemsWillBeRemovedFromAlbum": MessageLookupByLibrary.simpleMessage( + "Valda objekt kommer att tas bort från detta album"), + "keepPhotos": MessageLookupByLibrary.simpleMessage("Behåll foton"), "kiloMeterUnit": MessageLookupByLibrary.simpleMessage("km"), "kindlyHelpUsWithThisInformation": MessageLookupByLibrary.simpleMessage( "Vänligen hjälp oss med denna information"), @@ -323,7 +398,7 @@ class MessageLookup extends MessageLookupByLibrary { "linkDeviceLimit": MessageLookupByLibrary.simpleMessage("Enhetsgräns"), "linkEnabled": MessageLookupByLibrary.simpleMessage("Aktiverat"), "linkExpired": MessageLookupByLibrary.simpleMessage("Upphört"), - "linkExpiresOn": m44, + "linkExpiresOn": m46, "linkExpiry": MessageLookupByLibrary.simpleMessage("Länken upphör"), "linkHasExpired": MessageLookupByLibrary.simpleMessage("Länk har upphört att gälla"), @@ -350,7 +425,7 @@ class MessageLookup extends MessageLookupByLibrary { "mlConsentTitle": MessageLookupByLibrary.simpleMessage("Aktivera maskininlärning?"), "moderateStrength": MessageLookupByLibrary.simpleMessage("Måttligt"), - "moveItem": m45, + "moveItem": m49, "moveToAlbum": MessageLookupByLibrary.simpleMessage("Flytta till album"), "movingFilesToAlbum": @@ -372,14 +447,14 @@ class MessageLookup extends MessageLookupByLibrary { "noResults": MessageLookupByLibrary.simpleMessage("Inga resultat"), "noResultsFound": MessageLookupByLibrary.simpleMessage("Inga resultat hittades"), - "notPersonLabel": m48, + "notPersonLabel": m52, "ok": MessageLookupByLibrary.simpleMessage("OK"), - "onlyFamilyAdminCanChangeCode": m49, + "onlyFamilyAdminCanChangeCode": m53, "oops": MessageLookupByLibrary.simpleMessage("Hoppsan"), + "oopsSomethingWentWrong": + MessageLookupByLibrary.simpleMessage("Oj, något gick fel"), "orPickAnExistingOne": MessageLookupByLibrary.simpleMessage("Eller välj en befintlig"), - "orPickFromYourContacts": - MessageLookupByLibrary.simpleMessage("or pick from your contacts"), "passkey": MessageLookupByLibrary.simpleMessage("Nyckel"), "password": MessageLookupByLibrary.simpleMessage("Lösenord"), "passwordChangedSuccessfully": @@ -388,6 +463,8 @@ class MessageLookup extends MessageLookupByLibrary { "passwordStrength": m0, "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( + "Personer som använder din kod"), "photoSmallCase": MessageLookupByLibrary.simpleMessage("foto"), "pleaseCheckYourInternetConnectionAndTryAgain": MessageLookupByLibrary.simpleMessage( @@ -400,7 +477,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Integritetspolicy"), "publicLinkEnabled": MessageLookupByLibrary.simpleMessage("Offentlig länk aktiverad"), - "rateUsOnStore": m56, + "rateUsOnStore": m60, "recover": MessageLookupByLibrary.simpleMessage("Återställ"), "recoverAccount": MessageLookupByLibrary.simpleMessage("Återställ konto"), @@ -417,6 +494,8 @@ class MessageLookup extends MessageLookupByLibrary { "Grymt! Din återställningsnyckel är giltig. Tack för att du verifierade.\n\nKom ihåg att hålla din återställningsnyckel säker med backups."), "recoveryKeyVerified": MessageLookupByLibrary.simpleMessage( "Återställningsnyckel verifierad"), + "recoveryKeyVerifyReason": MessageLookupByLibrary.simpleMessage( + "Din återställningsnyckel är det enda sättet att återställa dina foton om du glömmer ditt lösenord. Du hittar din återställningsnyckel i Inställningar > Säkerhet.\n\nAnge din återställningsnyckel här för att verifiera att du har sparat den ordentligt."), "recoverySuccessful": MessageLookupByLibrary.simpleMessage("Återställning lyckades!"), "recreatePasswordBody": MessageLookupByLibrary.simpleMessage( @@ -424,10 +503,28 @@ class MessageLookup extends MessageLookupByLibrary { "recreatePasswordTitle": MessageLookupByLibrary.simpleMessage("Återskapa lösenord"), "reddit": MessageLookupByLibrary.simpleMessage("Reddit"), + "referralStep1": MessageLookupByLibrary.simpleMessage( + "1. Ge denna kod till dina vänner"), + "referralStep2": MessageLookupByLibrary.simpleMessage( + "2. De registrerar sig för en betalplan"), + "referralStep3": m65, + "referralsAreCurrentlyPaused": MessageLookupByLibrary.simpleMessage( + "Hänvisningar är för närvarande pausade"), "remove": MessageLookupByLibrary.simpleMessage("Ta bort"), + "removeFromAlbum": + MessageLookupByLibrary.simpleMessage("Ta bort från album"), + "removeFromAlbumTitle": + MessageLookupByLibrary.simpleMessage("Ta bort från album?"), "removeLink": MessageLookupByLibrary.simpleMessage("Radera länk"), "removeParticipant": MessageLookupByLibrary.simpleMessage("Ta bort användaren"), + "removeParticipantBody": m66, + "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": + MessageLookupByLibrary.simpleMessage("Ta bort?"), + "removingFromFavorites": + MessageLookupByLibrary.simpleMessage("Tar bort från favoriter..."), "renewSubscription": MessageLookupByLibrary.simpleMessage("Förnya prenumeration"), "resendEmail": MessageLookupByLibrary.simpleMessage( @@ -454,10 +551,16 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Albumnamn"), "searchFileTypesAndNamesEmptySection": MessageLookupByLibrary.simpleMessage("Filtyper och namn"), - "searchResultCount": m63, + "searchResultCount": m68, "selectAlbum": MessageLookupByLibrary.simpleMessage("Välj album"), + "selectAll": MessageLookupByLibrary.simpleMessage("Markera allt"), + "selectFoldersForBackup": MessageLookupByLibrary.simpleMessage( + "Välj mappar för säkerhetskopiering"), "selectLanguage": MessageLookupByLibrary.simpleMessage("Välj språk"), "selectReason": MessageLookupByLibrary.simpleMessage("Välj anledning"), + "selectedFoldersWillBeEncryptedAndBackedUp": + MessageLookupByLibrary.simpleMessage( + "Valda mappar kommer att krypteras och säkerhetskopieras"), "send": MessageLookupByLibrary.simpleMessage("Skicka"), "sendEmail": MessageLookupByLibrary.simpleMessage("Skicka e-post"), "sendInvite": MessageLookupByLibrary.simpleMessage("Skicka inbjudan"), @@ -471,17 +574,19 @@ class MessageLookup extends MessageLookupByLibrary { "share": MessageLookupByLibrary.simpleMessage("Dela"), "shareALink": MessageLookupByLibrary.simpleMessage("Dela en länk"), "shareLink": MessageLookupByLibrary.simpleMessage("Dela länk"), - "shareMyVerificationID": m66, + "shareMyVerificationID": m71, "shareTextConfirmOthersVerificationID": m7, "shareTextRecommendUsingEnte": MessageLookupByLibrary.simpleMessage( "Ladda ner Ente så att vi enkelt kan dela bilder och videor med originell kvalitet\n\nhttps://ente.io"), + "shareTextReferralCode": m72, "shareWithNonenteUsers": MessageLookupByLibrary.simpleMessage( "Dela med icke-Ente användare"), - "shareWithPeopleSectionTitle": m68, + "shareWithPeopleSectionTitle": m73, "shareYourFirstAlbum": MessageLookupByLibrary.simpleMessage("Dela ditt första album"), "sharedAlbumSectionDescription": MessageLookupByLibrary.simpleMessage( "Skapa delade och samarbetande album med andra Ente användare, inklusive användare med gratisnivån."), + "showMemories": MessageLookupByLibrary.simpleMessage("Visa minnen"), "showPerson": MessageLookupByLibrary.simpleMessage("Visa person"), "signUpTerms": MessageLookupByLibrary.simpleMessage( "Jag samtycker till användarvillkoren och integritetspolicyn"), @@ -495,6 +600,11 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage( "Något gick fel, vänligen försök igen"), "sorry": MessageLookupByLibrary.simpleMessage("Förlåt"), + "sorryCouldNotAddToFavorites": MessageLookupByLibrary.simpleMessage( + "Tyvärr, kunde inte lägga till i favoriterna!"), + "sorryCouldNotRemoveFromFavorites": + MessageLookupByLibrary.simpleMessage( + "Tyvärr kunde inte ta bort från favoriter!"), "sorryWeCouldNotGenerateSecureKeysOnThisDevicennplease": MessageLookupByLibrary.simpleMessage( "Tyvärr, vi kunde inte generera säkra nycklar på den här enheten.\n\nVänligen registrera dig från en annan enhet."), @@ -505,6 +615,8 @@ class MessageLookup extends MessageLookupByLibrary { "storageInGB": m1, "strongStrength": MessageLookupByLibrary.simpleMessage("Starkt"), "subscribe": MessageLookupByLibrary.simpleMessage("Prenumerera"), + "subscribeToEnableSharing": MessageLookupByLibrary.simpleMessage( + "Du behöver en aktiv betald prenumeration för att möjliggöra delning."), "subscription": MessageLookupByLibrary.simpleMessage("Prenumeration"), "tapToCopy": MessageLookupByLibrary.simpleMessage("tryck för att kopiera"), @@ -520,11 +632,12 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage( "Återställningsnyckeln du angav är felaktig"), "theme": MessageLookupByLibrary.simpleMessage("Tema"), + "theyAlsoGetXGb": m8, "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": m78, + "thisIsPersonVerificationId": m83, "thisIsYourVerificationId": MessageLookupByLibrary.simpleMessage( "Detta är ditt verifierings-ID"), "thisWillLogYouOutOfTheFollowingDevice": @@ -534,6 +647,7 @@ class MessageLookup extends MessageLookupByLibrary { "Detta kommer att logga ut dig från denna enhet!"), "toResetVerifyEmail": MessageLookupByLibrary.simpleMessage( "För att återställa ditt lösenord måste du först bekräfta din e-postadress."), + "total": MessageLookupByLibrary.simpleMessage("totalt"), "trash": MessageLookupByLibrary.simpleMessage("Papperskorg"), "tryAgain": MessageLookupByLibrary.simpleMessage("Försök igen"), "twitter": MessageLookupByLibrary.simpleMessage("Twitter"), @@ -544,11 +658,18 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Tvåfaktorsautentisering"), "twofactorSetup": MessageLookupByLibrary.simpleMessage("Tvåfaktorskonfiguration"), + "unavailableReferralCode": MessageLookupByLibrary.simpleMessage( + "Tyvärr är denna kod inte tillgänglig."), + "unselectAll": MessageLookupByLibrary.simpleMessage("Avmarkera alla"), "update": MessageLookupByLibrary.simpleMessage("Uppdatera"), + "updatingFolderSelection": + MessageLookupByLibrary.simpleMessage("Uppdaterar mappval..."), "upgrade": MessageLookupByLibrary.simpleMessage("Uppgradera"), - "uploadingMultipleMemories": m83, + "uploadingMultipleMemories": m88, "uploadingSingleMemory": MessageLookupByLibrary.simpleMessage("Bevarar 1 minne..."), + "usableReferralStorageInfo": MessageLookupByLibrary.simpleMessage( + "Användbart lagringsutrymme begränsas av din nuvarande plan. Överskrider du lagringsutrymmet kommer automatiskt att kunna använda det när du uppgraderar din plan."), "useAsCover": MessageLookupByLibrary.simpleMessage("Använd som omslag"), "useRecoveryKey": MessageLookupByLibrary.simpleMessage("Använd återställningsnyckel"), @@ -557,7 +678,7 @@ class MessageLookup extends MessageLookupByLibrary { "verify": MessageLookupByLibrary.simpleMessage("Bekräfta"), "verifyEmail": MessageLookupByLibrary.simpleMessage("Bekräfta e-postadress"), - "verifyEmailID": m85, + "verifyEmailID": m90, "verifyPasskey": MessageLookupByLibrary.simpleMessage("Verifiera nyckel"), "verifyPassword": @@ -578,15 +699,18 @@ class MessageLookup extends MessageLookupByLibrary { "welcomeBack": MessageLookupByLibrary.simpleMessage("Välkommen tillbaka!"), "whatsNew": MessageLookupByLibrary.simpleMessage("Nyheter"), - "yearsAgo": m87, + "yearsAgo": m92, "yes": MessageLookupByLibrary.simpleMessage("Ja"), "yesCancel": MessageLookupByLibrary.simpleMessage("Ja, avbryt"), "yesConvertToViewer": MessageLookupByLibrary.simpleMessage("Ja, konvertera till bildvy"), "yesDelete": MessageLookupByLibrary.simpleMessage("Ja, radera"), "yesLogout": MessageLookupByLibrary.simpleMessage("Ja, logga ut"), + "yesRemove": MessageLookupByLibrary.simpleMessage("Ja, ta bort"), "yesRenew": MessageLookupByLibrary.simpleMessage("Ja, förnya"), "you": MessageLookupByLibrary.simpleMessage("Du"), + "youCanAtMaxDoubleYourStorage": MessageLookupByLibrary.simpleMessage( + "* Du kan max fördubbla ditt lagringsutrymme"), "yourAccountHasBeenDeleted": MessageLookupByLibrary.simpleMessage("Ditt konto har raderats") }; diff --git a/mobile/lib/generated/intl/messages_ta.dart b/mobile/lib/generated/intl/messages_ta.dart index bcf99a6fa4..30c00c6d72 100644 --- a/mobile/lib/generated/intl/messages_ta.dart +++ b/mobile/lib/generated/intl/messages_ta.dart @@ -48,8 +48,6 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("தவறான மின்னஞ்சல் முகவரி"), "kindlyHelpUsWithThisInformation": MessageLookupByLibrary.simpleMessage( "இந்த தகவலுடன் தயவுசெய்து எங்களுக்கு உதவுங்கள்"), - "orPickFromYourContacts": - MessageLookupByLibrary.simpleMessage("or pick from your contacts"), "verify": MessageLookupByLibrary.simpleMessage("சரிபார்க்கவும்") }; } diff --git a/mobile/lib/generated/intl/messages_te.dart b/mobile/lib/generated/intl/messages_te.dart index 23005c447f..5e415c9da0 100644 --- a/mobile/lib/generated/intl/messages_te.dart +++ b/mobile/lib/generated/intl/messages_te.dart @@ -21,8 +21,5 @@ class MessageLookup extends MessageLookupByLibrary { String get localeName => 'te'; final messages = _notInlinedMessages(_notInlinedMessages); - static Map _notInlinedMessages(_) => { - "orPickFromYourContacts": - MessageLookupByLibrary.simpleMessage("or pick from your contacts") - }; + static Map _notInlinedMessages(_) => {}; } diff --git a/mobile/lib/generated/intl/messages_th.dart b/mobile/lib/generated/intl/messages_th.dart index 1d13b1ef24..1c54ec61e3 100644 --- a/mobile/lib/generated/intl/messages_th.dart +++ b/mobile/lib/generated/intl/messages_th.dart @@ -20,30 +20,30 @@ typedef String MessageIfAbsent(String messageStr, List args); class MessageLookup extends MessageLookupByLibrary { String get localeName => 'th'; - static String m10(count) => "${Intl.plural(count, other: 'เพิ่มรายการ')}"; + static String m11(count) => "${Intl.plural(count, other: 'เพิ่มรายการ')}"; - static String m16(versionValue) => "รุ่น: ${versionValue}"; + static String m17(versionValue) => "รุ่น: ${versionValue}"; - static String m26(count) => + static String m27(count) => "${Intl.plural(count, one: 'ลบ ${count} รายการ', other: 'ลบ ${count} รายการ')}"; - static String m27(currentlyDeleting, totalCount) => + static String m28(currentlyDeleting, totalCount) => "กำลังลบ ${currentlyDeleting} / ${totalCount}"; - static String m29(supportEmail) => + static String m30(supportEmail) => "กรุณาส่งอีเมลไปที่ ${supportEmail} จากที่อยู่อีเมลที่คุณลงทะเบียนไว้"; - static String m41(currentlyProcessing, totalCount) => + static String m43(currentlyProcessing, totalCount) => "กำลังประมวลผล ${currentlyProcessing} / ${totalCount}"; - static String m42(count) => "${Intl.plural(count, other: '${count} รายการ')}"; + static String m44(count) => "${Intl.plural(count, other: '${count} รายการ')}"; - static String m45(count) => "${Intl.plural(count, other: 'ย้ายรายการ')}"; + static String m49(count) => "${Intl.plural(count, other: 'ย้ายรายการ')}"; static String m0(passwordStrengthValue) => "ความแข็งแรงของรหัสผ่าน: ${passwordStrengthValue}"; - static String m73( + static String m78( usedAmount, usedStorageUnit, totalAmount, totalStorageUnit) => "ใช้ไป ${usedAmount} ${usedStorageUnit} จาก ${totalAmount} ${totalStorageUnit}"; @@ -60,7 +60,7 @@ class MessageLookup extends MessageLookupByLibrary { "addANewEmail": MessageLookupByLibrary.simpleMessage("เพิ่มอีเมลใหม่"), "addCollaborator": MessageLookupByLibrary.simpleMessage("เพิ่มผู้ทำงานร่วมกัน"), - "addItem": m10, + "addItem": m11, "addMore": MessageLookupByLibrary.simpleMessage("เพิ่มอีก"), "addToAlbum": MessageLookupByLibrary.simpleMessage("เพิ่มไปยังอัลบั้ม"), "addViewer": MessageLookupByLibrary.simpleMessage("เพิ่มผู้ชม"), @@ -77,7 +77,7 @@ class MessageLookup extends MessageLookupByLibrary { "androidBiometricSuccess": MessageLookupByLibrary.simpleMessage("สำเร็จ"), "androidCancelButton": MessageLookupByLibrary.simpleMessage("ยกเลิก"), - "appVersion": m16, + "appVersion": m17, "apply": MessageLookupByLibrary.simpleMessage("นำไปใช้"), "askDeleteReason": MessageLookupByLibrary.simpleMessage( "เหตุผลหลักที่คุณลบบัญชีคืออะไร?"), @@ -130,8 +130,8 @@ class MessageLookup extends MessageLookupByLibrary { "deleteEmptyAlbumsWithQuestionMark": MessageLookupByLibrary.simpleMessage( "ลบอัลบั้มที่ว่างเปล่าหรือไม่?"), - "deleteItemCount": m26, - "deleteProgress": m27, + "deleteItemCount": m27, + "deleteProgress": m28, "deleteReason1": MessageLookupByLibrary.simpleMessage( "ขาดคุณสมบัติสำคัญที่ฉันต้องการ"), "deleteReason2": MessageLookupByLibrary.simpleMessage( @@ -143,7 +143,7 @@ class MessageLookup extends MessageLookupByLibrary { "deleteRequestSLAText": MessageLookupByLibrary.simpleMessage( "คำขอของคุณจะได้รับการดำเนินการภายใน 72 ชั่วโมง"), "doThisLater": MessageLookupByLibrary.simpleMessage("ทำในภายหลัง"), - "dropSupportEmail": m29, + "dropSupportEmail": m30, "edit": MessageLookupByLibrary.simpleMessage("แก้ไข"), "editLocationTagTitle": MessageLookupByLibrary.simpleMessage("แก้ไขตำแหน่ง"), @@ -172,7 +172,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("เพิ่มคำอธิบาย..."), "forgotPassword": MessageLookupByLibrary.simpleMessage("ลืมรหัสผ่าน"), "freeTrial": MessageLookupByLibrary.simpleMessage("ทดลองใช้ฟรี"), - "genericProgress": m41, + "genericProgress": m43, "goToSettings": MessageLookupByLibrary.simpleMessage("ไปที่การตั้งค่า"), "hide": MessageLookupByLibrary.simpleMessage("ซ่อน"), "hostedAtOsmFrance": @@ -195,7 +195,7 @@ class MessageLookup extends MessageLookupByLibrary { "invalidKey": MessageLookupByLibrary.simpleMessage("รหัสไม่ถูกต้อง"), "invalidRecoveryKey": MessageLookupByLibrary.simpleMessage( "คีย์การกู้คืนที่คุณป้อนไม่ถูกต้อง โปรดตรวจสอบให้แน่ใจว่ามี 24 คำ และตรวจสอบการสะกดของแต่ละคำ\n\nหากคุณป้อนรหัสกู้คืนที่เก่ากว่า ตรวจสอบให้แน่ใจว่ามีความยาว 64 ตัวอักษร และตรวจสอบแต่ละตัวอักษร"), - "itemCount": m42, + "itemCount": m44, "kindlyHelpUsWithThisInformation": MessageLookupByLibrary.simpleMessage("กรุณาช่วยเราด้วยข้อมูลนี้"), "lastUpdated": MessageLookupByLibrary.simpleMessage("อัปเดตล่าสุด"), @@ -213,7 +213,7 @@ class MessageLookup extends MessageLookupByLibrary { "map": MessageLookupByLibrary.simpleMessage("แผนที่"), "maps": MessageLookupByLibrary.simpleMessage("แผนที่"), "moderateStrength": MessageLookupByLibrary.simpleMessage("ปานกลาง"), - "moveItem": m45, + "moveItem": m49, "moveToAlbum": MessageLookupByLibrary.simpleMessage("ย้ายไปยังอัลบั้ม"), "name": MessageLookupByLibrary.simpleMessage("ชื่อ"), "newest": MessageLookupByLibrary.simpleMessage("ใหม่สุด"), @@ -231,8 +231,6 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("ผู้มีส่วนร่วม OpenStreetMap"), "orPickAnExistingOne": MessageLookupByLibrary.simpleMessage("หรือเลือกที่มีอยู่แล้ว"), - "orPickFromYourContacts": - MessageLookupByLibrary.simpleMessage("or pick from your contacts"), "password": MessageLookupByLibrary.simpleMessage("รหัสผ่าน"), "passwordChangedSuccessfully": MessageLookupByLibrary.simpleMessage("เปลี่ยนรหัสผ่านสำเร็จ"), @@ -308,7 +306,7 @@ class MessageLookup extends MessageLookupByLibrary { "storageBreakupFamily": MessageLookupByLibrary.simpleMessage("ครอบครัว"), "storageBreakupYou": MessageLookupByLibrary.simpleMessage("คุณ"), - "storageUsageInfo": m73, + "storageUsageInfo": m78, "strongStrength": MessageLookupByLibrary.simpleMessage("แข็งแรง"), "syncStopped": MessageLookupByLibrary.simpleMessage("หยุดการซิงค์แล้ว"), "syncing": MessageLookupByLibrary.simpleMessage("กำลังซิงค์..."), diff --git a/mobile/lib/generated/intl/messages_ti.dart b/mobile/lib/generated/intl/messages_ti.dart index 5088ccbb47..775cc78213 100644 --- a/mobile/lib/generated/intl/messages_ti.dart +++ b/mobile/lib/generated/intl/messages_ti.dart @@ -21,8 +21,5 @@ class MessageLookup extends MessageLookupByLibrary { String get localeName => 'ti'; final messages = _notInlinedMessages(_notInlinedMessages); - static Map _notInlinedMessages(_) => { - "orPickFromYourContacts": - MessageLookupByLibrary.simpleMessage("or pick from your contacts") - }; + static Map _notInlinedMessages(_) => {}; } diff --git a/mobile/lib/generated/intl/messages_tr.dart b/mobile/lib/generated/intl/messages_tr.dart index 8c0278036b..9710f11e55 100644 --- a/mobile/lib/generated/intl/messages_tr.dart +++ b/mobile/lib/generated/intl/messages_tr.dart @@ -20,214 +20,316 @@ typedef String MessageIfAbsent(String messageStr, List args); class MessageLookup extends MessageLookupByLibrary { String get localeName => 'tr'; - static String m9(count) => - "${Intl.plural(count, zero: 'Ortak çalışan ekle', one: 'Ortak çalışan ekle', other: 'Ortak çalışan ekle')}"; + static String m9(title) => "${title} (Ben)"; static String m10(count) => + "${Intl.plural(count, zero: 'Ortak çalışan ekle', one: 'Ortak çalışan ekle', other: 'Ortak çalışan ekle')}"; + + static String m11(count) => "${Intl.plural(count, one: 'Öğeyi taşı', other: 'Öğeleri taşı')}"; - static String m11(storageAmount, endDate) => + static String m12(storageAmount, endDate) => "${storageAmount} eklentiniz ${endDate} tarihine kadar geçerlidir"; - static String m12(count) => + static String m13(count) => "${Intl.plural(count, zero: 'Görüntüleyen ekle', one: 'Görüntüleyen ekle', other: 'Görüntüleyen ekle')}"; - static String m13(emailOrName) => "${emailOrName} tarafından eklendi"; + static String m14(emailOrName) => "${emailOrName} tarafından eklendi"; - static String m14(albumName) => "${albumName} albümüne başarıyla eklendi"; + static String m15(albumName) => "${albumName} albümüne başarıyla eklendi"; - static String m15(count) => + static String m16(count) => "${Intl.plural(count, zero: 'Katılımcı Yok', one: '1 Katılımcı', other: '${count} Katılımcı')}"; - static String m16(versionValue) => "Sürüm: ${versionValue}"; + static String m17(versionValue) => "Sürüm: ${versionValue}"; - static String m18(paymentProvider) => + static String m18(freeAmount, storageUnit) => + "${freeAmount} ${storageUnit} ücretsiz"; + + static String m19(paymentProvider) => "Lütfen önce mevcut aboneliğinizi ${paymentProvider} adresinden iptal edin"; static String m3(user) => "${user}, bu albüme daha fazla fotoğraf ekleyemeyecek.\n\nAncak, kendi eklediği mevcut fotoğrafları kaldırmaya devam edebilecektir"; - static String m19(isFamilyMember, storageAmountInGb) => + static String m20(isFamilyMember, storageAmountInGb) => "${Intl.select(isFamilyMember, { 'true': 'Şu ana kadar aileniz ${storageAmountInGb} GB aldı', 'false': 'Şu ana kadar ${storageAmountInGb} GB aldınız', 'other': 'Şu ana kadar ${storageAmountInGb} GB aldınız!', })}"; - static String m20(albumName) => + static String m21(albumName) => "${albumName} için ortak çalışma bağlantısı oluşturuldu"; - static String m23(familyAdminEmail) => + static String m22(count) => + "${Intl.plural(count, zero: '0 işbirlikçi eklendi', one: '1 işbirlikçi eklendi', other: '${count} işbirlikçi eklendi')}"; + + static String m23(email, numOfDays) => + "Güvenilir bir kişi olarak ${email} eklemek üzeresiniz. Eğer ${numOfDays} gün boyunca yoksanız hesabınızı kurtarabilecekler."; + + static String m24(familyAdminEmail) => "Aboneliğinizi yönetmek için lütfen ${familyAdminEmail} ile iletişime geçin"; - static String m24(provider) => + static String m25(provider) => "Lütfen ${provider} aboneliğinizi yönetmek için support@ente.io adresinden bizimle iletişime geçin."; - static String m25(endpoint) => "${endpoint}\'e bağlanıldı"; + static String m26(endpoint) => "${endpoint}\'e bağlanıldı"; - static String m26(count) => + static String m27(count) => "${Intl.plural(count, one: 'Delete ${count} item', other: 'Delete ${count} items')}"; - static String m27(currentlyDeleting, totalCount) => + static String m28(currentlyDeleting, totalCount) => "Siliniyor ${currentlyDeleting} / ${totalCount}"; - static String m28(albumName) => + static String m29(albumName) => "Bu, \"${albumName}\"e erişim için olan genel bağlantıyı kaldıracaktır."; - static String m29(supportEmail) => + static String m30(supportEmail) => "Lütfen kayıtlı e-posta adresinizden ${supportEmail} adresine bir e-posta gönderin"; - static String m30(count, storageSaved) => + static String m31(count, storageSaved) => "You have cleaned up ${Intl.plural(count, one: '${count} duplicate file', other: '${count} duplicate files')}, saving (${storageSaved}!)"; - static String m31(count, formattedSize) => + static String m32(count, formattedSize) => "${count} dosyalar, ${formattedSize} her biri"; - static String m32(newEmail) => "E-posta ${newEmail} olarak değiştirildi"; + static String m33(newEmail) => "E-posta ${newEmail} olarak değiştirildi"; - static String m33(email) => + static String m34(email) => "${email} bir Ente hesabına sahip değil"; + + static String m35(email) => "${email}, Ente hesabı bulunmamaktadır.\n\nOnlarla fotoğraf paylaşımı için bir davet gönder."; - static String m35(count, formattedNumber) => + static String m36(text) => "${text} için ekstra fotoğraflar bulundu"; + + static String m37(count, formattedNumber) => "Bu cihazdaki ${Intl.plural(count, one: '1 file', other: '${formattedNumber} dosya')} güvenli bir şekilde yedeklendi"; - static String m36(count, formattedNumber) => + static String m38(count, formattedNumber) => "Bu albümdeki ${Intl.plural(count, one: '1 file', other: '${formattedNumber} dosya')} güvenli bir şekilde yedeklendi"; static String m4(storageAmountInGB) => "Birisinin davet kodunuzu uygulayıp ücretli hesap açtığı her seferede ${storageAmountInGB} GB"; - static String m37(endDate) => "Ücretsiz deneme ${endDate} sona erir"; + static String m39(endDate) => "Ücretsiz deneme ${endDate} sona erir"; - static String m39(sizeInMBorGB) => "${sizeInMBorGB} yer açın"; + static String m40(count) => + "Aktif bir aboneliğiniz olduğu sürece ente\'de ${Intl.plural(count, one: 'it', other: 'them')} erişimine devam edebilirsiniz"; - static String m40(count, formattedSize) => + static String m41(sizeInMBorGB) => "${sizeInMBorGB} yer açın"; + + static String m42(count, formattedSize) => "${Intl.plural(count, one: 'Yer açmak için cihazdan silinebilir ${formattedSize}', other: 'Yer açmak için cihazdan silinebilir ${formattedSize}')}"; - static String m41(currentlyProcessing, totalCount) => + static String m43(currentlyProcessing, totalCount) => "Siliniyor ${currentlyProcessing} / ${totalCount}"; - static String m42(count) => + static String m44(count) => "${Intl.plural(count, one: '${count} öğe', other: '${count} öğeler')}"; - static String m44(expiryTime) => + static String m45(email) => + "${email} sizi güvenilir bir kişi olmaya davet etti"; + + static String m46(expiryTime) => "Bu bağlantı ${expiryTime} dan sonra geçersiz olacaktır"; + static String m47(email) => "Kişiyi ${email} adresine bağlayın"; + + static String m48(personName, email) => + "Bu, ${personName} ile ${email} arasında bağlantı kuracaktır."; + static String m5(count, formattedCount) => "${Intl.plural(count, zero: 'anı yok', one: '${formattedCount} anı', other: '${formattedCount} anılar')}"; - static String m45(count) => + static String m49(count) => "${Intl.plural(count, one: 'Öğeyi taşı', other: 'Öğeleri taşı')}"; - static String m46(albumName) => "${albumName} adlı albüme başarıyla taşındı"; + static String m50(albumName) => "${albumName} adlı albüme başarıyla taşındı"; + + static String m51(personName) => "${personName} için öneri yok"; + + static String m52(name) => "${name} değil mi?"; + + static String m53(familyAdminEmail) => + "Kodunuzu değiştirmek için lütfen ${familyAdminEmail} ile iletişime geçin."; static String m0(passwordStrengthValue) => "Şifrenin güçlülük seviyesi: ${passwordStrengthValue}"; - static String m50(providerName) => + static String m54(providerName) => "Sizden ücret alındıysa lütfen ${providerName} destek ekibiyle görüşün"; - static String m53(toEmail) => "Lütfen bize ${toEmail} adresinden ulaşın"; + static String m55(count) => + "${Intl.plural(count, zero: '0 fotoğraf', one: '1 fotoğraf', other: '${count} fotoğraflar')}"; - static String m54(toEmail) => + static String m56(endDate) => + "Ücretsiz deneme süresi ${endDate} tarihine kadar geçerlidir.\nDaha sonra ücretli bir plan seçebilirsiniz."; + + static String m57(toEmail) => "Lütfen bize ${toEmail} adresinden ulaşın"; + + static String m58(toEmail) => "Lütfen günlükleri şu adrese gönderin\n${toEmail}"; - static String m56(storeName) => "Bizi ${storeName} üzerinden değerlendirin"; + static String m59(folderName) => "İşleniyor ${folderName}..."; - static String m60(storageInGB) => "3. Hepimiz ${storageInGB} GB* bedava alın"; + static String m60(storeName) => "Bizi ${storeName} üzerinden değerlendirin"; - static String m61(userEmail) => + static String m61(name) => "Sizi ${name}\'e yeniden atadım"; + + static String m62(days, email) => + "Hesabınıza ${days} gün sonra erişebilirsiniz. ${email} adresine bir bildirim gönderilecektir."; + + static String m63(email) => + "Artık yeni bir parola belirleyerek ${email} hesabını kurtarabilirsiniz."; + + static String m64(email) => "${email} hesabınızı kurtarmaya çalışıyor."; + + static String m65(storageInGB) => "3. Hepimiz ${storageInGB} GB* bedava alın"; + + static String m66(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 m62(endDate) => "Abonelik ${endDate} tarihinde yenilenir"; + static String m67(endDate) => "Abonelik ${endDate} tarihinde yenilenir"; - static String m63(count) => + static String m68(count) => "${Intl.plural(count, one: '${count} yıl önce', other: '${count} yıl önce')}"; + static String m69(snapshotLength, searchLength) => + "Bölüm uzunluğu uyuşmazlığı: ${snapshotLength} != ${searchLength}"; + static String m6(count) => "${count} seçildi"; - static String m65(count, yourCount) => + static String m70(count, yourCount) => "Seçilenler: ${count} (${yourCount} sizin seçiminiz)"; - static String m66(verificationID) => + static String m71(verificationID) => "İşte ente.io için doğrulama kimliğim: ${verificationID}."; static String m7(verificationID) => "Merhaba, bu ente.io doğrulama kimliğinizin doğruluğunu onaylayabilir misiniz: ${verificationID}"; - static String m68(numberOfPeople) => + static String m72(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 m73(numberOfPeople) => "${Intl.plural(numberOfPeople, zero: 'Belirli kişilerle paylaş', one: '1 kişiyle paylaşıldı', other: '${numberOfPeople} kişiyle paylaşıldı')}"; - static String m69(emailIDs) => "${emailIDs} ile paylaşıldı"; + static String m74(emailIDs) => "${emailIDs} ile paylaşıldı"; - static String m70(fileType) => "Bu ${fileType}, cihazınızdan silinecek."; + static String m75(fileType) => "Bu ${fileType}, cihazınızdan silinecek."; + + static String m76(fileType) => + "${fileType} Ente ve cihazınızdan silinecektir."; + + static String m77(fileType) => "${fileType} Ente\'den silinecektir."; static String m1(storageAmountInGB) => "${storageAmountInGB} GB"; - static String m73( + static String m78( usedAmount, usedStorageUnit, totalAmount, totalStorageUnit) => "${usedAmount} ${usedStorageUnit} / ${totalAmount} ${totalStorageUnit} kullanıldı"; - static String m75(endDate) => + static String m79(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 m80(endDate) => "Aboneliğiniz ${endDate} tarihinde iptal edilecektir"; - static String m76(completed, total) => "${completed}/${total} anı korundu"; + static String m81(completed, total) => "${completed}/${total} anı korundu"; + + static String m82(ignoreReason) => + "Yüklemek için dokunun, yükleme şu anda ${ignoreReason} nedeniyle yok sayılıyor"; static String m8(storageAmountInGB) => "Aynı zamanda ${storageAmountInGB} GB alıyorlar"; - static String m78(email) => "Bu, ${email}\'in Doğrulama Kimliği"; + static String m83(email) => "Bu, ${email}\'in Doğrulama Kimliği"; - static String m84(endDate) => "${endDate} tarihine kadar geçerli"; + static String m84(count) => + "${Intl.plural(count, zero: 'yakında', one: '1 gün', other: '${count} gün')}"; - static String m85(email) => "${email} doğrula"; + static String m85(email) => + "${email} ile eski bir irtibat kişisi olmaya davet edildiniz."; + + static String m86(galleryType) => + "Galeri türü ${galleryType} yeniden adlandırma için desteklenmiyor"; + + static String m87(ignoreReason) => + "Yükleme ${ignoreReason} nedeniyle yok sayıldı"; + + static String m88(count) => "${count} anı korunuyor..."; + + static String m89(endDate) => "${endDate} tarihine kadar geçerli"; + + static String m90(email) => "${email} doğrula"; + + static String m91(count) => + "${Intl.plural(count, zero: '0 Görüntüleyen eklendi', one: '1 Görüntüleyen eklendi', other: '${count} Görüntüleyen eklendi')}"; static String m2(email) => "E-postayı ${email} adresine gönderdik"; - static String m87(count) => + static String m92(count) => "${Intl.plural(count, one: '${count} yıl önce', other: '${count} yıl önce')}"; - static String m88(storageSaved) => + static String m93(storageSaved) => "Başarılı bir şekilde ${storageSaved} alanını boşalttınız!"; final messages = _notInlinedMessages(_notInlinedMessages); static Map _notInlinedMessages(_) => { + "aNewVersionOfEnteIsAvailable": MessageLookupByLibrary.simpleMessage( + "Ente için yeni bir sürüm mevcut."), "about": MessageLookupByLibrary.simpleMessage("Hakkında"), + "acceptTrustInvite": + MessageLookupByLibrary.simpleMessage("Daveti Kabul Et"), "account": MessageLookupByLibrary.simpleMessage("Hesap"), + "accountIsAlreadyConfigured": MessageLookupByLibrary.simpleMessage( + "Hesap zaten yapılandırılmıştır."), + "accountOwnerPersonAppbarTitle": m9, "accountWelcomeBack": MessageLookupByLibrary.simpleMessage("Tekrar hoş geldiniz!"), "ackPasswordLostWarning": MessageLookupByLibrary.simpleMessage( "Şifremi kaybedersem, verilerim uçtan uca şifrelendiği için verilerimi kaybedebileceğimi farkındayım."), "activeSessions": MessageLookupByLibrary.simpleMessage("Aktif oturumlar"), + "add": MessageLookupByLibrary.simpleMessage("Ekle"), + "addAName": MessageLookupByLibrary.simpleMessage("Bir Ad Ekle"), "addANewEmail": MessageLookupByLibrary.simpleMessage("Yeni e-posta ekle"), "addCollaborator": MessageLookupByLibrary.simpleMessage("Düzenleyici ekle"), - "addCollaborators": m9, + "addCollaborators": m10, + "addFiles": MessageLookupByLibrary.simpleMessage("Dosyaları Ekle"), "addFromDevice": MessageLookupByLibrary.simpleMessage("Cihazdan ekle"), - "addItem": m10, + "addItem": m11, "addLocation": MessageLookupByLibrary.simpleMessage("Konum Ekle"), "addLocationButton": MessageLookupByLibrary.simpleMessage("Ekle"), "addMore": MessageLookupByLibrary.simpleMessage("Daha fazla ekle"), + "addName": MessageLookupByLibrary.simpleMessage("İsim Ekle"), + "addNameOrMerge": MessageLookupByLibrary.simpleMessage( + "İsim ekleyin veya birleştirin"), "addNew": MessageLookupByLibrary.simpleMessage("Yeni ekle"), + "addNewPerson": MessageLookupByLibrary.simpleMessage("Yeni kişi ekle"), "addOnPageSubtitle": MessageLookupByLibrary.simpleMessage("Eklentilerin ayrıntıları"), - "addOnValidTill": m11, + "addOnValidTill": m12, "addOns": MessageLookupByLibrary.simpleMessage("Eklentiler"), "addPhotos": MessageLookupByLibrary.simpleMessage("Fotoğraf ekle"), "addSelected": MessageLookupByLibrary.simpleMessage("Seçileni ekle"), "addToAlbum": MessageLookupByLibrary.simpleMessage("Albüme ekle"), + "addToEnte": MessageLookupByLibrary.simpleMessage("Ente\'ye ekle"), "addToHiddenAlbum": MessageLookupByLibrary.simpleMessage("Gizli albüme ekle"), + "addTrustedContact": + MessageLookupByLibrary.simpleMessage("Güvenilir kişi ekle"), "addViewer": MessageLookupByLibrary.simpleMessage("Görüntüleyici ekle"), - "addViewers": m12, + "addViewers": m13, "addYourPhotosNow": MessageLookupByLibrary.simpleMessage( "Fotoğraflarınızı şimdi ekleyin"), "addedAs": MessageLookupByLibrary.simpleMessage("Eklendi"), - "addedBy": m13, - "addedSuccessfullyTo": m14, + "addedBy": m14, + "addedSuccessfullyTo": m15, "addingToFavorites": MessageLookupByLibrary.simpleMessage("Favorilere ekleniyor..."), "advanced": MessageLookupByLibrary.simpleMessage("Gelişmiş"), @@ -238,7 +340,7 @@ class MessageLookup extends MessageLookupByLibrary { "after1Week": MessageLookupByLibrary.simpleMessage("1 hafta sonra"), "after1Year": MessageLookupByLibrary.simpleMessage("1 yıl sonra"), "albumOwner": MessageLookupByLibrary.simpleMessage("Sahip"), - "albumParticipantsCount": m15, + "albumParticipantsCount": m16, "albumTitle": MessageLookupByLibrary.simpleMessage("Albüm Başlığı"), "albumUpdated": MessageLookupByLibrary.simpleMessage("Albüm güncellendi"), @@ -246,14 +348,23 @@ class MessageLookup extends MessageLookupByLibrary { "allClear": MessageLookupByLibrary.simpleMessage("✨ Tamamen temizle"), "allMemoriesPreserved": MessageLookupByLibrary.simpleMessage("Tüm anılar saklandı"), + "allPersonGroupingWillReset": MessageLookupByLibrary.simpleMessage( + "Bu kişi için tüm gruplamalar sıfırlanacak ve bu kişi için yaptığınız tüm önerileri kaybedeceksiniz"), + "allow": MessageLookupByLibrary.simpleMessage("İzin ver"), "allowAddPhotosDescription": MessageLookupByLibrary.simpleMessage( "Bağlantıya sahip olan kişilere, paylaşılan albüme fotoğraf eklemelerine izin ver."), "allowAddingPhotos": MessageLookupByLibrary.simpleMessage("Fotoğraf eklemeye izin ver"), + "allowAppToOpenSharedAlbumLinks": MessageLookupByLibrary.simpleMessage( + "Uygulamanın paylaşılan albüm bağlantılarını açmasına izin ver"), "allowDownloads": MessageLookupByLibrary.simpleMessage("İndirmeye izin ver"), "allowPeopleToAddPhotos": MessageLookupByLibrary.simpleMessage( "Kullanıcıların fotoğraf eklemesine izin ver"), + "allowPermBody": MessageLookupByLibrary.simpleMessage( + "Ente\'nin kitaplığınızı görüntüleyebilmesi ve yedekleyebilmesi için lütfen Ayarlar\'dan fotoğraflarınıza erişime izin verin."), + "allowPermTitle": MessageLookupByLibrary.simpleMessage( + "Fotoğraflara erişime izin verin"), "androidBiometricHint": MessageLookupByLibrary.simpleMessage("Kimliği doğrula"), "androidBiometricNotRecognized": @@ -275,7 +386,10 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Android, iOS, Web, Masaüstü"), "androidSignInTitle": MessageLookupByLibrary.simpleMessage("Kimlik doğrulaması gerekli"), - "appVersion": m16, + "appLock": MessageLookupByLibrary.simpleMessage("Uygulama kilidi"), + "appLockDescriptions": MessageLookupByLibrary.simpleMessage( + "Cihazınızın varsayılan kilit ekranı ile PIN veya parola içeren özel bir kilit ekranı arasında seçim yapın."), + "appVersion": m17, "appleId": MessageLookupByLibrary.simpleMessage("Apple kimliği"), "apply": MessageLookupByLibrary.simpleMessage("Uygula"), "applyCodeTitle": MessageLookupByLibrary.simpleMessage("Kodu girin"), @@ -298,6 +412,9 @@ class MessageLookup extends MessageLookupByLibrary { "Çıkış yapmak istediğinize emin misiniz?"), "areYouSureYouWantToRenew": MessageLookupByLibrary.simpleMessage( "Yenilemek istediğinize emin misiniz?"), + "areYouSureYouWantToResetThisPerson": + MessageLookupByLibrary.simpleMessage( + "Bu kişiyi sıfırlamak istediğinden emin misin?"), "askCancelReason": MessageLookupByLibrary.simpleMessage( "Aboneliğiniz iptal edilmiştir. Bunun sebebini paylaşmak ister misiniz?"), "askDeleteReason": MessageLookupByLibrary.simpleMessage( @@ -320,6 +437,12 @@ class MessageLookup extends MessageLookupByLibrary { "İki faktörlü kimlik doğrulamayı yapılandırmak için lütfen kimlik doğrulaması yapın"), "authToInitiateAccountDeletion": MessageLookupByLibrary.simpleMessage( "Hesap silme işlemini başlatmak için lütfen kimlik doğrulaması yapın"), + "authToManageLegacy": MessageLookupByLibrary.simpleMessage( + "Güvenilir kişilerinizi yönetmek için lütfen kimlik doğrulaması yapın"), + "authToViewPasskey": MessageLookupByLibrary.simpleMessage( + "Geçiş anahtarınızı görüntülemek için lütfen kimlik doğrulaması yapın"), + "authToViewTrashedFiles": MessageLookupByLibrary.simpleMessage( + "Çöp dosyalarınızı görüntülemek için lütfen kimlik doğrulaması yapın"), "authToViewYourActiveSessions": MessageLookupByLibrary.simpleMessage( "Aktif oturumlarınızı görüntülemek için lütfen kimliğinizi doğrulayın"), "authToViewYourHiddenFiles": MessageLookupByLibrary.simpleMessage( @@ -335,24 +458,47 @@ class MessageLookup extends MessageLookupByLibrary { "Kimlik doğrulama başarısız oldu, lütfen tekrar deneyin"), "authenticationSuccessful": MessageLookupByLibrary.simpleMessage("Kimlik doğrulama başarılı!"), + "autoCastDialogBody": MessageLookupByLibrary.simpleMessage( + "Mevcut Cast cihazlarını burada görebilirsiniz."), + "autoCastiOSPermission": MessageLookupByLibrary.simpleMessage( + "Ayarlar\'da Ente Photos uygulaması için Yerel Ağ izinlerinin açık olduğundan emin olun."), + "autoLock": MessageLookupByLibrary.simpleMessage("Otomatik Kilit"), + "autoLockFeatureDescription": MessageLookupByLibrary.simpleMessage( + "Uygulamayı arka plana attıktan sonra kilitlendiği süre"), + "autoLogoutMessage": MessageLookupByLibrary.simpleMessage( + "Teknik aksaklık nedeniyle oturumunuz kapatıldı. Verdiğimiz rahatsızlıktan dolayı özür dileriz."), + "autoPair": MessageLookupByLibrary.simpleMessage("Otomatik eşle"), + "autoPairDesc": MessageLookupByLibrary.simpleMessage( + "Otomatik eşleştirme yalnızca Chromecast destekleyen cihazlarla çalışır."), "available": MessageLookupByLibrary.simpleMessage("Mevcut"), + "availableStorageSpace": m18, "backedUpFolders": MessageLookupByLibrary.simpleMessage("Yedeklenmiş klasörler"), "backup": MessageLookupByLibrary.simpleMessage("Yedekle"), "backupFailed": MessageLookupByLibrary.simpleMessage("Yedekleme başarısız oldu"), + "backupFile": MessageLookupByLibrary.simpleMessage("Yedek Dosyası"), "backupOverMobileData": MessageLookupByLibrary.simpleMessage("Mobil veri ile yedekle"), "backupSettings": MessageLookupByLibrary.simpleMessage("Yedekleme seçenekleri"), + "backupStatus": + MessageLookupByLibrary.simpleMessage("Yedekleme durumu"), + "backupStatusDescription": MessageLookupByLibrary.simpleMessage( + "Eklenen öğeler burada görünecek"), "backupVideos": MessageLookupByLibrary.simpleMessage("Videolari yedekle"), + "birthday": MessageLookupByLibrary.simpleMessage("Doğum Günü"), "blackFridaySale": MessageLookupByLibrary.simpleMessage("Muhteşem Cuma kampanyası"), "blog": MessageLookupByLibrary.simpleMessage("Blog"), "cachedData": MessageLookupByLibrary.simpleMessage("Ön belleğe alınan veri"), "calculating": MessageLookupByLibrary.simpleMessage("Hesaplanıyor..."), + "canNotOpenBody": MessageLookupByLibrary.simpleMessage( + "Üzgünüz, Bu albüm uygulama içinde açılamadı."), + "canNotOpenTitle": + MessageLookupByLibrary.simpleMessage("Albüm açılamadı"), "canNotUploadToAlbumsOwnedByOthers": MessageLookupByLibrary.simpleMessage( "Başkalarına ait albümlere yüklenemez"), @@ -362,35 +508,64 @@ class MessageLookup extends MessageLookupByLibrary { "canOnlyRemoveFilesOwnedByYou": MessageLookupByLibrary.simpleMessage( "Yalnızca size ait dosyaları kaldırabilir"), "cancel": MessageLookupByLibrary.simpleMessage("İptal Et"), - "cancelOtherSubscription": m18, + "cancelAccountRecovery": + MessageLookupByLibrary.simpleMessage("Kurtarma işlemini iptal et"), + "cancelAccountRecoveryBody": MessageLookupByLibrary.simpleMessage( + "Kurtarmayı iptal etmek istediğinize emin misiniz?"), + "cancelOtherSubscription": m19, "cancelSubscription": MessageLookupByLibrary.simpleMessage("Abonelik iptali"), "cannotAddMorePhotosAfterBecomingViewer": m3, "cannotDeleteSharedFiles": MessageLookupByLibrary.simpleMessage("Dosyalar silinemiyor"), + "castAlbum": MessageLookupByLibrary.simpleMessage("Yayın albümü"), + "castIPMismatchBody": MessageLookupByLibrary.simpleMessage( + "Lütfen TV ile aynı ağda olduğunuzdan emin olun."), + "castIPMismatchTitle": MessageLookupByLibrary.simpleMessage( + "Albüm yüklenirken hata oluştu"), "castInstruction": MessageLookupByLibrary.simpleMessage( "Eşleştirmek istediğiniz cihazda cast.ente.io adresini ziyaret edin.\n\nAlbümü TV\'nizde oynatmak için aşağıdaki kodu girin."), "centerPoint": MessageLookupByLibrary.simpleMessage("Merkez noktası"), + "change": MessageLookupByLibrary.simpleMessage("Değiştir"), "changeEmail": MessageLookupByLibrary.simpleMessage("E-posta adresini değiştir"), "changeLocationOfSelectedItems": MessageLookupByLibrary.simpleMessage( "Seçilen öğelerin konumu değiştirilsin mi?"), + "changeLogBackupStatusContent": MessageLookupByLibrary.simpleMessage( + "Ente\'ye yüklenen tüm dosyaların, hatalar ve sıraya alınanlar da dahil olmak üzere bir günlüğünü ekledik."), + "changeLogBackupStatusTitle": + MessageLookupByLibrary.simpleMessage("Yedekleme Durumu"), + "changeLogDiscoverContent": MessageLookupByLibrary.simpleMessage( + "Kimlik kartlarınızın, notlarınızın ve hatta memlerinizin fotoğraflarını mı arıyorsunuz? Arama sekmesine gidin ve Keşfet\'e göz atın. Anlamsal aramamıza dayanarak, sizin için önemli olabilecek fotoğrafları bulabileceğiniz bir yerdir.\\n\\n Sadece Makine Öğrenimini etkinleştirdiyseniz kullanılabilir."), + "changeLogDiscoverTitle": + MessageLookupByLibrary.simpleMessage("Keşfet"), + "changeLogMagicSearchImprovementContent": + MessageLookupByLibrary.simpleMessage( + "Sihirli aramayı çok daha hızlı olacak şekilde geliştirdik, böylece aradığınızı bulmak için beklemek zorunda kalmazsınız."), + "changeLogMagicSearchImprovementTitle": + MessageLookupByLibrary.simpleMessage("Sihirli Arama İyileştirme"), "changePassword": MessageLookupByLibrary.simpleMessage("Sifrenizi değiştirin"), "changePasswordTitle": MessageLookupByLibrary.simpleMessage("Parolanızı değiştirin"), "changePermissions": MessageLookupByLibrary.simpleMessage("İzinleri değiştir?"), + "changeYourReferralCode": MessageLookupByLibrary.simpleMessage( + "Referans kodunuzu değiştirin"), "checkForUpdates": MessageLookupByLibrary.simpleMessage("Güncellemeleri kontol et"), "checkInboxAndSpamFolder": MessageLookupByLibrary.simpleMessage( "Lütfen doğrulama işlemini tamamlamak için gelen kutunuzu (ve spam klasörünüzü) kontrol edin"), + "checkStatus": + MessageLookupByLibrary.simpleMessage("Durumu kontrol et"), "checking": MessageLookupByLibrary.simpleMessage("Kontrol ediliyor..."), + "checkingModels": MessageLookupByLibrary.simpleMessage( + "Modelleri kontrol ediyorum..."), "claimFreeStorage": MessageLookupByLibrary.simpleMessage("Bedava alan talep edin"), "claimMore": MessageLookupByLibrary.simpleMessage("Arttır!"), "claimed": MessageLookupByLibrary.simpleMessage("Alındı"), - "claimedStorageSoFar": m19, + "claimedStorageSoFar": m20, "cleanUncategorized": MessageLookupByLibrary.simpleMessage("Temiz Genel"), "cleanUncategorizedDescription": MessageLookupByLibrary.simpleMessage( @@ -406,32 +581,44 @@ class MessageLookup extends MessageLookupByLibrary { "Yakalama zamanına göre kulüp"), "clubByFileName": MessageLookupByLibrary.simpleMessage("Dosya adına göre kulüp"), + "clusteringProgress": + MessageLookupByLibrary.simpleMessage("Kümeleme ilerlemesi"), "codeAppliedPageTitle": MessageLookupByLibrary.simpleMessage("Kod kabul edildi"), + "codeChangeLimitReached": MessageLookupByLibrary.simpleMessage( + "Üzgünüz, kod değişikliklerinin sınırına ulaştınız."), "codeCopiedToClipboard": MessageLookupByLibrary.simpleMessage("Kodunuz panoya kopyalandı"), "codeUsedByYou": MessageLookupByLibrary.simpleMessage("Sizin kullandığınız kod"), + "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ı"), - "collaborativeLinkCreatedFor": m20, + "collaborativeLinkCreatedFor": m21, "collaborator": MessageLookupByLibrary.simpleMessage("Düzenleyici"), "collaboratorsCanAddPhotosAndVideosToTheSharedAlbum": MessageLookupByLibrary.simpleMessage( "Düzenleyiciler, paylaşılan albüme fotoğraf ve videolar ekleyebilir."), + "collaboratorsSuccessfullyAdded": m22, "collageLayout": MessageLookupByLibrary.simpleMessage("Düzen"), "collageSaved": MessageLookupByLibrary.simpleMessage( "Kolajınız galeriye kaydedildi"), + "collect": MessageLookupByLibrary.simpleMessage("Topla"), "collectEventPhotos": MessageLookupByLibrary.simpleMessage( "Etkinlik fotoğraflarını topla"), "collectPhotos": MessageLookupByLibrary.simpleMessage("Fotoğrafları topla"), + "collectPhotosDescription": MessageLookupByLibrary.simpleMessage( + "Arkadaşlarınızın orijinal kalitede fotoğraf yükleyebileceği bir bağlantı oluşturun."), "color": MessageLookupByLibrary.simpleMessage("Renk"), + "configuration": MessageLookupByLibrary.simpleMessage("Yapılandırma"), "confirm": MessageLookupByLibrary.simpleMessage("Onayla"), "confirm2FADisable": MessageLookupByLibrary.simpleMessage( "İki adımlı kimlik doğrulamasını devre dışı bırakmak istediğinize emin misiniz?"), "confirmAccountDeletion": MessageLookupByLibrary.simpleMessage("Hesap silme işlemini onayla"), + "confirmAddingTrustedContact": m23, "confirmDeletePrompt": MessageLookupByLibrary.simpleMessage( "Evet, bu hesabı ve verilerini tüm uygulamalardan kalıcı olarak silmek istiyorum."), "confirmPassword": @@ -442,10 +629,12 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Kurtarma anahtarını doğrula"), "confirmYourRecoveryKey": MessageLookupByLibrary.simpleMessage( "Kurtarma anahtarını doğrulayın"), - "contactFamilyAdmin": m23, + "connectToDevice": + MessageLookupByLibrary.simpleMessage("Cihaza bağlanın"), + "contactFamilyAdmin": m24, "contactSupport": MessageLookupByLibrary.simpleMessage("Destek ile iletişim"), - "contactToManageSubscription": m24, + "contactToManageSubscription": m25, "contacts": MessageLookupByLibrary.simpleMessage("Kişiler"), "contents": MessageLookupByLibrary.simpleMessage("İçerikler"), "continueLabel": MessageLookupByLibrary.simpleMessage("Devam edin"), @@ -472,6 +661,8 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Hesap oluşturun"), "createAlbumActionHint": MessageLookupByLibrary.simpleMessage( "Fotoğrafları seçmek için uzun basın ve + düğmesine tıklayarak bir albüm oluşturun"), + "createCollaborativeLink": + MessageLookupByLibrary.simpleMessage("Ortak bağlantı oluşturun"), "createCollage": MessageLookupByLibrary.simpleMessage("Kolaj oluştur"), "createNewAccount": MessageLookupByLibrary.simpleMessage("Yeni bir hesap oluşturun"), @@ -483,13 +674,18 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Bağlantı oluşturuluyor..."), "criticalUpdateAvailable": MessageLookupByLibrary.simpleMessage("Kritik güncelleme mevcut"), + "crop": MessageLookupByLibrary.simpleMessage("Kırp"), "currentUsageIs": MessageLookupByLibrary.simpleMessage("Güncel kullanımınız "), + "currentlyRunning": + MessageLookupByLibrary.simpleMessage("şu anda çalışıyor"), "custom": MessageLookupByLibrary.simpleMessage("Kişisel"), - "customEndpoint": m25, + "customEndpoint": m26, "darkTheme": MessageLookupByLibrary.simpleMessage("Karanlık"), "dayToday": MessageLookupByLibrary.simpleMessage("Bugün"), "dayYesterday": MessageLookupByLibrary.simpleMessage("Dün"), + "declineTrustInvite": + MessageLookupByLibrary.simpleMessage("Daveti Reddet"), "decrypting": MessageLookupByLibrary.simpleMessage("Şifre çözülüyor..."), "decryptingVideo": MessageLookupByLibrary.simpleMessage( @@ -508,6 +704,8 @@ class MessageLookup extends MessageLookupByLibrary { "deleteAlbumsDialogBody": MessageLookupByLibrary.simpleMessage( "Bu, tüm boş albümleri silecektir. Bu, albüm listenizdeki dağınıklığı azaltmak istediğinizde kullanışlıdır."), "deleteAll": MessageLookupByLibrary.simpleMessage("Hepsini Sil"), + "deleteConfirmDialogBody": MessageLookupByLibrary.simpleMessage( + "Kullandığınız Ente uygulamaları varsa bu hesap diğer Ente uygulamalarıyla bağlantılıdır. Tüm Ente uygulamalarına yüklediğiniz veriler ve hesabınız kalıcı olarak silinecektir."), "deleteEmailRequest": MessageLookupByLibrary.simpleMessage( "Lütfen kayıtlı e-posta adresinizden account-deletion@ente.io\'a e-posta gönderiniz."), "deleteEmptyAlbums": @@ -518,11 +716,12 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Her ikisinden de sil"), "deleteFromDevice": MessageLookupByLibrary.simpleMessage("Cihazınızdan silin"), - "deleteItemCount": m26, + "deleteFromEnte": MessageLookupByLibrary.simpleMessage("Ente\'den Sil"), + "deleteItemCount": m27, "deleteLocation": MessageLookupByLibrary.simpleMessage("Konumu sil"), "deletePhotos": MessageLookupByLibrary.simpleMessage("Fotoğrafları sil"), - "deleteProgress": m27, + "deleteProgress": m28, "deleteReason1": MessageLookupByLibrary.simpleMessage( "İhtiyacım olan önemli bir özellik eksik"), "deleteReason2": MessageLookupByLibrary.simpleMessage( @@ -547,6 +746,11 @@ class MessageLookup extends MessageLookupByLibrary { "developerSettingsWarning": MessageLookupByLibrary.simpleMessage( "Geliştirici ayarlarını değiştirmek istediğinizden emin misiniz?"), "deviceCodeHint": MessageLookupByLibrary.simpleMessage("Kodu girin"), + "deviceFilesAutoUploading": MessageLookupByLibrary.simpleMessage( + "Bu cihazın albümüne eklenen dosyalar otomatik olarak ente\'ye yüklenecektir."), + "deviceLock": MessageLookupByLibrary.simpleMessage("Cihaz kilidi"), + "deviceLockExplanation": MessageLookupByLibrary.simpleMessage( + "Ente uygulaması önplanda calıştığında ve bir yedekleme işlemi devam ettiğinde, cihaz ekran kilidini devre dışı bırakın. Bu genellikle gerekli olmasa da, büyük dosyaların yüklenmesi ve büyük kütüphanelerin başlangıçta içe aktarılması sürecini hızlandırabilir."), "deviceNotFound": MessageLookupByLibrary.simpleMessage("Cihaz bulunamadı"), "didYouKnow": MessageLookupByLibrary.simpleMessage("Biliyor musun?"), @@ -556,13 +760,34 @@ class MessageLookup extends MessageLookupByLibrary { "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"), "disableDownloadWarningTitle": MessageLookupByLibrary.simpleMessage("Lütfen dikkate alın"), - "disableLinkMessage": m28, + "disableLinkMessage": m29, "disableTwofactor": MessageLookupByLibrary.simpleMessage( "İki Aşamalı Doğrulamayı Devre Dışı Bırak"), "disablingTwofactorAuthentication": MessageLookupByLibrary.simpleMessage( "İki aşamalı doğrulamayı devre dışı bırak..."), "discord": MessageLookupByLibrary.simpleMessage("Discord"), + "discover": MessageLookupByLibrary.simpleMessage("Keşfet"), + "discover_babies": MessageLookupByLibrary.simpleMessage("Bebekler"), + "discover_celebrations": + MessageLookupByLibrary.simpleMessage("Kutlamalar "), + "discover_food": MessageLookupByLibrary.simpleMessage("Yiyecek"), + "discover_greenery": MessageLookupByLibrary.simpleMessage("Yeşillik"), + "discover_hills": MessageLookupByLibrary.simpleMessage("Tepeler"), + "discover_identity": MessageLookupByLibrary.simpleMessage("Kimlik"), + "discover_memes": MessageLookupByLibrary.simpleMessage("Mimler"), + "discover_notes": MessageLookupByLibrary.simpleMessage("Notlar"), + "discover_pets": + MessageLookupByLibrary.simpleMessage("Evcil Hayvanlar"), + "discover_receipts": MessageLookupByLibrary.simpleMessage("Makbuzlar"), + "discover_screenshots": + MessageLookupByLibrary.simpleMessage("Ekran Görüntüleri"), + "discover_selfies": MessageLookupByLibrary.simpleMessage("Özçekimler"), + "discover_sunset": MessageLookupByLibrary.simpleMessage("Gün batımı"), + "discover_visiting_cards": + MessageLookupByLibrary.simpleMessage("Ziyaret Kartları"), + "discover_wallpapers": + MessageLookupByLibrary.simpleMessage("Duvar Kağıtları"), "dismiss": MessageLookupByLibrary.simpleMessage("Reddet"), "distanceInKMUnit": MessageLookupByLibrary.simpleMessage("km"), "doNotSignOut": MessageLookupByLibrary.simpleMessage("Çıkış yapma"), @@ -571,19 +796,21 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage( "Yaptığınız düzenlemeleri silmek istiyor musunuz?"), "done": MessageLookupByLibrary.simpleMessage("Bitti"), + "dontSave": MessageLookupByLibrary.simpleMessage("Kaydetme"), "doubleYourStorage": MessageLookupByLibrary.simpleMessage( "Depolama alanınızı ikiye katlayın"), "download": MessageLookupByLibrary.simpleMessage("İndir"), "downloadFailed": MessageLookupByLibrary.simpleMessage("İndirme başarısız"), "downloading": MessageLookupByLibrary.simpleMessage("İndiriliyor..."), - "dropSupportEmail": m29, - "duplicateFileCountWithStorageSaved": m30, - "duplicateItemsGroup": m31, + "dropSupportEmail": m30, + "duplicateFileCountWithStorageSaved": m31, + "duplicateItemsGroup": m32, "edit": MessageLookupByLibrary.simpleMessage("Düzenle"), "editLocation": MessageLookupByLibrary.simpleMessage("Konumu düzenle"), "editLocationTagTitle": MessageLookupByLibrary.simpleMessage("Konumu düzenle"), + "editPerson": MessageLookupByLibrary.simpleMessage("Kişiyi Düzenle"), "editsSaved": MessageLookupByLibrary.simpleMessage("Düzenleme kaydedildi"), "editsToLocationWillOnlyBeSeenWithinEnte": @@ -591,19 +818,32 @@ class MessageLookup extends MessageLookupByLibrary { "Konumda yapılan düzenlemeler yalnızca Ente\'de görülecektir"), "eligible": MessageLookupByLibrary.simpleMessage("uygun"), "email": MessageLookupByLibrary.simpleMessage("E-Posta"), - "emailChangedTo": m32, - "emailNoEnteAccount": m33, + "emailAlreadyRegistered": MessageLookupByLibrary.simpleMessage( + "Bu e-posta adresi zaten kayıtlı."), + "emailChangedTo": m33, + "emailDoesNotHaveEnteAccount": m34, + "emailNoEnteAccount": m35, + "emailNotRegistered": MessageLookupByLibrary.simpleMessage( + "Bu e-posta adresi sistemde kayıtlı değil."), "emailVerificationToggle": MessageLookupByLibrary.simpleMessage("E-posta doğrulama"), "emailYourLogs": MessageLookupByLibrary.simpleMessage( "Günlüklerinizi e-postayla gönderin"), + "emergencyContacts": MessageLookupByLibrary.simpleMessage( + "Acil Durum İletişim Bilgileri"), "empty": MessageLookupByLibrary.simpleMessage("Boşalt"), "emptyTrash": MessageLookupByLibrary.simpleMessage("Çöp kutusu boşaltılsın mı?"), + "enable": MessageLookupByLibrary.simpleMessage("Etkinleştir"), + "enableMLIndexingDesc": MessageLookupByLibrary.simpleMessage( + "Ente, yüz tanıma, sihirli arama ve diğer gelişmiş arama özellikleri için cihaz üzerinde makine öğrenimini destekler"), + "enableMachineLearningBanner": MessageLookupByLibrary.simpleMessage( + "Sihirli arama ve yüz tanıma için makine öğrenimini etkinleştirin"), "enableMaps": MessageLookupByLibrary.simpleMessage("Haritaları Etkinleştir"), "enableMapsDesc": MessageLookupByLibrary.simpleMessage( "Bu, fotoğraflarınızı bir dünya haritasında gösterecektir.\n\nBu harita Open Street Map tarafından barındırılmaktadır ve fotoğraflarınızın tam konumları hiçbir zaman paylaşılmaz.\n\nBu özelliği istediğiniz zaman Ayarlar\'dan devre dışı bırakabilirsiniz."), + "enabled": MessageLookupByLibrary.simpleMessage("Etkin"), "encryptingBackup": MessageLookupByLibrary.simpleMessage("Yedekleme şifreleniyor..."), "encryption": MessageLookupByLibrary.simpleMessage("Şifreleme"), @@ -613,8 +853,13 @@ class MessageLookup extends MessageLookupByLibrary { "Fatura başarıyla güncellendi"), "endtoendEncryptedByDefault": MessageLookupByLibrary.simpleMessage( "Varsayılan olarak uçtan uca şifrelenmiş"), + "enteCanEncryptAndPreserveFilesOnlyIfYouGrant": + MessageLookupByLibrary.simpleMessage( + "Ente dosyaları yalnızca erişim izni verdiğiniz takdirde şifreleyebilir ve koruyabilir"), "entePhotosPerm": MessageLookupByLibrary.simpleMessage( "Ente fotoğrafları saklamak için iznine ihtiyaç duyuyor"), + "enteSubscriptionPitch": MessageLookupByLibrary.simpleMessage( + "Ente anılarınızı korur, böylece cihazınızı kaybetseniz bile anılarınıza her zaman ulaşabilirsiniz."), "enteSubscriptionShareWithFamily": MessageLookupByLibrary.simpleMessage( "Aileniz de planınıza eklenebilir."), "enterAlbumName": @@ -622,16 +867,22 @@ class MessageLookup extends MessageLookupByLibrary { "enterCode": MessageLookupByLibrary.simpleMessage("Kodu giriniz"), "enterCodeDescription": MessageLookupByLibrary.simpleMessage( "Arkadaşınız tarafından sağlanan kodu girerek hem sizin hem de arkadaşınızın ücretsiz depolamayı talep etmek için girin"), + "enterDateOfBirth": + MessageLookupByLibrary.simpleMessage("Doğum Günü (isteğe bağlı)"), "enterEmail": MessageLookupByLibrary.simpleMessage("E-postanızı giriniz"), "enterFileName": MessageLookupByLibrary.simpleMessage("Dosya adını girin"), + "enterName": MessageLookupByLibrary.simpleMessage("İsim girin"), "enterNewPasswordToEncrypt": MessageLookupByLibrary.simpleMessage( "Verilerinizi şifrelemek için kullanabileceğimiz yeni bir şifre girin"), "enterPassword": MessageLookupByLibrary.simpleMessage("Şifrenizi girin"), "enterPasswordToEncrypt": MessageLookupByLibrary.simpleMessage( "Verilerinizi şifrelemek için kullanabileceğimiz bir şifre girin"), + "enterPersonName": + MessageLookupByLibrary.simpleMessage("Kişi ismini giriniz"), + "enterPin": MessageLookupByLibrary.simpleMessage("PIN Girin"), "enterReferralCode": MessageLookupByLibrary.simpleMessage("Davet kodunuzu girin"), "enterThe6digitCodeFromnyourAuthenticatorApp": @@ -656,65 +907,94 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Günlüğü dışa aktar"), "exportYourData": MessageLookupByLibrary.simpleMessage("Veriyi dışarı aktar"), + "extraPhotosFound": + MessageLookupByLibrary.simpleMessage("Ekstra fotoğraflar bulundu"), + "extraPhotosFoundFor": m36, + "faceNotClusteredYet": MessageLookupByLibrary.simpleMessage( + "Yüz henüz kümelenmedi, lütfen daha sonra tekrar gelin"), + "faceRecognition": MessageLookupByLibrary.simpleMessage("Yüz Tanıma"), "faces": MessageLookupByLibrary.simpleMessage("Yüzler"), + "failed": MessageLookupByLibrary.simpleMessage("Başarısız oldu"), "failedToApplyCode": MessageLookupByLibrary.simpleMessage("Uygulanırken hata oluştu"), "failedToCancel": MessageLookupByLibrary.simpleMessage( "İptal edilirken sorun oluştu"), "failedToDownloadVideo": MessageLookupByLibrary.simpleMessage("Video indirilemedi"), + "failedToFetchActiveSessions": MessageLookupByLibrary.simpleMessage( + "Etkin oturumlar getirilemedi"), "failedToFetchOriginalForEdit": MessageLookupByLibrary.simpleMessage( "Düzenleme için orijinal getirilemedi"), "failedToFetchReferralDetails": MessageLookupByLibrary.simpleMessage( "Davet ayrıntıları çekilemedi. Iütfen daha sonra deneyin."), "failedToLoadAlbums": MessageLookupByLibrary.simpleMessage( "Albüm yüklenirken hata oluştu"), + "failedToPlayVideo": + MessageLookupByLibrary.simpleMessage("Video oynatılamadı"), + "failedToRefreshStripeSubscription": + MessageLookupByLibrary.simpleMessage("Abonelik yenilenemedi"), "failedToRenew": MessageLookupByLibrary.simpleMessage( "Abonelik yenilenirken hata oluştu"), "failedToVerifyPaymentStatus": MessageLookupByLibrary.simpleMessage("Ödeme durumu doğrulanamadı"), + "familyPlanOverview": MessageLookupByLibrary.simpleMessage( + "Ekstra ödeme yapmadan mevcut planınıza 5 aile üyesi ekleyin.\n\nHer üyenin kendine ait özel alanı vardır ve paylaşılmadıkça birbirlerinin dosyalarını göremezler.\n\nAile planları ücretli ente aboneliğine sahip müşteriler tarafından kullanılabilir.\n\nBaşlamak için şimdi abone olun!"), "familyPlanPortalTitle": MessageLookupByLibrary.simpleMessage("Aile"), "familyPlans": MessageLookupByLibrary.simpleMessage("Aile Planı"), "faq": MessageLookupByLibrary.simpleMessage("Sıkça sorulan sorular"), "faqs": MessageLookupByLibrary.simpleMessage("Sık sorulanlar"), "favorite": MessageLookupByLibrary.simpleMessage("Favori"), "feedback": MessageLookupByLibrary.simpleMessage("Geri Bildirim"), + "file": MessageLookupByLibrary.simpleMessage("Dosya"), "fileFailedToSaveToGallery": MessageLookupByLibrary.simpleMessage( "Dosya galeriye kaydedilemedi"), "fileInfoAddDescHint": MessageLookupByLibrary.simpleMessage("Bir açıklama ekle..."), + "fileNotUploadedYet": + MessageLookupByLibrary.simpleMessage("Dosya henüz yüklenmedi"), "fileSavedToGallery": MessageLookupByLibrary.simpleMessage("Video galeriye kaydedildi"), "fileTypes": MessageLookupByLibrary.simpleMessage("Dosya türü"), "fileTypesAndNames": MessageLookupByLibrary.simpleMessage("Dosya türleri ve adları"), - "filesBackedUpFromDevice": m35, - "filesBackedUpInAlbum": m36, + "filesBackedUpFromDevice": m37, + "filesBackedUpInAlbum": m38, "filesDeleted": MessageLookupByLibrary.simpleMessage("Dosyalar silinmiş"), + "filesSavedToGallery": MessageLookupByLibrary.simpleMessage( + "Dosyalar galeriye kaydedildi"), + "findPeopleByName": MessageLookupByLibrary.simpleMessage( + "Kişileri isimlere göre çabucak bulun"), + "findThemQuickly": + MessageLookupByLibrary.simpleMessage("Onları çabucak bulun"), "flip": MessageLookupByLibrary.simpleMessage("Çevir"), "forYourMemories": MessageLookupByLibrary.simpleMessage("anıların için"), "forgotPassword": MessageLookupByLibrary.simpleMessage("Şifremi unuttum"), + "foundFaces": MessageLookupByLibrary.simpleMessage("Yüzler bulundu"), "freeStorageClaimed": MessageLookupByLibrary.simpleMessage("Alınan bedava alan"), "freeStorageOnReferralSuccess": m4, "freeStorageUsable": MessageLookupByLibrary.simpleMessage("Kullanılabilir bedava alan"), "freeTrial": MessageLookupByLibrary.simpleMessage("Ücretsiz deneme"), - "freeTrialValidTill": m37, - "freeUpAmount": m39, + "freeTrialValidTill": m39, + "freeUpAccessPostDelete": m40, + "freeUpAmount": m41, "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": m40, + "freeUpSpaceSaving": m42, + "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": m41, + "genericProgress": m43, "goToSettings": MessageLookupByLibrary.simpleMessage("Ayarlara git"), "googlePlayId": MessageLookupByLibrary.simpleMessage("Google play kimliği"), @@ -724,6 +1004,9 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("İzinleri değiştir"), "groupNearbyPhotos": MessageLookupByLibrary.simpleMessage( "Yakındaki fotoğrafları gruplandır"), + "guestView": MessageLookupByLibrary.simpleMessage("Misafir Görünümü"), + "guestViewEnablePreSteps": MessageLookupByLibrary.simpleMessage( + "Misafir görünümünü etkinleştirmek için lütfen sistem ayarlarınızda cihaz şifresi veya ekran kilidi ayarlayın."), "hearUsExplanation": MessageLookupByLibrary.simpleMessage( "Biz uygulama kurulumlarını takip etmiyoruz. Bizi nereden duyduğunuzdan bahsetmeniz bize çok yardımcı olacak!"), "hearUsWhereTitle": MessageLookupByLibrary.simpleMessage( @@ -731,6 +1014,13 @@ class MessageLookup extends MessageLookupByLibrary { "help": MessageLookupByLibrary.simpleMessage("Yardım"), "hidden": MessageLookupByLibrary.simpleMessage("Gizle"), "hide": MessageLookupByLibrary.simpleMessage("Gizle"), + "hideContent": MessageLookupByLibrary.simpleMessage("İçeriği gizle"), + "hideContentDescriptionAndroid": MessageLookupByLibrary.simpleMessage( + "Uygulama değiştiricide bulunan uygulama içeriğini gizler ve ekran görüntülerini devre dışı bırakır"), + "hideContentDescriptionIos": MessageLookupByLibrary.simpleMessage( + "Uygulama değiştiricideki uygulama içeriğini gizler"), + "hideSharedItemsFromHomeGallery": MessageLookupByLibrary.simpleMessage( + "Paylaşılan öğeleri ana galeriden gizle"), "hiding": MessageLookupByLibrary.simpleMessage("Gizleniyor..."), "hostedAtOsmFrance": MessageLookupByLibrary.simpleMessage("OSM Fransa\'da ağırlandı"), @@ -743,6 +1033,12 @@ class MessageLookup extends MessageLookupByLibrary { "Biyometrik kimlik doğrulama devre dışı. Etkinleştirmek için lütfen ekranınızı kilitleyin ve kilidini açın."), "iOSOkButton": MessageLookupByLibrary.simpleMessage("Tamam"), "ignoreUpdate": MessageLookupByLibrary.simpleMessage("Yoksay"), + "ignored": MessageLookupByLibrary.simpleMessage("yoksayıldı"), + "ignoredFolderUploadReason": MessageLookupByLibrary.simpleMessage( + "Bu albümdeki bazı dosyalar daha önce ente\'den silindiğinden yükleme işleminde göz ardı edildi."), + "imageNotAnalyzed": + MessageLookupByLibrary.simpleMessage("Görüntü analiz edilmedi"), + "immediately": MessageLookupByLibrary.simpleMessage("Hemen"), "importing": MessageLookupByLibrary.simpleMessage("İçeri aktarılıyor...."), "incorrectCode": MessageLookupByLibrary.simpleMessage("Yanlış kod"), @@ -756,6 +1052,10 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Yanlış kurtarma kodu"), "indexedItems": MessageLookupByLibrary.simpleMessage("Yeni öğeleri indeksle"), + "indexingIsPaused": MessageLookupByLibrary.simpleMessage( + "İndeksleme duraklatılmıştır. Cihaz hazır olduğunda otomatik olarak devam edecektir."), + "ineligible": MessageLookupByLibrary.simpleMessage("Uygun Değil"), + "info": MessageLookupByLibrary.simpleMessage("Bilgi"), "insecureDevice": MessageLookupByLibrary.simpleMessage("Güvenilir olmayan cihaz"), "installManually": @@ -770,17 +1070,29 @@ class MessageLookup extends MessageLookupByLibrary { "invalidRecoveryKey": MessageLookupByLibrary.simpleMessage( "Girdiğiniz kurtarma anahtarı geçerli değil. Lütfen anahtarın 24 kelime içerdiğinden ve her bir kelimenin doğru şekilde yazıldığından emin olun.\n\nEğer eski bir kurtarma kodu girdiyseniz, o zaman kodun 64 karakter uzunluğunda olduğunu kontrol edin."), "invite": MessageLookupByLibrary.simpleMessage("Davet et"), + "inviteToEnte": + MessageLookupByLibrary.simpleMessage("Ente\'ye davet edin"), "inviteYourFriends": MessageLookupByLibrary.simpleMessage("Arkadaşlarını davet et"), + "inviteYourFriendsToEnte": MessageLookupByLibrary.simpleMessage( + "Katılmaları için arkadaşlarınızı davet edin"), "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": m44, "itemsShowTheNumberOfDaysRemainingBeforePermanentDeletion": MessageLookupByLibrary.simpleMessage( "Öğeler, kalıcı olarak silinmeden önce kalan gün sayısını gösterir"), "itemsWillBeRemovedFromAlbum": MessageLookupByLibrary.simpleMessage( "Seçilen öğeler bu albümden kaldırılacak"), + "join": MessageLookupByLibrary.simpleMessage("Katıl"), + "joinAlbum": MessageLookupByLibrary.simpleMessage("Albüme Katılın"), + "joinAlbumConfirmationDialogBody": MessageLookupByLibrary.simpleMessage( + "Bir albüme katılmak, e-postanızın katılımcılar tarafından görülebilmesini sağlayacaktır."), + "joinAlbumSubtext": MessageLookupByLibrary.simpleMessage( + "fotoğraflarınızı görüntülemek ve eklemek için"), + "joinAlbumSubtextViewer": MessageLookupByLibrary.simpleMessage( + "bunu paylaşılan albümlere eklemek için"), "joinDiscord": MessageLookupByLibrary.simpleMessage("Discord\'a Katıl"), "keepPhotos": MessageLookupByLibrary.simpleMessage("Fotoğrafları sakla"), @@ -797,19 +1109,37 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Aile planından ayrıl"), "leaveSharedAlbum": MessageLookupByLibrary.simpleMessage( "Paylaşılan albüm silinsin mi?"), + "left": MessageLookupByLibrary.simpleMessage("Sol"), + "legacy": MessageLookupByLibrary.simpleMessage("Geleneksel"), + "legacyAccounts": + MessageLookupByLibrary.simpleMessage("Geleneksel hesaplar"), + "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( + "Güvenilir kişiler hesap kurtarma işlemini başlatabilir ve 30 gün içinde engellenmezse şifrenizi sıfırlayabilir ve hesabınıza erişebilir."), "light": MessageLookupByLibrary.simpleMessage("Aydınlık"), "lightTheme": MessageLookupByLibrary.simpleMessage("Aydınlık"), + "link": MessageLookupByLibrary.simpleMessage("Bağlantı"), "linkCopiedToClipboard": MessageLookupByLibrary.simpleMessage("Link panoya kopyalandı"), "linkDeviceLimit": MessageLookupByLibrary.simpleMessage("Cihaz limiti"), + "linkEmail": MessageLookupByLibrary.simpleMessage("E-posta bağlantısı"), + "linkEmailToContactBannerCaption": + MessageLookupByLibrary.simpleMessage("d"), "linkEnabled": MessageLookupByLibrary.simpleMessage("Geçerli"), "linkExpired": MessageLookupByLibrary.simpleMessage("Süresi dolmuş"), - "linkExpiresOn": m44, + "linkExpiresOn": m46, "linkExpiry": MessageLookupByLibrary.simpleMessage("Linkin geçerliliği"), "linkHasExpired": MessageLookupByLibrary.simpleMessage("Bağlantının süresi dolmuş"), "linkNeverExpires": MessageLookupByLibrary.simpleMessage("Asla"), + "linkPerson": MessageLookupByLibrary.simpleMessage("Bağlantı kişisi"), + "linkPersonCaption": MessageLookupByLibrary.simpleMessage( + "daha iyi paylaşım deneyimi için"), + "linkPersonToEmail": m47, + "linkPersonToEmailConfirmation": m48, "livePhotos": MessageLookupByLibrary.simpleMessage("Canlı Fotoğraf"), "loadMessage1": MessageLookupByLibrary.simpleMessage( "Aboneliğinizi ailenizle paylaşabilirsiniz"), @@ -837,7 +1167,13 @@ class MessageLookup extends MessageLookupByLibrary { "Fotoğraflarınız yükleniyor..."), "loadingModel": MessageLookupByLibrary.simpleMessage("Modeller indiriliyor..."), + "loadingYourPhotos": MessageLookupByLibrary.simpleMessage( + "Fotoğraflarınız yükleniyor..."), "localGallery": MessageLookupByLibrary.simpleMessage("Yerel galeri"), + "localIndexing": + MessageLookupByLibrary.simpleMessage("Yerel indeksleme"), + "localSyncErrorMessage": MessageLookupByLibrary.simpleMessage( + "Yerel fotoğraf senkronizasyonu beklenenden daha uzun sürdüğü için bir şeyler ters gitmiş gibi görünüyor. Lütfen destek ekibimize ulaşın"), "location": MessageLookupByLibrary.simpleMessage("Konum"), "locationName": MessageLookupByLibrary.simpleMessage("Konum Adı"), "locationTagFeatureDescription": MessageLookupByLibrary.simpleMessage( @@ -848,8 +1184,14 @@ class MessageLookup extends MessageLookupByLibrary { "logInLabel": MessageLookupByLibrary.simpleMessage("Giriş yap"), "loggingOut": MessageLookupByLibrary.simpleMessage("Çıkış yapılıyor..."), + "loginSessionExpired": + MessageLookupByLibrary.simpleMessage("Oturum süresi doldu"), + "loginSessionExpiredDetails": MessageLookupByLibrary.simpleMessage( + "Oturum süreniz doldu. Tekrar giriş yapın."), "loginTerms": MessageLookupByLibrary.simpleMessage( "\"Giriş yap\" düğmesine tıklayarak, Hizmet Şartları\'nı ve Gizlilik Politikası\'nı kabul ediyorum"), + "loginWithTOTP": + MessageLookupByLibrary.simpleMessage("TOTP ile giriş yap"), "logout": MessageLookupByLibrary.simpleMessage("Çıkış yap"), "logsDialogBody": MessageLookupByLibrary.simpleMessage( "Bu, sorununuzu gidermemize yardımcı olmak için günlükleri gönderecektir. Belirli dosyalarla ilgili sorunların izlenmesine yardımcı olmak için dosya adlarının ekleneceğini lütfen unutmayın."), @@ -859,23 +1201,52 @@ class MessageLookup extends MessageLookupByLibrary { "longpressOnAnItemToViewInFullscreen": MessageLookupByLibrary.simpleMessage( "Tam ekranda görüntülemek için bir öğeye uzun basın"), + "loopVideoOff": + MessageLookupByLibrary.simpleMessage("Video Döngüsü Kapalı"), + "loopVideoOn": + MessageLookupByLibrary.simpleMessage("Video Döngüsü Açık"), "lostDevice": MessageLookupByLibrary.simpleMessage("Cihazı kayıp mı ettiniz?"), "machineLearning": MessageLookupByLibrary.simpleMessage("Makine öğrenimi"), "magicSearch": MessageLookupByLibrary.simpleMessage("Sihirli arama"), + "magicSearchHint": MessageLookupByLibrary.simpleMessage( + "Sihirli arama, fotoğrafları içeriklerine göre aramanıza olanak tanır, örneğin \'çiçek\', \'kırmızı araba\', \'kimlik belgeleri\'"), "manage": MessageLookupByLibrary.simpleMessage("Yönet"), + "manageDeviceStorage": + MessageLookupByLibrary.simpleMessage("Cihaz önbelliğini yönet"), + "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"), "manageParticipants": MessageLookupByLibrary.simpleMessage("Yönet"), "manageSubscription": MessageLookupByLibrary.simpleMessage("Abonelikleri yönet"), + "manualPairDesc": MessageLookupByLibrary.simpleMessage( + "PIN ile eşleştirme, albümünüzü görüntülemek istediğiniz herhangi bir ekranla çalışır."), "map": MessageLookupByLibrary.simpleMessage("Harita"), "maps": MessageLookupByLibrary.simpleMessage("Haritalar"), "mastodon": MessageLookupByLibrary.simpleMessage("Mastodon"), "matrix": MessageLookupByLibrary.simpleMessage("Matrix"), + "me": MessageLookupByLibrary.simpleMessage("Ben"), "memoryCount": m5, "merchandise": MessageLookupByLibrary.simpleMessage("Ürünler"), + "mergeWithExisting": + MessageLookupByLibrary.simpleMessage("Var olan ile birleştir."), + "mergedPhotos": + MessageLookupByLibrary.simpleMessage("Birleştirilmiş fotoğraflar"), + "mlConsent": MessageLookupByLibrary.simpleMessage( + "Makine öğrenimini etkinleştir"), + "mlConsentConfirmation": MessageLookupByLibrary.simpleMessage( + "Anladım, ve makine öğrenimini etkinleştirmek istiyorum"), + "mlConsentDescription": MessageLookupByLibrary.simpleMessage( + "Makine öğrenimini etkinleştirirseniz, Ente sizinle paylaşılanlar da dahil olmak üzere dosyalardan yüz geometrisi gibi bilgileri çıkarır.\n\nBu, cihazınızda gerçekleşecek ve oluşturulan tüm biyometrik bilgiler uçtan uca şifrelenecektir."), + "mlConsentPrivacy": MessageLookupByLibrary.simpleMessage( + "Gizlilik politikamızdaki bu özellik hakkında daha fazla ayrıntı için lütfen buraya tıklayın"), + "mlConsentTitle": MessageLookupByLibrary.simpleMessage( + "Makine öğrenimi etkinleştirilsin mi?"), + "mlIndexingDescription": MessageLookupByLibrary.simpleMessage( + "Tüm öğeler dizine eklenene kadar makine öğreniminin daha yüksek bant genişliği ve pil kullanımı ile sonuçlanacağını lütfen unutmayın. Daha hızlı indeksleme için masaüstü uygulamasını kullanmayı düşünün, tüm sonuçlar otomatik olarak senkronize edilecektir."), "mobileWebDesktop": MessageLookupByLibrary.simpleMessage("Mobil, Web, Masaüstü"), "moderateStrength": MessageLookupByLibrary.simpleMessage("Ilımlı"), @@ -883,33 +1254,47 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage( "Sorgunuzu değiştirin veya aramayı deneyin"), "moments": MessageLookupByLibrary.simpleMessage("Anlar"), + "month": MessageLookupByLibrary.simpleMessage("ay"), "monthly": MessageLookupByLibrary.simpleMessage("Aylık"), - "moveItem": m45, + "moreDetails": MessageLookupByLibrary.simpleMessage("Daha fazla detay"), + "mostRecent": MessageLookupByLibrary.simpleMessage("En son"), + "mostRelevant": MessageLookupByLibrary.simpleMessage("En alakalı"), + "moveItem": m49, "moveToAlbum": MessageLookupByLibrary.simpleMessage("Albüme taşı"), "moveToHiddenAlbum": MessageLookupByLibrary.simpleMessage("Gizli albüme ekle"), - "movedSuccessfullyTo": m46, + "movedSuccessfullyTo": m50, "movedToTrash": MessageLookupByLibrary.simpleMessage("Cöp kutusuna taşı"), "movingFilesToAlbum": MessageLookupByLibrary.simpleMessage( "Dosyalar albüme taşınıyor..."), "name": MessageLookupByLibrary.simpleMessage("İsim"), + "nameTheAlbum": MessageLookupByLibrary.simpleMessage("Albüm İsmi"), "networkConnectionRefusedErr": MessageLookupByLibrary.simpleMessage( "Ente\'ye bağlanılamıyor. Lütfen bir süre sonra tekrar deneyin. Hata devam ederse lütfen desteğe başvurun."), "networkHostLookUpErr": MessageLookupByLibrary.simpleMessage( "Ente\'ye bağlanılamıyor. Lütfen ağ ayarlarınızı kontrol edin ve hata devam ederse destek ekibiyle iletişime geçin."), "never": MessageLookupByLibrary.simpleMessage("Asla"), "newAlbum": MessageLookupByLibrary.simpleMessage("Yeni albüm"), + "newLocation": MessageLookupByLibrary.simpleMessage("Yeni konum"), + "newPerson": MessageLookupByLibrary.simpleMessage("Yeni Kişi"), + "newToEnte": MessageLookupByLibrary.simpleMessage("Ente\'de yeniyim"), "newest": MessageLookupByLibrary.simpleMessage("En yeni"), + "next": MessageLookupByLibrary.simpleMessage("Sonraki"), "no": MessageLookupByLibrary.simpleMessage("Hayır"), "noAlbumsSharedByYouYet": MessageLookupByLibrary.simpleMessage( "Henüz paylaştığınız albüm yok"), + "noDeviceFound": + MessageLookupByLibrary.simpleMessage("Aygıt bulunamadı"), "noDeviceLimit": MessageLookupByLibrary.simpleMessage("Yok"), "noDeviceThatCanBeDeleted": MessageLookupByLibrary.simpleMessage( "Bu cihazda silinebilecek hiçbir dosyanız yok"), "noDuplicates": MessageLookupByLibrary.simpleMessage("Yinelenenleri kaldır"), + "noEnteAccountExclamation": + MessageLookupByLibrary.simpleMessage("Ente hesabı yok!"), "noExifData": MessageLookupByLibrary.simpleMessage("EXIF verisi yok"), + "noFacesFound": MessageLookupByLibrary.simpleMessage("Yüz bulunamadı"), "noHiddenPhotosOrVideos": MessageLookupByLibrary.simpleMessage( "Gizli fotoğraf veya video yok"), "noImagesWithLocation": @@ -921,6 +1306,8 @@ class MessageLookup extends MessageLookupByLibrary { "Şu anda hiçbir fotoğraf yedeklenmiyor"), "noPhotosFoundHere": MessageLookupByLibrary.simpleMessage("Burada fotoğraf bulunamadı"), + "noQuickLinksSelected": + MessageLookupByLibrary.simpleMessage("Hızlı bağlantılar seçilmedi"), "noRecoveryKey": MessageLookupByLibrary.simpleMessage("Kurtarma kodunuz yok mu?"), "noRecoveryKeyNoDecryption": MessageLookupByLibrary.simpleMessage( @@ -928,6 +1315,10 @@ class MessageLookup extends MessageLookupByLibrary { "noResults": MessageLookupByLibrary.simpleMessage("Sonuç bulunamadı"), "noResultsFound": MessageLookupByLibrary.simpleMessage("Hiçbir sonuç bulunamadı"), + "noSuggestionsForPerson": m51, + "noSystemLockFound": + MessageLookupByLibrary.simpleMessage("Sistem kilidi bulunamadı"), + "notPersonLabel": m52, "nothingSharedWithYouYet": MessageLookupByLibrary.simpleMessage( "Henüz sizinle paylaşılan bir şey yok"), "nothingToSeeHere": MessageLookupByLibrary.simpleMessage( @@ -937,22 +1328,38 @@ class MessageLookup extends MessageLookupByLibrary { "onDevice": MessageLookupByLibrary.simpleMessage("Bu cihaz"), "onEnte": MessageLookupByLibrary.simpleMessage( "ente üzerinde"), + "onlyFamilyAdminCanChangeCode": m53, + "onlyThem": MessageLookupByLibrary.simpleMessage("Sadece onlar"), "oops": MessageLookupByLibrary.simpleMessage("Hay aksi"), "oopsCouldNotSaveEdits": MessageLookupByLibrary.simpleMessage( "Hata! Düzenlemeler kaydedilemedi"), "oopsSomethingWentWrong": MessageLookupByLibrary.simpleMessage( "Hoop, Birşeyler yanlış gitti"), + "openAlbumInBrowser": + MessageLookupByLibrary.simpleMessage("Albümü tarayıcıda aç"), + "openAlbumInBrowserTitle": MessageLookupByLibrary.simpleMessage( + "Bu albüme fotoğraf eklemek için lütfen web uygulamasını kullanın"), + "openFile": MessageLookupByLibrary.simpleMessage("Dosyayı aç"), "openSettings": MessageLookupByLibrary.simpleMessage("Ayarları Açın"), "openTheItem": MessageLookupByLibrary.simpleMessage("• Öğeyi açın"), "openstreetmapContributors": MessageLookupByLibrary.simpleMessage( "© OpenStreetMap katkıda bululanlar"), "optionalAsShortAsYouLike": MessageLookupByLibrary.simpleMessage( "İsteğe bağlı, istediğiniz kadar kısa..."), + "orMergeWithExistingPerson": MessageLookupByLibrary.simpleMessage( + "Ya da mevcut olan ile birleştirin"), "orPickAnExistingOne": MessageLookupByLibrary.simpleMessage("Veya mevcut birini seçiniz"), - "orPickFromYourContacts": - MessageLookupByLibrary.simpleMessage("or pick from your contacts"), + "orPickFromYourContacts": MessageLookupByLibrary.simpleMessage( + "veya kişilerinizden birini seçin"), "pair": MessageLookupByLibrary.simpleMessage("Eşleştir"), + "pairWithPin": + MessageLookupByLibrary.simpleMessage("PIN ile eşleştirin"), + "pairingComplete": + MessageLookupByLibrary.simpleMessage("Eşleştirme tamamlandı"), + "panorama": MessageLookupByLibrary.simpleMessage("Panorama"), + "passKeyPendingVerification": + MessageLookupByLibrary.simpleMessage("Doğrulama hala bekliyor"), "passkey": MessageLookupByLibrary.simpleMessage("Parola Anahtarı"), "passkeyAuthTitle": MessageLookupByLibrary.simpleMessage("Geçiş anahtarı doğrulaması"), @@ -961,6 +1368,8 @@ class MessageLookup extends MessageLookupByLibrary { "Şifreniz başarılı bir şekilde değiştirildi"), "passwordLock": MessageLookupByLibrary.simpleMessage("Sifre kilidi"), "passwordStrength": m0, + "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( "Şifrelerinizi saklamıyoruz, bu yüzden unutursanız, verilerinizi deşifre edemeyiz"), "paymentDetails": @@ -969,10 +1378,11 @@ 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": m50, + "paymentFailedTalkToProvider": m54, "pendingItems": MessageLookupByLibrary.simpleMessage("Bekleyen Öğeler"), "pendingSync": MessageLookupByLibrary.simpleMessage("Senkronizasyon bekleniyor"), + "people": MessageLookupByLibrary.simpleMessage("Kişiler"), "peopleUsingYourCode": MessageLookupByLibrary.simpleMessage("Kodunuzu kullananlar"), "permDeleteWarning": MessageLookupByLibrary.simpleMessage( @@ -981,6 +1391,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Kalıcı olarak sil"), "permanentlyDeleteFromDevice": MessageLookupByLibrary.simpleMessage( "Cihazdan kalıcı olarak silinsin mi?"), + "personName": MessageLookupByLibrary.simpleMessage("Kişi Adı"), "photoDescriptions": MessageLookupByLibrary.simpleMessage("Fotoğraf Açıklaması"), "photoGridSize": @@ -990,10 +1401,15 @@ class MessageLookup extends MessageLookupByLibrary { "photosAddedByYouWillBeRemovedFromTheAlbum": MessageLookupByLibrary.simpleMessage( "Eklediğiniz fotoğraflar albümden kaldırılacak"), + "photosCount": m55, "pickCenterPoint": MessageLookupByLibrary.simpleMessage("Merkez noktasını seçin"), "pinAlbum": MessageLookupByLibrary.simpleMessage("Albümü sabitle"), + "pinLock": MessageLookupByLibrary.simpleMessage("Pin kilidi"), "playOnTv": MessageLookupByLibrary.simpleMessage("Albümü TV\'de oynat"), + "playOriginal": MessageLookupByLibrary.simpleMessage("Orijinali oynat"), + "playStoreFreeTrialValidTill": m56, + "playStream": MessageLookupByLibrary.simpleMessage("Akışı oynat"), "playstoreSubscription": MessageLookupByLibrary.simpleMessage("PlayStore aboneliği"), "pleaseCheckYourInternetConnectionAndTryAgain": @@ -1005,12 +1421,14 @@ class MessageLookup extends MessageLookupByLibrary { "pleaseContactSupportIfTheProblemPersists": MessageLookupByLibrary.simpleMessage( "Bu hata devam ederse lütfen desteğe başvurun"), - "pleaseEmailUsAt": m53, + "pleaseEmailUsAt": m57, "pleaseGrantPermissions": MessageLookupByLibrary.simpleMessage("Lütfen izin ver"), "pleaseLoginAgain": MessageLookupByLibrary.simpleMessage("Lütfen tekrar giriş yapın"), - "pleaseSendTheLogsTo": m54, + "pleaseSelectQuickLinksToRemove": MessageLookupByLibrary.simpleMessage( + "Lütfen kaldırmak için hızlı bağlantıları seçin"), + "pleaseSendTheLogsTo": m58, "pleaseTryAgain": MessageLookupByLibrary.simpleMessage("Lütfen tekrar deneyiniz"), "pleaseVerifyTheCodeYouHaveEntered": @@ -1037,20 +1455,37 @@ class MessageLookup extends MessageLookupByLibrary { "privateBackups": MessageLookupByLibrary.simpleMessage("Özel yedeklemeler"), "privateSharing": MessageLookupByLibrary.simpleMessage("Özel paylaşım"), + "proceed": MessageLookupByLibrary.simpleMessage("Devam edin"), + "processed": MessageLookupByLibrary.simpleMessage("İşlenen"), + "processing": MessageLookupByLibrary.simpleMessage("İşleniyor"), + "processingImport": m59, + "processingVideos": + MessageLookupByLibrary.simpleMessage("Videolar işleniyor"), "publicLinkCreated": MessageLookupByLibrary.simpleMessage( "Herkese açık link oluşturuldu"), "publicLinkEnabled": MessageLookupByLibrary.simpleMessage( "Herkese açık bağlantı aktive edildi"), + "queued": MessageLookupByLibrary.simpleMessage("Kuyrukta"), "quickLinks": MessageLookupByLibrary.simpleMessage("Hızlı Erişim"), "radius": MessageLookupByLibrary.simpleMessage("Yarıçap"), "raiseTicket": MessageLookupByLibrary.simpleMessage("Bileti artır"), "rateTheApp": MessageLookupByLibrary.simpleMessage("Uygulamaya puan verin"), "rateUs": MessageLookupByLibrary.simpleMessage("Bizi değerlendirin"), - "rateUsOnStore": m56, + "rateUsOnStore": m60, + "reassignMe": + MessageLookupByLibrary.simpleMessage("\"Ben \"i yeniden atayın"), + "reassignedToName": m61, + "reassigningLoading": + MessageLookupByLibrary.simpleMessage("Yeniden atama..."), "recover": MessageLookupByLibrary.simpleMessage("Kurtarma"), "recoverAccount": MessageLookupByLibrary.simpleMessage("Hesabı kurtar"), "recoverButton": MessageLookupByLibrary.simpleMessage("Kurtar"), + "recoveryAccount": + MessageLookupByLibrary.simpleMessage("Hesabı kurtar"), + "recoveryInitiated": + MessageLookupByLibrary.simpleMessage("Kurtarma başlatıldı"), + "recoveryInitiatedDesc": m62, "recoveryKey": MessageLookupByLibrary.simpleMessage("Kurtarma anahtarı"), "recoveryKeyCopiedToClipboard": MessageLookupByLibrary.simpleMessage( @@ -1063,23 +1498,35 @@ class MessageLookup extends MessageLookupByLibrary { "Harika! Kurtarma anahtarınız geçerlidir. Doğrulama için teşekkür ederim.\n\nLütfen kurtarma anahtarınızı güvenli bir şekilde yedeklediğinizden emin olun."), "recoveryKeyVerified": 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": m63, "recoverySuccessful": MessageLookupByLibrary.simpleMessage("Kurtarma başarılı!"), + "recoveryWarning": MessageLookupByLibrary.simpleMessage( + "Güvenilir bir kişi hesabınıza erişmeye çalışıyor"), + "recoveryWarningBody": m64, "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( "Sifrenizi tekrardan oluşturun"), "reddit": MessageLookupByLibrary.simpleMessage("Reddit"), + "reenterPassword": + MessageLookupByLibrary.simpleMessage("Şifrenizi tekrar girin"), + "reenterPin": + MessageLookupByLibrary.simpleMessage("PIN\'inizi tekrar girin"), "referFriendsAnd2xYourPlan": MessageLookupByLibrary.simpleMessage( "Arkadaşlarınıza önerin ve planınızı 2 katına çıkarın"), "referralStep1": MessageLookupByLibrary.simpleMessage( "1. Bu kodu arkadaşlarınıza verin"), "referralStep2": MessageLookupByLibrary.simpleMessage( "2. Ücretli bir plan için kaydolsunlar"), - "referralStep3": m60, + "referralStep3": m65, "referrals": MessageLookupByLibrary.simpleMessage("Referanslar"), "referralsAreCurrentlyPaused": MessageLookupByLibrary.simpleMessage( "Davetler şu anda durmuş durumda"), + "rejectRecovery": + MessageLookupByLibrary.simpleMessage("Kurtarmayı reddet"), "remindToEmptyDeviceTrash": MessageLookupByLibrary.simpleMessage( "Ayrıca boşalan alanı talep etmek için \"Ayarlar\" -> \"Depolama\" bölümünden \"Son Silinenler \"i boşaltın"), "remindToEmptyEnteTrash": MessageLookupByLibrary.simpleMessage( @@ -1092,20 +1539,32 @@ class MessageLookup extends MessageLookupByLibrary { "remove": MessageLookupByLibrary.simpleMessage("Kaldır"), "removeDuplicates": MessageLookupByLibrary.simpleMessage("Yinelenenleri kaldır"), + "removeDuplicatesDesc": MessageLookupByLibrary.simpleMessage( + "Tam olarak yinelenen dosyaları gözden geçirin ve kaldırın."), "removeFromAlbum": MessageLookupByLibrary.simpleMessage("Albümden çıkar"), "removeFromAlbumTitle": MessageLookupByLibrary.simpleMessage("Albümden çıkarılsın mı?"), + "removeFromFavorite": + MessageLookupByLibrary.simpleMessage("Favorilerden Kaldır"), + "removeInvite": + MessageLookupByLibrary.simpleMessage("Davetiyeyi kaldır"), "removeLink": MessageLookupByLibrary.simpleMessage("Linki kaldır"), "removeParticipant": MessageLookupByLibrary.simpleMessage("Katılımcıyı kaldır"), - "removeParticipantBody": m61, + "removeParticipantBody": m66, + "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"), "removeShareItemsWarning": MessageLookupByLibrary.simpleMessage( "Kaldırdığınız öğelerden bazıları başkaları tarafından eklenmiştir ve bunlara erişiminizi kaybedeceksiniz"), "removeWithQuestionMark": MessageLookupByLibrary.simpleMessage("Kaldır?"), + "removeYourselfAsTrustedContact": MessageLookupByLibrary.simpleMessage( + "Kendinizi güvenilir kişi olarak kaldırın"), "removingFromFavorites": MessageLookupByLibrary.simpleMessage("Favorilerimden kaldır..."), "rename": MessageLookupByLibrary.simpleMessage("Yeniden adlandır"), @@ -1115,7 +1574,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Dosyayı yeniden adlandır"), "renewSubscription": MessageLookupByLibrary.simpleMessage("Abonelik yenileme"), - "renewsOn": m62, + "renewsOn": m67, "reportABug": MessageLookupByLibrary.simpleMessage("Hatayı bildir"), "reportBug": MessageLookupByLibrary.simpleMessage("Hata bildir"), "resendEmail": @@ -1124,31 +1583,46 @@ class MessageLookup extends MessageLookupByLibrary { "Yok sayılan dosyaları sıfırla"), "resetPasswordTitle": MessageLookupByLibrary.simpleMessage("Parolanızı sıfırlayın"), + "resetPerson": MessageLookupByLibrary.simpleMessage("Kaldır"), "resetToDefault": MessageLookupByLibrary.simpleMessage("Varsayılana sıfırla"), "restore": MessageLookupByLibrary.simpleMessage("Yenile"), "restoreToAlbum": MessageLookupByLibrary.simpleMessage("Albümü yenile"), "restoringFiles": MessageLookupByLibrary.simpleMessage("Dosyalar geri yükleniyor..."), + "resumableUploads": + MessageLookupByLibrary.simpleMessage("Devam edilebilir yüklemeler"), "retry": MessageLookupByLibrary.simpleMessage("Tekrar dene"), + "review": MessageLookupByLibrary.simpleMessage("Gözden Geçir"), "reviewDeduplicateItems": MessageLookupByLibrary.simpleMessage( "Lütfen kopya olduğunu düşündüğünüz öğeleri inceleyin ve silin."), + "reviewSuggestions": + MessageLookupByLibrary.simpleMessage("Önerileri inceleyin"), + "right": MessageLookupByLibrary.simpleMessage("Sağ"), + "rotate": MessageLookupByLibrary.simpleMessage("Döndür"), "rotateLeft": MessageLookupByLibrary.simpleMessage("Sola döndür"), "rotateRight": MessageLookupByLibrary.simpleMessage("Sağa döndür"), "safelyStored": MessageLookupByLibrary.simpleMessage("Güvenle saklanır"), "save": MessageLookupByLibrary.simpleMessage("Kaydet"), + "saveChangesBeforeLeavingQuestion": + MessageLookupByLibrary.simpleMessage( + "Ayrılmadan önce değişiklikleri kaydedin mi?"), "saveCollage": MessageLookupByLibrary.simpleMessage("Kolajı kaydet"), "saveCopy": MessageLookupByLibrary.simpleMessage("Kopyasını kaydet"), "saveKey": MessageLookupByLibrary.simpleMessage("Anahtarı kaydet"), + "savePerson": MessageLookupByLibrary.simpleMessage("Kişiyi Kaydet"), "saveYourRecoveryKeyIfYouHaventAlready": MessageLookupByLibrary.simpleMessage( "Henüz yapmadıysanız kurtarma anahtarınızı kaydetmeyi unutmayın"), "saving": MessageLookupByLibrary.simpleMessage("Kaydediliyor..."), + "savingEdits": MessageLookupByLibrary.simpleMessage( + "Düzenlemeler kaydediliyor..."), "scanCode": MessageLookupByLibrary.simpleMessage("Kodu tarayın"), "scanThisBarcodeWithnyourAuthenticatorApp": MessageLookupByLibrary.simpleMessage( "Kimlik doğrulama uygulamanız ile kodu tarayın"), + "search": MessageLookupByLibrary.simpleMessage("Ara"), "searchAlbumsEmptySection": MessageLookupByLibrary.simpleMessage("Albümler"), "searchByAlbumNameHint": @@ -1159,6 +1633,10 @@ class MessageLookup extends MessageLookupByLibrary { "Fotoğraf bilgilerini burada hızlı bir şekilde bulmak için \"#trip\" gibi açıklamalar ekleyin"), "searchDatesEmptySection": MessageLookupByLibrary.simpleMessage( "Tarihe, aya veya yıla göre arama yapın"), + "searchDiscoverEmptySection": MessageLookupByLibrary.simpleMessage( + "İşleme ve senkronizasyon tamamlandığında görüntüler burada gösterilecektir"), + "searchFaceEmptySection": MessageLookupByLibrary.simpleMessage( + "İndeksleme yapıldıktan sonra insanlar burada gösterilecek"), "searchFileTypesAndNamesEmptySection": MessageLookupByLibrary.simpleMessage("Dosya türleri ve adları"), "searchHint1": @@ -1174,25 +1652,41 @@ class MessageLookup extends MessageLookupByLibrary { "Bir fotoğrafın belli bir yarıçapında çekilen fotoğrafları gruplandırın"), "searchPeopleEmptySection": MessageLookupByLibrary.simpleMessage( "İnsanları davet ettiğinizde onların paylaştığı tüm fotoğrafları burada göreceksiniz"), - "searchResultCount": m63, + "searchPersonsEmptySection": MessageLookupByLibrary.simpleMessage( + "İşleme ve senkronizasyon tamamlandığında kişiler burada gösterilecektir"), + "searchResultCount": m68, + "searchSectionsLengthMismatch": m69, "security": MessageLookupByLibrary.simpleMessage("Güvenlik"), + "seePublicAlbumLinksInApp": MessageLookupByLibrary.simpleMessage( + "Uygulamadaki herkese açık albüm bağlantılarını görün"), "selectALocation": MessageLookupByLibrary.simpleMessage("Bir konum seçin"), "selectALocationFirst": MessageLookupByLibrary.simpleMessage("Önce yeni yer seçin"), "selectAlbum": MessageLookupByLibrary.simpleMessage("Albüm seçin"), "selectAll": MessageLookupByLibrary.simpleMessage("Hepsini seç"), + "selectAllShort": MessageLookupByLibrary.simpleMessage("Tümü"), + "selectCoverPhoto": + MessageLookupByLibrary.simpleMessage("Kapak fotoğrafı seçin"), "selectFoldersForBackup": MessageLookupByLibrary.simpleMessage( "Yedekleme için klasörleri seçin"), "selectItemsToAdd": MessageLookupByLibrary.simpleMessage("Eklenecek eşyaları seçin"), "selectLanguage": MessageLookupByLibrary.simpleMessage("Dil Seçin"), + "selectMailApp": + MessageLookupByLibrary.simpleMessage("Mail Uygulamasını Seç"), "selectMorePhotos": MessageLookupByLibrary.simpleMessage("Daha Fazla Fotoğraf Seç"), + "selectPersonToLink": MessageLookupByLibrary.simpleMessage( + "Bağlantı kurulacak kişiyi seçin"), "selectReason": MessageLookupByLibrary.simpleMessage("Ayrılma nedeninizi seçin"), + "selectYourFace": + MessageLookupByLibrary.simpleMessage("Yüzünüzü seçin"), "selectYourPlan": MessageLookupByLibrary.simpleMessage("Planınızı seçin"), + "selectedFilesAreNotOnEnte": MessageLookupByLibrary.simpleMessage( + "Seçilen dosyalar Ente\'de değil"), "selectedFoldersWillBeEncryptedAndBackedUp": MessageLookupByLibrary.simpleMessage( "Seçilen klasörler şifrelenecek ve yedeklenecektir"), @@ -1200,7 +1694,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage( "Seçilen öğeler tüm albümlerden silinecek ve çöp kutusuna taşınacak."), "selectedPhotos": m6, - "selectedPhotosWithYours": m65, + "selectedPhotosWithYours": m70, "send": MessageLookupByLibrary.simpleMessage("Gönder"), "sendEmail": MessageLookupByLibrary.simpleMessage("E-posta gönder"), "sendInvite": MessageLookupByLibrary.simpleMessage("Davet kodu gönder"), @@ -1209,10 +1703,16 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Sunucu uç noktası"), "sessionExpired": MessageLookupByLibrary.simpleMessage("Oturum süresi doldu"), + "sessionIdMismatch": + MessageLookupByLibrary.simpleMessage("Oturum kimliği uyuşmazlığı"), "setAPassword": MessageLookupByLibrary.simpleMessage("Şifre ayarla"), "setAs": MessageLookupByLibrary.simpleMessage("Şu şekilde ayarla"), "setCover": MessageLookupByLibrary.simpleMessage("Kapak Belirle"), "setLabel": MessageLookupByLibrary.simpleMessage("Ayarla"), + "setNewPassword": + MessageLookupByLibrary.simpleMessage("Yeni şifre belirle"), + "setNewPin": + MessageLookupByLibrary.simpleMessage("Yeni PIN belirleyin"), "setPasswordTitle": MessageLookupByLibrary.simpleMessage("Parola ayarlayın"), "setRadius": MessageLookupByLibrary.simpleMessage("Yarıçapı ayarla"), @@ -1225,15 +1725,20 @@ class MessageLookup extends MessageLookupByLibrary { "shareAnAlbumNow": MessageLookupByLibrary.simpleMessage("Şimdi bir albüm paylaşın"), "shareLink": MessageLookupByLibrary.simpleMessage("Linki paylaş"), - "shareMyVerificationID": m66, + "shareMyVerificationID": m71, "shareOnlyWithThePeopleYouWant": MessageLookupByLibrary.simpleMessage( "Yalnızca istediğiniz kişilerle paylaşın"), "shareTextConfirmOthersVerificationID": m7, + "shareTextRecommendUsingEnte": MessageLookupByLibrary.simpleMessage( + "Orijinal kalitede fotoğraf ve videoları kolayca paylaşabilmemiz için Ente\'yi indirin\n\nhttps://ente.io"), + "shareTextReferralCode": m72, "shareWithNonenteUsers": MessageLookupByLibrary.simpleMessage( "Ente kullanıcısı olmayanlar için paylaş"), - "shareWithPeopleSectionTitle": m68, + "shareWithPeopleSectionTitle": m73, "shareYourFirstAlbum": MessageLookupByLibrary.simpleMessage("İlk albümünüzü paylaşın"), + "sharedAlbumSectionDescription": MessageLookupByLibrary.simpleMessage( + "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."), "sharedByMe": MessageLookupByLibrary.simpleMessage("Benim paylaştıklarım"), "sharedByYou": MessageLookupByLibrary.simpleMessage("Paylaştıklarınız"), @@ -1241,13 +1746,14 @@ class MessageLookup extends MessageLookupByLibrary { "Paylaşılan fotoğrafları ekle"), "sharedPhotoNotificationsExplanation": MessageLookupByLibrary.simpleMessage( "Birisi sizin de parçası olduğunuz paylaşılan bir albüme fotoğraf eklediğinde bildirim alın"), - "sharedWith": m69, + "sharedWith": m74, "sharedWithMe": MessageLookupByLibrary.simpleMessage("Benimle paylaşılan"), "sharedWithYou": MessageLookupByLibrary.simpleMessage("Sizinle paylaşıldı"), "sharing": MessageLookupByLibrary.simpleMessage("Paylaşılıyor..."), "showMemories": MessageLookupByLibrary.simpleMessage("Anıları göster"), + "showPerson": MessageLookupByLibrary.simpleMessage("Kişiyi Göster"), "signOutFromOtherDevices": MessageLookupByLibrary.simpleMessage("Diğer cihazlardan çıkış yap"), "signOutOtherBody": MessageLookupByLibrary.simpleMessage( @@ -1256,11 +1762,16 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Diğer cihazlardan çıkış yap"), "signUpTerms": MessageLookupByLibrary.simpleMessage( "Hizmet Şartları\'nı ve Gizlilik Politikası\'nı kabul ediyorum"), - "singleFileDeleteFromDevice": m70, + "singleFileDeleteFromDevice": m75, "singleFileDeleteHighlight": MessageLookupByLibrary.simpleMessage("Tüm albümlerden silinecek."), + "singleFileInBothLocalAndRemote": m76, + "singleFileInRemoteOnly": m77, "skip": MessageLookupByLibrary.simpleMessage("Geç"), "social": MessageLookupByLibrary.simpleMessage("Sosyal Medya"), + "someItemsAreInBothEnteAndYourDevice": + MessageLookupByLibrary.simpleMessage( + "Bazı öğeler hem Ente\'de hem de cihazınızda bulunur."), "someOfTheFilesYouAreTryingToDeleteAre": MessageLookupByLibrary.simpleMessage( "Silmeye çalıştığınız dosyalardan bazıları yalnızca cihazınızda mevcuttur ve silindiği takdirde kurtarılamaz"), @@ -1284,24 +1795,36 @@ class MessageLookup extends MessageLookupByLibrary { "sorryWeCouldNotGenerateSecureKeysOnThisDevicennplease": MessageLookupByLibrary.simpleMessage( "Üzgünüm, bu cihazda güvenli anahtarlarını oluşturamadık.\n\nLütfen başka bir cihazdan giriş yapmayı deneyiniz."), + "sort": MessageLookupByLibrary.simpleMessage("Sırala"), "sortAlbumsBy": MessageLookupByLibrary.simpleMessage("Sırala"), "sortNewestFirst": MessageLookupByLibrary.simpleMessage("Yeniden eskiye"), "sortOldestFirst": MessageLookupByLibrary.simpleMessage("Önce en eski"), "sparkleSuccess": MessageLookupByLibrary.simpleMessage("✨ Başarılı"), + "startAccountRecoveryTitle": + MessageLookupByLibrary.simpleMessage("Kurtarmayı başlat"), "startBackup": MessageLookupByLibrary.simpleMessage("Yedeklemeyi başlat"), "status": MessageLookupByLibrary.simpleMessage("Durum"), + "stopCastingBody": MessageLookupByLibrary.simpleMessage( + "Yansıtmayı durdurmak istiyor musunuz?"), + "stopCastingTitle": + MessageLookupByLibrary.simpleMessage("Yayını durdur"), "storage": MessageLookupByLibrary.simpleMessage("Depolama"), "storageBreakupFamily": MessageLookupByLibrary.simpleMessage("Aile"), "storageBreakupYou": MessageLookupByLibrary.simpleMessage("Sen"), "storageInGB": m1, "storageLimitExceeded": MessageLookupByLibrary.simpleMessage("Depolama sınırı aşıldı"), - "storageUsageInfo": m73, + "storageUsageInfo": m78, + "streamDetails": + MessageLookupByLibrary.simpleMessage("Yayın detayları"), "strongStrength": MessageLookupByLibrary.simpleMessage("Güçlü"), - "subWillBeCancelledOn": m75, + "subAlreadyLinkedErrMessage": m79, + "subWillBeCancelledOn": m80, "subscribe": MessageLookupByLibrary.simpleMessage("Abone ol"), + "subscribeToEnableSharing": MessageLookupByLibrary.simpleMessage( + "Paylaşımı etkinleştirmek için aktif bir ücretli aboneliğe ihtiyacınız var."), "subscription": MessageLookupByLibrary.simpleMessage("Abonelik"), "success": MessageLookupByLibrary.simpleMessage("Başarılı"), "successfullyArchived": @@ -1315,7 +1838,7 @@ class MessageLookup extends MessageLookupByLibrary { "suggestFeatures": MessageLookupByLibrary.simpleMessage("Özellik önerin"), "support": MessageLookupByLibrary.simpleMessage("Destek"), - "syncProgress": m76, + "syncProgress": m81, "syncStopped": MessageLookupByLibrary.simpleMessage("Senkronizasyon durduruldu"), "syncing": MessageLookupByLibrary.simpleMessage("Eşitleniyor..."), @@ -1324,6 +1847,10 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("kopyalamak için dokunun"), "tapToEnterCode": MessageLookupByLibrary.simpleMessage("Kodu girmek icin tıklayın"), + "tapToUnlock": MessageLookupByLibrary.simpleMessage("Açmak için dokun"), + "tapToUpload": + MessageLookupByLibrary.simpleMessage("Yüklemek için tıklayın"), + "tapToUploadIsIgnoredDue": m82, "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"), @@ -1336,6 +1863,9 @@ class MessageLookup extends MessageLookupByLibrary { "Abone olduğunuz için teşekkürler!"), "theDownloadCouldNotBeCompleted": MessageLookupByLibrary.simpleMessage( "İndirme işlemi tamamlanamadı"), + "theLinkYouAreTryingToAccessHasExpired": + MessageLookupByLibrary.simpleMessage( + "Erişmeye çalıştığınız bağlantının süresi dolmuştur."), "theRecoveryKeyYouEnteredIsIncorrect": MessageLookupByLibrary.simpleMessage( "Girdiğiniz kurtarma kodu yanlış"), @@ -1359,7 +1889,9 @@ class MessageLookup extends MessageLookupByLibrary { "Bu e-posta zaten kullanılıyor"), "thisImageHasNoExifData": MessageLookupByLibrary.simpleMessage("Bu görselde exif verisi yok"), - "thisIsPersonVerificationId": m78, + "thisIsMeExclamation": + MessageLookupByLibrary.simpleMessage("Bu benim!"), + "thisIsPersonVerificationId": m83, "thisIsYourVerificationId": MessageLookupByLibrary.simpleMessage("Doğrulama kimliğiniz"), "thisWillLogYouOutOfTheFollowingDevice": @@ -1367,16 +1899,31 @@ class MessageLookup extends MessageLookupByLibrary { "Bu, sizi aşağıdaki cihazdan çıkış yapacak:"), "thisWillLogYouOutOfThisDevice": MessageLookupByLibrary.simpleMessage( "Bu cihazdaki oturumunuz kapatılacak!"), + "thisWillRemovePublicLinksOfAllSelectedQuickLinks": + MessageLookupByLibrary.simpleMessage( + "Bu, seçilen tüm hızlı bağlantıların genel bağlantılarını kaldıracaktır."), + "toEnableAppLockPleaseSetupDevicePasscodeOrScreen": + MessageLookupByLibrary.simpleMessage( + "Uygulama kilidini etkinleştirmek için lütfen sistem ayarlarınızda cihaz şifresi veya ekran kilidi ayarlayın."), "toHideAPhotoOrVideo": MessageLookupByLibrary.simpleMessage( "Bir fotoğrafı veya videoyu gizlemek için"), "toResetVerifyEmail": MessageLookupByLibrary.simpleMessage( "Şifrenizi sıfılamak için lütfen e-postanızı girin."), "todaysLogs": MessageLookupByLibrary.simpleMessage("Bugünün günlükleri"), + "tooManyIncorrectAttempts": + MessageLookupByLibrary.simpleMessage("Çok fazla hatalı deneme"), "total": MessageLookupByLibrary.simpleMessage("total"), "totalSize": MessageLookupByLibrary.simpleMessage("Toplam boyut"), "trash": MessageLookupByLibrary.simpleMessage("Cöp kutusu"), + "trashDaysLeft": m84, + "trim": MessageLookupByLibrary.simpleMessage("Kes"), + "trustedContacts": + MessageLookupByLibrary.simpleMessage("Güvenilir kişiler"), + "trustedInviteBody": m85, "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."), "twitter": MessageLookupByLibrary.simpleMessage("Twitter"), "twoMonthsFreeOnYearlyPlans": MessageLookupByLibrary.simpleMessage( "Yıllık planlarda 2 ay ücretsiz"), @@ -1391,11 +1938,14 @@ class MessageLookup extends MessageLookupByLibrary { "İki faktörlü kimlik doğrulama başarıyla sıfırlandı"), "twofactorSetup": MessageLookupByLibrary.simpleMessage("Cift faktör ayarı"), + "typeOfGallerGallerytypeIsNotSupportedForRename": m86, "unarchive": MessageLookupByLibrary.simpleMessage("Arşivden cıkar"), "unarchiveAlbum": MessageLookupByLibrary.simpleMessage("Arşivden Çıkar"), "unarchiving": MessageLookupByLibrary.simpleMessage("Arşivden çıkarılıyor..."), + "unavailableReferralCode": MessageLookupByLibrary.simpleMessage( + "Üzgünüz, bu kod mevcut değil."), "uncategorized": MessageLookupByLibrary.simpleMessage("Kategorisiz"), "unhide": MessageLookupByLibrary.simpleMessage("Gizleme"), "unhideToAlbum": MessageLookupByLibrary.simpleMessage("Albümü gizleme"), @@ -1413,18 +1963,29 @@ class MessageLookup extends MessageLookupByLibrary { "updatingFolderSelection": MessageLookupByLibrary.simpleMessage( "Klasör seçimi güncelleniyor..."), "upgrade": MessageLookupByLibrary.simpleMessage("Yükselt"), + "uploadIsIgnoredDueToIgnorereason": m87, "uploadingFilesToAlbum": MessageLookupByLibrary.simpleMessage( "Dosyalar albüme taşınıyor..."), + "uploadingMultipleMemories": m88, + "uploadingSingleMemory": + MessageLookupByLibrary.simpleMessage("1 anı korunuyor..."), "upto50OffUntil4thDec": MessageLookupByLibrary.simpleMessage( "4 Aralık\'a kadar %50\'ye varan indirim."), "usableReferralStorageInfo": MessageLookupByLibrary.simpleMessage( "Kullanılabilir depolama alanı mevcut planınızla sınırlıdır. Talep edilen fazla depolama alanı, planınızı yükselttiğinizde otomatik olarak kullanılabilir hale gelecektir."), + "useAsCover": + MessageLookupByLibrary.simpleMessage("Kapak olarak kullanın"), + "useDifferentPlayerInfo": MessageLookupByLibrary.simpleMessage( + "Bu videoyu oynatmakta sorun mu yaşıyorsunuz? Farklı bir oynatıcı denemek için buraya uzun basın."), + "usePublicLinksForPeopleNotOnEnte": + MessageLookupByLibrary.simpleMessage( + "Ente\'de olmayan kişiler için genel bağlantıları kullanın"), "useRecoveryKey": MessageLookupByLibrary.simpleMessage("Kurtarma anahtarını kullan"), "useSelectedPhoto": MessageLookupByLibrary.simpleMessage("Seçilen fotoğrafı kullan"), "usedSpace": MessageLookupByLibrary.simpleMessage("Kullanılan alan"), - "validTill": m84, + "validTill": m89, "verificationFailedPleaseTryAgain": MessageLookupByLibrary.simpleMessage( "Doğrulama başarısız oldu, lütfen tekrar deneyin"), @@ -1433,7 +1994,7 @@ class MessageLookup extends MessageLookupByLibrary { "verify": MessageLookupByLibrary.simpleMessage("Doğrula"), "verifyEmail": MessageLookupByLibrary.simpleMessage("E-posta adresini doğrulayın"), - "verifyEmailID": m85, + "verifyEmailID": m90, "verifyIDLabel": MessageLookupByLibrary.simpleMessage("Doğrula"), "verifyPasskey": MessageLookupByLibrary.simpleMessage("Şifrenizi doğrulayın"), @@ -1442,7 +2003,9 @@ class MessageLookup extends MessageLookupByLibrary { "verifying": MessageLookupByLibrary.simpleMessage("Doğrulanıyor..."), "verifyingRecoveryKey": MessageLookupByLibrary.simpleMessage( "Kurtarma kodu doğrulanıyor..."), + "videoInfo": MessageLookupByLibrary.simpleMessage("Video Bilgileri"), "videoSmallCase": MessageLookupByLibrary.simpleMessage("video"), + "videoStreaming": MessageLookupByLibrary.simpleMessage("Video akışı"), "videos": MessageLookupByLibrary.simpleMessage("Videolar"), "viewActiveSessions": MessageLookupByLibrary.simpleMessage("Aktif oturumları görüntüle"), @@ -1451,16 +2014,22 @@ class MessageLookup extends MessageLookupByLibrary { "viewAll": MessageLookupByLibrary.simpleMessage("Tümünü görüntüle"), "viewAllExifData": MessageLookupByLibrary.simpleMessage( "Tüm EXIF verilerini görüntüle"), + "viewLargeFiles": + MessageLookupByLibrary.simpleMessage("Büyük dosyalar"), + "viewLargeFilesDesc": MessageLookupByLibrary.simpleMessage( + "En fazla depolama alanı tüketen dosyaları görüntüleyin."), "viewLogs": MessageLookupByLibrary.simpleMessage("Günlükleri göster"), "viewRecoveryKey": MessageLookupByLibrary.simpleMessage( "Kurtarma anahtarını görüntüle"), "viewer": MessageLookupByLibrary.simpleMessage("Görüntüleyici"), + "viewersSuccessfullyAdded": m91, "visitWebToManage": MessageLookupByLibrary.simpleMessage( "Aboneliğinizi yönetmek için lütfen web.ente.io adresini ziyaret edin"), "waitingForVerification": MessageLookupByLibrary.simpleMessage("Doğrulama bekleniyor..."), "waitingForWifi": MessageLookupByLibrary.simpleMessage("WiFi bekleniyor..."), + "warning": MessageLookupByLibrary.simpleMessage("Uyarı"), "weAreOpenSource": MessageLookupByLibrary.simpleMessage("Biz açık kaynağız!"), "weDontSupportEditingPhotosAndAlbumsThatYouDont": @@ -1470,8 +2039,11 @@ class MessageLookup extends MessageLookupByLibrary { "weakStrength": MessageLookupByLibrary.simpleMessage("Zayıf"), "welcomeBack": MessageLookupByLibrary.simpleMessage("Tekrardan hoşgeldin!"), + "whatsNew": MessageLookupByLibrary.simpleMessage("Yenilikler"), + "whyAddTrustContact": MessageLookupByLibrary.simpleMessage("."), + "yearShort": MessageLookupByLibrary.simpleMessage("yıl"), "yearly": MessageLookupByLibrary.simpleMessage("Yıllık"), - "yearsAgo": m87, + "yearsAgo": m92, "yes": MessageLookupByLibrary.simpleMessage("Evet"), "yesCancel": MessageLookupByLibrary.simpleMessage("Evet, iptal et"), "yesConvertToViewer": MessageLookupByLibrary.simpleMessage( @@ -1483,6 +2055,8 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Evet, oturumu kapat"), "yesRemove": MessageLookupByLibrary.simpleMessage("Evet, sil"), "yesRenew": MessageLookupByLibrary.simpleMessage("Evet, yenile"), + "yesResetPerson": + MessageLookupByLibrary.simpleMessage("Evet, kişiyi sıfırla"), "you": MessageLookupByLibrary.simpleMessage("Sen"), "youAreOnAFamilyPlan": MessageLookupByLibrary.simpleMessage("Aile planı kullanıyorsunuz!"), @@ -1502,7 +2076,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Kendinizle paylaşamazsınız"), "youDontHaveAnyArchivedItems": MessageLookupByLibrary.simpleMessage("Arşivlenmiş öğeniz yok."), - "youHaveSuccessfullyFreedUp": m88, + "youHaveSuccessfullyFreedUp": m93, "yourAccountHasBeenDeleted": MessageLookupByLibrary.simpleMessage("Hesabınız silindi"), "yourMap": MessageLookupByLibrary.simpleMessage("Haritalarınız"), @@ -1522,9 +2096,6 @@ class MessageLookup extends MessageLookupByLibrary { "Aboneliğiniz başarıyla güncellendi"), "yourVerificationCodeHasExpired": MessageLookupByLibrary.simpleMessage( "Doğrulama kodunuzun süresi doldu"), - "youveNoDuplicateFilesThatCanBeCleared": - MessageLookupByLibrary.simpleMessage( - "Temizlenebilecek yinelenen dosyalarınız yok"), "youveNoFilesInThisAlbumThatCanBeDeleted": MessageLookupByLibrary.simpleMessage( "Bu cihazda silinebilecek hiçbir dosyanız yok"), diff --git a/mobile/lib/generated/intl/messages_uk.dart b/mobile/lib/generated/intl/messages_uk.dart index a2a1e6b966..f4231faa04 100644 --- a/mobile/lib/generated/intl/messages_uk.dart +++ b/mobile/lib/generated/intl/messages_uk.dart @@ -20,246 +20,246 @@ typedef String MessageIfAbsent(String messageStr, List args); class MessageLookup extends MessageLookupByLibrary { String get localeName => 'uk'; - static String m9(count) => + static String m10(count) => "${Intl.plural(count, one: 'Додано співавтора', other: 'Додано співавторів')}"; - static String m10(count) => + static String m11(count) => "${Intl.plural(count, one: 'Додавання елемента', other: 'Додавання елементів')}"; - static String m11(storageAmount, endDate) => + static String m12(storageAmount, endDate) => "Ваше доповнення ${storageAmount} діє до ${endDate}"; - static String m12(count) => + static String m13(count) => "${Intl.plural(count, one: 'Додано глядача', other: 'Додано глядачів')}"; - static String m13(emailOrName) => "Додано ${emailOrName}"; + static String m14(emailOrName) => "Додано ${emailOrName}"; - static String m14(albumName) => "Успішно додано до «${albumName}»"; + static String m15(albumName) => "Успішно додано до «${albumName}»"; - static String m15(count) => + static String m16(count) => "${Intl.plural(count, zero: 'Немає учасників', one: '1 учасник', other: '${count} учасників')}"; - static String m16(versionValue) => "Версія: ${versionValue}"; + static String m17(versionValue) => "Версія: ${versionValue}"; - static String m17(freeAmount, storageUnit) => + static String m18(freeAmount, storageUnit) => "${freeAmount} ${storageUnit} вільно"; - static String m18(paymentProvider) => + static String m19(paymentProvider) => "Спочатку скасуйте вашу передплату від ${paymentProvider}"; static String m3(user) => "${user} не зможе додавати більше фотографій до цього альбому\n\nВони все ще зможуть видаляти додані ними фотографії"; - static String m19(isFamilyMember, storageAmountInGb) => + static String m20(isFamilyMember, storageAmountInGb) => "${Intl.select(isFamilyMember, { 'true': 'Ваша сім\'я отримала ${storageAmountInGb} ГБ', 'false': 'Ви отримали ${storageAmountInGb} ГБ', 'other': 'Ви отримали ${storageAmountInGb} ГБ!', })}"; - static String m20(albumName) => + static String m21(albumName) => "Створено спільне посилання для «${albumName}»"; - static String m21(count) => + static String m22(count) => "${Intl.plural(count, zero: 'Додано 0 співавторів', one: 'Додано 1 співавтор', few: 'Додано ${count} співаторів', many: 'Додано ${count} співаторів', other: 'Додано ${count} співавторів')}"; - static String m22(email, numOfDays) => + static String m23(email, numOfDays) => "Ви збираєтеся додати ${email} як довірений контакт. Вони зможуть відновити ваш обліковий запис, якщо ви будете відсутні протягом ${numOfDays} днів."; - static String m23(familyAdminEmail) => + static String m24(familyAdminEmail) => "Зв\'яжіться з ${familyAdminEmail} для керування вашою передплатою"; - static String m24(provider) => + static String m25(provider) => "Зв\'яжіться з нами за адресою support@ente.io для управління вашою передплатою ${provider}."; - static String m25(endpoint) => "Під\'єднано до ${endpoint}"; + static String m26(endpoint) => "Під\'єднано до ${endpoint}"; - static String m26(count) => + static String m27(count) => "${Intl.plural(count, one: 'Видалено ${count} елемент', few: 'Видалено ${count} елементи', many: 'Видалено ${count} елементів', other: 'Видалено ${count} елементів')}"; - static String m27(currentlyDeleting, totalCount) => + static String m28(currentlyDeleting, totalCount) => "Видалення ${currentlyDeleting} / ${totalCount}"; - static String m28(albumName) => + static String m29(albumName) => "Це видалить публічне посилання для доступу до «${albumName}»."; - static String m29(supportEmail) => + static String m30(supportEmail) => "Надішліть листа на ${supportEmail} з вашої зареєстрованої поштової адреси"; - static String m30(count, storageSaved) => + static String m31(count, storageSaved) => "Ви очистили ${Intl.plural(count, one: '${count} дублікат файлу', other: '${count} дублікатів файлів')}, збережено (${storageSaved}!)"; - static String m31(count, formattedSize) => + static String m32(count, formattedSize) => "${count} файлів, кожен по ${formattedSize}"; - static String m32(newEmail) => "Поштову адресу змінено на ${newEmail}"; + static String m33(newEmail) => "Поштову адресу змінено на ${newEmail}"; - static String m33(email) => + static String m35(email) => "У ${email} немає облікового запису Ente.\n\nНадішліть їм запрошення для обміну фотографіями."; - static String m34(text) => "Знайдено додаткові фотографії для ${text}"; + static String m36(text) => "Знайдено додаткові фотографії для ${text}"; - static String m35(count, formattedNumber) => + static String m37(count, formattedNumber) => "${Intl.plural(count, one: 'Для 1 файлу', other: 'Для ${formattedNumber} файлів')} на цьому пристрої було створено резервну копію"; - static String m36(count, formattedNumber) => + static String m38(count, formattedNumber) => "${Intl.plural(count, one: 'Для 1 файлу', few: 'Для ${formattedNumber} файлів', many: 'Для ${formattedNumber} файлів', other: 'Для ${formattedNumber} файлів')} у цьому альбомі було створено резервну копію"; static String m4(storageAmountInGB) => "${storageAmountInGB} ГБ щоразу, коли хтось оформлює передплату і застосовує ваш код"; - static String m37(endDate) => "Безплатна пробна версія діє до ${endDate}"; + static String m39(endDate) => "Безплатна пробна версія діє до ${endDate}"; - static String m38(count) => + static String m40(count) => "Ви все ще можете отримати доступ до ${Intl.plural(count, one: 'нього', other: 'них')} в Ente, доки у вас активна передплата"; - static String m39(sizeInMBorGB) => "Звільніть ${sizeInMBorGB}"; + static String m41(sizeInMBorGB) => "Звільніть ${sizeInMBorGB}"; - static String m40(count, formattedSize) => + static String m42(count, formattedSize) => "${Intl.plural(count, one: 'Його можна видалити з пристрою, щоб звільнити ${formattedSize}', other: 'Їх можна видалити з пристрою, щоб звільнити ${formattedSize}')}"; - static String m41(currentlyProcessing, totalCount) => + static String m43(currentlyProcessing, totalCount) => "Обробка ${currentlyProcessing} / ${totalCount}"; - static String m42(count) => + static String m44(count) => "${Intl.plural(count, one: '${count} елемент', few: '${count} елементи', many: '${count} елементів', other: '${count} елементів')}"; - static String m43(email) => "${email} запросив вас стати довіреною особою"; + static String m45(email) => "${email} запросив вас стати довіреною особою"; - static String m44(expiryTime) => "Посилання закінчується через ${expiryTime}"; + static String m46(expiryTime) => "Посилання закінчується через ${expiryTime}"; static String m5(count, formattedCount) => "${Intl.plural(count, zero: 'немає спогадів', one: '${formattedCount} спогад', other: '${formattedCount} спогадів')}"; - static String m45(count) => + static String m49(count) => "${Intl.plural(count, one: 'Переміщення елемента', other: 'Переміщення елементів')}"; - static String m46(albumName) => "Успішно перенесено до «${albumName}»"; + static String m50(albumName) => "Успішно перенесено до «${albumName}»"; - static String m47(personName) => "Немає пропозицій для ${personName}"; + static String m51(personName) => "Немає пропозицій для ${personName}"; - static String m48(name) => "Не ${name}?"; + static String m52(name) => "Не ${name}?"; - static String m49(familyAdminEmail) => + static String m53(familyAdminEmail) => "Зв\'яжіться з ${familyAdminEmail}, щоб змінити код."; static String m0(passwordStrengthValue) => "Надійність пароля: ${passwordStrengthValue}"; - static String m50(providerName) => + static String m54(providerName) => "Зверніться до ${providerName}, якщо було знято платіж"; - static String m51(count) => + static String m55(count) => "${Intl.plural(count, zero: '0 фото', one: '1 фото', few: '${count} фото', many: '${count} фото', other: '${count} фото')}"; - static String m52(endDate) => + static String m56(endDate) => "Безплатна пробна версія діє до ${endDate}.\nПісля цього ви можете обрати платний план."; - static String m53(toEmail) => "Напишіть нам на ${toEmail}"; + static String m57(toEmail) => "Напишіть нам на ${toEmail}"; - static String m54(toEmail) => "Надішліть журнали на \n${toEmail}"; + static String m58(toEmail) => "Надішліть журнали на \n${toEmail}"; - static String m55(folderName) => "Оброблюємо «${folderName}»..."; + static String m59(folderName) => "Оброблюємо «${folderName}»..."; - static String m56(storeName) => "Оцініть нас в ${storeName}"; + static String m60(storeName) => "Оцініть нас в ${storeName}"; - static String m57(days, email) => + static String m62(days, email) => "Ви зможете отримати доступ до облікового запису через ${days} днів. Повідомлення буде надіслано на ${email}."; - static String m58(email) => + static String m63(email) => "Тепер ви можете відновити обліковий запис ${email}, встановивши новий пароль."; - static String m59(email) => + static String m64(email) => "${email} намагається відновити ваш обліковий запис."; - static String m60(storageInGB) => + static String m65(storageInGB) => "3. Ви обоє отримуєте ${storageInGB} ГБ* безплатно"; - static String m61(userEmail) => + static String m66(userEmail) => "${userEmail} буде видалено з цього спільного альбому\n\nБудь-які додані вами фото, будуть також видалені з альбому"; - static String m62(endDate) => "Передплата поновиться ${endDate}"; + static String m67(endDate) => "Передплата поновиться ${endDate}"; - static String m63(count) => + static String m68(count) => "${Intl.plural(count, one: 'Знайдено ${count} результат', few: 'Знайдено ${count} результати', many: 'Знайдено ${count} результатів', other: 'Знайдено ${count} результати')}"; - static String m64(snapshotLength, searchLength) => + static String m69(snapshotLength, searchLength) => "Невідповідність довжини розділів: ${snapshotLength} != ${searchLength}"; static String m6(count) => "${count} вибрано"; - static String m65(count, yourCount) => "${count} вибрано (${yourCount} ваші)"; + static String m70(count, yourCount) => "${count} вибрано (${yourCount} ваші)"; - static String m66(verificationID) => + static String m71(verificationID) => "Ось мій ідентифікатор підтвердження: ${verificationID} для ente.io."; static String m7(verificationID) => "Гей, ви можете підтвердити, що це ваш ідентифікатор підтвердження: ${verificationID}"; - static String m67(referralCode, referralStorageInGB) => + static String m72(referralCode, referralStorageInGB) => "Реферальний код Ente: ${referralCode} \n\nЗастосуйте його в «Налаштування» → «Загальні» → «Реферали», щоб отримати ${referralStorageInGB} ГБ безплатно після переходу на платний тариф\n\nhttps://ente.io"; - static String m68(numberOfPeople) => + static String m73(numberOfPeople) => "${Intl.plural(numberOfPeople, zero: 'Поділитися з конкретними людьми', one: 'Поділитися з 1 особою', other: 'Поділитися з ${numberOfPeople} людьми')}"; - static String m69(emailIDs) => "Поділилися з ${emailIDs}"; + static String m74(emailIDs) => "Поділилися з ${emailIDs}"; - static String m70(fileType) => "Цей ${fileType} буде видалено з пристрою."; + static String m75(fileType) => "Цей ${fileType} буде видалено з пристрою."; - static String m71(fileType) => + static String m76(fileType) => "Цей ${fileType} знаходиться і в Ente, і на вашому пристрої."; - static String m72(fileType) => "Цей ${fileType} буде видалено з Ente."; + static String m77(fileType) => "Цей ${fileType} буде видалено з Ente."; static String m1(storageAmountInGB) => "${storageAmountInGB} ГБ"; - static String m73( + static String m78( usedAmount, usedStorageUnit, totalAmount, totalStorageUnit) => "${usedAmount} ${usedStorageUnit} з ${totalAmount} ${totalStorageUnit} використано"; - static String m74(id) => + static String m79(id) => "Ваш ${id} вже пов\'язаний з іншим обліковим записом Ente.\nЯкщо ви хочете використовувати свій ${id} з цим обліковим записом, зверніться до нашої служби підтримки"; - static String m75(endDate) => "Вашу передплату буде скасовано ${endDate}"; + static String m80(endDate) => "Вашу передплату буде скасовано ${endDate}"; - static String m76(completed, total) => + static String m81(completed, total) => "${completed} / ${total} спогадів збережено"; - static String m77(ignoreReason) => + static String m82(ignoreReason) => "Натисніть, щоб завантажити; завантаження наразі ігнорується через: ${ignoreReason}"; static String m8(storageAmountInGB) => "Вони також отримують ${storageAmountInGB} ГБ"; - static String m78(email) => "Це ідентифікатор підтвердження пошти ${email}"; + static String m83(email) => "Це ідентифікатор підтвердження пошти ${email}"; - static String m79(count) => + static String m84(count) => "${Intl.plural(count, zero: 'Незабаром', one: '1 день', other: '${count} днів')}"; - static String m80(email) => + static String m85(email) => "Ви отримали запрошення стати спадковим контактом від ${email}."; - static String m81(galleryType) => + static String m86(galleryType) => "Тип галереї «${galleryType}» не підтримується для перейменування"; - static String m82(ignoreReason) => + static String m87(ignoreReason) => "Завантаження проігноровано через: ${ignoreReason}"; - static String m83(count) => "Збереження ${count} спогадів..."; + static String m88(count) => "Збереження ${count} спогадів..."; - static String m84(endDate) => "Діє до ${endDate}"; + static String m89(endDate) => "Діє до ${endDate}"; - static String m85(email) => "Підтвердити ${email}"; + static String m90(email) => "Підтвердити ${email}"; - static String m86(count) => + static String m91(count) => "${Intl.plural(count, zero: 'Додано 0 користувачів', one: 'Додано 1 користувач', few: 'Додано ${count} користувача', many: 'Додано ${count} користувачів', other: 'Додано ${count} користувачів')}"; static String m2(email) => "Ми надіслали листа на ${email}"; - static String m87(count) => + static String m92(count) => "${Intl.plural(count, one: '${count} рік тому', few: '${count} роки тому', many: '${count} років тому', other: '${count} років тому')}"; - static String m88(storageSaved) => "Ви успішно звільнили ${storageSaved}!"; + static String m93(storageSaved) => "Ви успішно звільнили ${storageSaved}!"; final messages = _notInlinedMessages(_notInlinedMessages); static Map _notInlinedMessages(_) => { @@ -283,11 +283,11 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Додати нову пошту"), "addCollaborator": MessageLookupByLibrary.simpleMessage("Додати співавтора"), - "addCollaborators": m9, + "addCollaborators": m10, "addFiles": MessageLookupByLibrary.simpleMessage("Додати файли"), "addFromDevice": MessageLookupByLibrary.simpleMessage("Додати з пристрою"), - "addItem": m10, + "addItem": m11, "addLocation": MessageLookupByLibrary.simpleMessage("Додати розташування"), "addLocationButton": MessageLookupByLibrary.simpleMessage("Додати"), @@ -300,7 +300,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Додати нову особу"), "addOnPageSubtitle": MessageLookupByLibrary.simpleMessage("Подробиці доповнень"), - "addOnValidTill": m11, + "addOnValidTill": m12, "addOns": MessageLookupByLibrary.simpleMessage("Доповнення"), "addPhotos": MessageLookupByLibrary.simpleMessage("Додати фотографії"), "addSelected": MessageLookupByLibrary.simpleMessage("Додати вибране"), @@ -311,12 +311,12 @@ class MessageLookup extends MessageLookupByLibrary { "addTrustedContact": MessageLookupByLibrary.simpleMessage("Додати довірений контакт"), "addViewer": MessageLookupByLibrary.simpleMessage("Додати глядача"), - "addViewers": m12, + "addViewers": m13, "addYourPhotosNow": MessageLookupByLibrary.simpleMessage("Додайте свої фотографії"), "addedAs": MessageLookupByLibrary.simpleMessage("Додано як"), - "addedBy": m13, - "addedSuccessfullyTo": m14, + "addedBy": m14, + "addedSuccessfullyTo": m15, "addingToFavorites": MessageLookupByLibrary.simpleMessage("Додавання до обраного..."), "advanced": MessageLookupByLibrary.simpleMessage("Додатково"), @@ -327,7 +327,7 @@ class MessageLookup extends MessageLookupByLibrary { "after1Week": MessageLookupByLibrary.simpleMessage("Через 1 тиждень"), "after1Year": MessageLookupByLibrary.simpleMessage("Через 1 рік"), "albumOwner": MessageLookupByLibrary.simpleMessage("Власник"), - "albumParticipantsCount": m15, + "albumParticipantsCount": m16, "albumTitle": MessageLookupByLibrary.simpleMessage("Назва альбому"), "albumUpdated": MessageLookupByLibrary.simpleMessage("Альбом оновлено"), "albums": MessageLookupByLibrary.simpleMessage("Альбоми"), @@ -377,7 +377,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Блокування застосунку"), "appLockDescriptions": MessageLookupByLibrary.simpleMessage( "Виберіть між типовим екраном блокування вашого пристрою та власним екраном блокування з PIN-кодом або паролем."), - "appVersion": m16, + "appVersion": m17, "appleId": MessageLookupByLibrary.simpleMessage("Apple ID"), "apply": MessageLookupByLibrary.simpleMessage("Застосувати"), "applyCodeTitle": @@ -460,7 +460,7 @@ class MessageLookup extends MessageLookupByLibrary { "autoPairDesc": MessageLookupByLibrary.simpleMessage( "Автоматичне створення пари працює лише з пристроями, що підтримують Chromecast."), "available": MessageLookupByLibrary.simpleMessage("Доступно"), - "availableStorageSpace": m17, + "availableStorageSpace": m18, "backedUpFolders": MessageLookupByLibrary.simpleMessage("Резервне копіювання тек"), "backup": MessageLookupByLibrary.simpleMessage("Резервне копіювання"), @@ -497,7 +497,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Скасувати відновлення"), "cancelAccountRecoveryBody": MessageLookupByLibrary.simpleMessage( "Ви впевнені, що хочете скасувати відновлення?"), - "cancelOtherSubscription": m18, + "cancelOtherSubscription": m19, "cancelSubscription": MessageLookupByLibrary.simpleMessage("Скасувати передплату"), "cannotAddMorePhotosAfterBecomingViewer": m3, @@ -549,7 +549,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Отримайте безплатне сховище"), "claimMore": MessageLookupByLibrary.simpleMessage("Отримайте більше!"), "claimed": MessageLookupByLibrary.simpleMessage("Отримано"), - "claimedStorageSoFar": m19, + "claimedStorageSoFar": m20, "cleanUncategorized": MessageLookupByLibrary.simpleMessage("Очистити «Без категорії»"), "cleanUncategorizedDescription": MessageLookupByLibrary.simpleMessage( @@ -579,12 +579,12 @@ class MessageLookup extends MessageLookupByLibrary { "Створіть посилання, щоб дозволити людям додавати й переглядати фотографії у вашому спільному альбомі без використання застосунку Ente або облікового запису. Чудово підходить для збору фотографій з подій."), "collaborativeLink": MessageLookupByLibrary.simpleMessage("Спільне посилання"), - "collaborativeLinkCreatedFor": m20, + "collaborativeLinkCreatedFor": m21, "collaborator": MessageLookupByLibrary.simpleMessage("Співавтор"), "collaboratorsCanAddPhotosAndVideosToTheSharedAlbum": MessageLookupByLibrary.simpleMessage( "Співавтори можуть додавати фотографії та відео до спільного альбому."), - "collaboratorsSuccessfullyAdded": m21, + "collaboratorsSuccessfullyAdded": m22, "collageLayout": MessageLookupByLibrary.simpleMessage("Макет"), "collageSaved": MessageLookupByLibrary.simpleMessage("Колаж збережено до галереї"), @@ -602,7 +602,7 @@ class MessageLookup extends MessageLookupByLibrary { "Ви впевнені, що хочете вимкнути двоетапну перевірку?"), "confirmAccountDeletion": MessageLookupByLibrary.simpleMessage( "Підтвердьте видалення облікового запису"), - "confirmAddingTrustedContact": m22, + "confirmAddingTrustedContact": m23, "confirmDeletePrompt": MessageLookupByLibrary.simpleMessage( "Так, я хочу безповоротно видалити цей обліковий запис та його дані з усіх застосунків."), "confirmPassword": @@ -615,10 +615,10 @@ class MessageLookup extends MessageLookupByLibrary { "Підтвердіть ваш ключ відновлення"), "connectToDevice": MessageLookupByLibrary.simpleMessage("Під\'єднатися до пристрою"), - "contactFamilyAdmin": m23, + "contactFamilyAdmin": m24, "contactSupport": MessageLookupByLibrary.simpleMessage( "Звернутися до служби підтримки"), - "contactToManageSubscription": m24, + "contactToManageSubscription": m25, "contacts": MessageLookupByLibrary.simpleMessage("Контакти"), "contents": MessageLookupByLibrary.simpleMessage("Вміст"), "continueLabel": MessageLookupByLibrary.simpleMessage("Продовжити"), @@ -665,7 +665,7 @@ class MessageLookup extends MessageLookupByLibrary { "currentlyRunning": MessageLookupByLibrary.simpleMessage("зараз працює"), "custom": MessageLookupByLibrary.simpleMessage("Власне"), - "customEndpoint": m25, + "customEndpoint": m26, "darkTheme": MessageLookupByLibrary.simpleMessage("Темна"), "dayToday": MessageLookupByLibrary.simpleMessage("Сьогодні"), "dayYesterday": MessageLookupByLibrary.simpleMessage("Вчора"), @@ -703,11 +703,11 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Видалити з пристрою"), "deleteFromEnte": MessageLookupByLibrary.simpleMessage("Видалити з Ente"), - "deleteItemCount": m26, + "deleteItemCount": m27, "deleteLocation": MessageLookupByLibrary.simpleMessage("Видалити розташування"), "deletePhotos": MessageLookupByLibrary.simpleMessage("Видалити фото"), - "deleteProgress": m27, + "deleteProgress": m28, "deleteReason1": MessageLookupByLibrary.simpleMessage( "Мені бракує ключової функції"), "deleteReason2": MessageLookupByLibrary.simpleMessage( @@ -746,7 +746,7 @@ class MessageLookup extends MessageLookupByLibrary { "Переглядачі все ще можуть робити знімки екрана або зберігати копію ваших фотографій за допомогою зовнішніх інструментів"), "disableDownloadWarningTitle": MessageLookupByLibrary.simpleMessage("Зверніть увагу"), - "disableLinkMessage": m28, + "disableLinkMessage": m29, "disableTwofactor": MessageLookupByLibrary.simpleMessage( "Вимкнути двоетапну перевірку"), "disablingTwofactorAuthentication": @@ -789,9 +789,9 @@ class MessageLookup extends MessageLookupByLibrary { "downloadFailed": MessageLookupByLibrary.simpleMessage("Не вдалося завантажити"), "downloading": MessageLookupByLibrary.simpleMessage("Завантаження..."), - "dropSupportEmail": m29, - "duplicateFileCountWithStorageSaved": m30, - "duplicateItemsGroup": m31, + "dropSupportEmail": m30, + "duplicateFileCountWithStorageSaved": m31, + "duplicateItemsGroup": m32, "edit": MessageLookupByLibrary.simpleMessage("Редагувати"), "editLocation": MessageLookupByLibrary.simpleMessage("Змінити розташування"), @@ -805,8 +805,8 @@ class MessageLookup extends MessageLookupByLibrary { "eligible": MessageLookupByLibrary.simpleMessage("придатний"), "email": MessageLookupByLibrary.simpleMessage("Адреса електронної пошти"), - "emailChangedTo": m32, - "emailNoEnteAccount": m33, + "emailChangedTo": m33, + "emailNoEnteAccount": m35, "emailVerificationToggle": MessageLookupByLibrary.simpleMessage("Підтвердження через пошту"), "emailYourLogs": MessageLookupByLibrary.simpleMessage( @@ -888,7 +888,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Експортувати дані"), "extraPhotosFound": MessageLookupByLibrary.simpleMessage( "Знайдено додаткові фотографії"), - "extraPhotosFoundFor": m34, + "extraPhotosFoundFor": m36, "faceNotClusteredYet": MessageLookupByLibrary.simpleMessage( "Обличчя ще не згруповані, поверніться пізніше"), "faceRecognition": @@ -938,8 +938,8 @@ class MessageLookup extends MessageLookupByLibrary { "fileTypes": MessageLookupByLibrary.simpleMessage("Типи файлів"), "fileTypesAndNames": MessageLookupByLibrary.simpleMessage("Типи та назви файлів"), - "filesBackedUpFromDevice": m35, - "filesBackedUpInAlbum": m36, + "filesBackedUpFromDevice": m37, + "filesBackedUpInAlbum": m38, "filesDeleted": MessageLookupByLibrary.simpleMessage("Файли видалено"), "filesSavedToGallery": MessageLookupByLibrary.simpleMessage("Файли збережено до галереї"), @@ -960,21 +960,21 @@ class MessageLookup extends MessageLookupByLibrary { "Безплатне сховище можна використовувати"), "freeTrial": MessageLookupByLibrary.simpleMessage("Безплатний пробний період"), - "freeTrialValidTill": m37, - "freeUpAccessPostDelete": m38, - "freeUpAmount": m39, + "freeTrialValidTill": m39, + "freeUpAccessPostDelete": m40, + "freeUpAmount": m41, "freeUpDeviceSpace": MessageLookupByLibrary.simpleMessage("Звільніть місце на пристрої"), "freeUpDeviceSpaceDesc": MessageLookupByLibrary.simpleMessage( "Збережіть місце на вашому пристрої, очистивши файли, які вже збережено."), "freeUpSpace": MessageLookupByLibrary.simpleMessage("Звільнити місце"), - "freeUpSpaceSaving": m40, + "freeUpSpaceSaving": m42, "galleryMemoryLimitInfo": MessageLookupByLibrary.simpleMessage( "До 1000 спогадів, показаних у галереї"), "general": MessageLookupByLibrary.simpleMessage("Загальні"), "generatingEncryptionKeys": MessageLookupByLibrary.simpleMessage( "Створення ключів шифрування..."), - "genericProgress": m41, + "genericProgress": m43, "goToSettings": MessageLookupByLibrary.simpleMessage("Перейти до налаштувань"), "googlePlayId": MessageLookupByLibrary.simpleMessage("Google Play ID"), @@ -1055,7 +1055,7 @@ class MessageLookup extends MessageLookupByLibrary { "itLooksLikeSomethingWentWrongPleaseRetryAfterSome": MessageLookupByLibrary.simpleMessage( "Схоже, що щось пішло не так. Спробуйте ще раз через деякий час. Якщо помилка не зникне, зв\'яжіться з нашою командою підтримки."), - "itemCount": m42, + "itemCount": m44, "itemsShowTheNumberOfDaysRemainingBeforePermanentDeletion": MessageLookupByLibrary.simpleMessage( "Елементи показують кількість днів, що залишилися до остаточного видалення"), @@ -1079,7 +1079,7 @@ class MessageLookup extends MessageLookupByLibrary { "legacy": MessageLookupByLibrary.simpleMessage("Спадок"), "legacyAccounts": MessageLookupByLibrary.simpleMessage("Облікові записи «Спадку»"), - "legacyInvite": m43, + "legacyInvite": m45, "legacyPageDesc": MessageLookupByLibrary.simpleMessage( "«Спадок» дозволяє довіреним контактам отримати доступ до вашого облікового запису під час вашої відсутності."), "legacyPageDesc2": MessageLookupByLibrary.simpleMessage( @@ -1092,7 +1092,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Досягнуто ліміту пристроїв"), "linkEnabled": MessageLookupByLibrary.simpleMessage("Увімкнено"), "linkExpired": MessageLookupByLibrary.simpleMessage("Закінчився"), - "linkExpiresOn": m44, + "linkExpiresOn": m46, "linkExpiry": MessageLookupByLibrary.simpleMessage( "Термін дії посилання закінчився"), "linkHasExpired": @@ -1219,12 +1219,12 @@ class MessageLookup extends MessageLookupByLibrary { "moreDetails": MessageLookupByLibrary.simpleMessage("Детальніше"), "mostRecent": MessageLookupByLibrary.simpleMessage("Останні"), "mostRelevant": MessageLookupByLibrary.simpleMessage("Найактуальніші"), - "moveItem": m45, + "moveItem": m49, "moveToAlbum": MessageLookupByLibrary.simpleMessage("Перемістити до альбому"), "moveToHiddenAlbum": MessageLookupByLibrary.simpleMessage( "Перемістити до прихованого альбому"), - "movedSuccessfullyTo": m46, + "movedSuccessfullyTo": m50, "movedToTrash": MessageLookupByLibrary.simpleMessage("Переміщено у смітник"), "movingFilesToAlbum": MessageLookupByLibrary.simpleMessage( @@ -1276,10 +1276,10 @@ class MessageLookup extends MessageLookupByLibrary { "noResults": MessageLookupByLibrary.simpleMessage("Немає результатів"), "noResultsFound": MessageLookupByLibrary.simpleMessage("Нічого не знайдено"), - "noSuggestionsForPerson": m47, + "noSuggestionsForPerson": m51, "noSystemLockFound": MessageLookupByLibrary.simpleMessage( "Не знайдено системного блокування"), - "notPersonLabel": m48, + "notPersonLabel": m52, "nothingSharedWithYouYet": MessageLookupByLibrary.simpleMessage( "Поки що з вами ніхто не поділився"), "nothingToSeeHere": MessageLookupByLibrary.simpleMessage( @@ -1289,7 +1289,7 @@ class MessageLookup extends MessageLookupByLibrary { "onDevice": MessageLookupByLibrary.simpleMessage("На пристрої"), "onEnte": MessageLookupByLibrary.simpleMessage("В Ente"), - "onlyFamilyAdminCanChangeCode": m49, + "onlyFamilyAdminCanChangeCode": m53, "onlyThem": MessageLookupByLibrary.simpleMessage("Тільки вони"), "oops": MessageLookupByLibrary.simpleMessage("От халепа"), "oopsCouldNotSaveEdits": MessageLookupByLibrary.simpleMessage( @@ -1313,8 +1313,6 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Або об\'єднати з наявними"), "orPickAnExistingOne": MessageLookupByLibrary.simpleMessage("Або виберіть наявну"), - "orPickFromYourContacts": - MessageLookupByLibrary.simpleMessage("or pick from your contacts"), "pair": MessageLookupByLibrary.simpleMessage("Створити пару"), "pairWithPin": MessageLookupByLibrary.simpleMessage("Під’єднатися через PIN-код"), @@ -1342,7 +1340,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Не вдалося оплатити"), "paymentFailedMessage": MessageLookupByLibrary.simpleMessage( "На жаль, ваш платіж не вдався. Зв\'яжіться зі службою підтримки і ми вам допоможемо!"), - "paymentFailedTalkToProvider": m50, + "paymentFailedTalkToProvider": m54, "pendingItems": MessageLookupByLibrary.simpleMessage("Елементи на розгляді"), "pendingSync": @@ -1366,14 +1364,14 @@ class MessageLookup extends MessageLookupByLibrary { "photosAddedByYouWillBeRemovedFromTheAlbum": MessageLookupByLibrary.simpleMessage( "Додані вами фотографії будуть видалені з альбому"), - "photosCount": m51, + "photosCount": m55, "pickCenterPoint": MessageLookupByLibrary.simpleMessage("Вкажіть центральну точку"), "pinAlbum": MessageLookupByLibrary.simpleMessage("Закріпити альбом"), "pinLock": MessageLookupByLibrary.simpleMessage("Блокування PIN-кодом"), "playOnTv": MessageLookupByLibrary.simpleMessage("Відтворити альбом на ТБ"), - "playStoreFreeTrialValidTill": m52, + "playStoreFreeTrialValidTill": m56, "playstoreSubscription": MessageLookupByLibrary.simpleMessage("Передплата Play Store"), "pleaseCheckYourInternetConnectionAndTryAgain": @@ -1385,14 +1383,14 @@ class MessageLookup extends MessageLookupByLibrary { "pleaseContactSupportIfTheProblemPersists": MessageLookupByLibrary.simpleMessage( "Зверніться до служби підтримки, якщо проблема не зникне"), - "pleaseEmailUsAt": m53, + "pleaseEmailUsAt": m57, "pleaseGrantPermissions": MessageLookupByLibrary.simpleMessage("Надайте дозволи"), "pleaseLoginAgain": MessageLookupByLibrary.simpleMessage("Увійдіть знову"), "pleaseSelectQuickLinksToRemove": MessageLookupByLibrary.simpleMessage( "Виберіть посилання для видалення"), - "pleaseSendTheLogsTo": m54, + "pleaseSendTheLogsTo": m58, "pleaseTryAgain": MessageLookupByLibrary.simpleMessage("Спробуйте ще раз"), "pleaseVerifyTheCodeYouHaveEntered": @@ -1419,7 +1417,7 @@ class MessageLookup extends MessageLookupByLibrary { "privateSharing": MessageLookupByLibrary.simpleMessage("Приватне поширення"), "proceed": MessageLookupByLibrary.simpleMessage("Продовжити"), - "processingImport": m55, + "processingImport": m59, "publicLinkCreated": MessageLookupByLibrary.simpleMessage("Публічне посилання створено"), "publicLinkEnabled": MessageLookupByLibrary.simpleMessage( @@ -1430,7 +1428,7 @@ class MessageLookup extends MessageLookupByLibrary { "rateTheApp": MessageLookupByLibrary.simpleMessage("Оцініть застосунок"), "rateUs": MessageLookupByLibrary.simpleMessage("Оцініть нас"), - "rateUsOnStore": m56, + "rateUsOnStore": m60, "recover": MessageLookupByLibrary.simpleMessage("Відновити"), "recoverAccount": MessageLookupByLibrary.simpleMessage("Відновити обліковий запис"), @@ -1439,7 +1437,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Відновити обліковий запис"), "recoveryInitiated": MessageLookupByLibrary.simpleMessage("Почато відновлення"), - "recoveryInitiatedDesc": m57, + "recoveryInitiatedDesc": m62, "recoveryKey": MessageLookupByLibrary.simpleMessage("Ключ відновлення"), "recoveryKeyCopiedToClipboard": MessageLookupByLibrary.simpleMessage( "Ключ відновлення скопійовано в буфер обміну"), @@ -1453,12 +1451,12 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Ключ відновлення перевірено"), "recoveryKeyVerifyReason": MessageLookupByLibrary.simpleMessage( "Ключ відновлення — це єдиний спосіб відновити фотографії, якщо ви забули пароль. Ви можете знайти свій ключ в розділі «Налаштування» > «Обліковий запис».\n\nВведіть ключ відновлення тут, щоб перевірити, чи правильно ви його зберегли."), - "recoveryReady": m58, + "recoveryReady": m63, "recoverySuccessful": MessageLookupByLibrary.simpleMessage("Відновлення успішне!"), "recoveryWarning": MessageLookupByLibrary.simpleMessage( "Довірений контакт намагається отримати доступ до вашого облікового запису"), - "recoveryWarningBody": m59, + "recoveryWarningBody": m64, "recreatePasswordBody": MessageLookupByLibrary.simpleMessage( "Ваш пристрій недостатньо потужний для перевірки пароля, але ми можемо відновити його таким чином, щоб він працював на всіх пристроях.\n\nУвійдіть за допомогою ключа відновлення та відновіть свій пароль (за бажанням ви можете використати той самий ключ знову)."), "recreatePasswordTitle": @@ -1474,7 +1472,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("1. Дайте цей код друзям"), "referralStep2": MessageLookupByLibrary.simpleMessage( "2. Вони оформлюють передплату"), - "referralStep3": m60, + "referralStep3": m65, "referrals": MessageLookupByLibrary.simpleMessage("Реферали"), "referralsAreCurrentlyPaused": MessageLookupByLibrary.simpleMessage("Реферали зараз призупинені"), @@ -1506,7 +1504,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Вилучити посилання"), "removeParticipant": MessageLookupByLibrary.simpleMessage("Видалити учасника"), - "removeParticipantBody": m61, + "removeParticipantBody": m66, "removePersonLabel": MessageLookupByLibrary.simpleMessage("Видалити мітку особи"), "removePublicLink": @@ -1528,7 +1526,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Перейменувати файл"), "renewSubscription": MessageLookupByLibrary.simpleMessage("Поновити передплату"), - "renewsOn": m62, + "renewsOn": m67, "reportABug": MessageLookupByLibrary.simpleMessage("Повідомити про помилку"), "reportBug": @@ -1608,8 +1606,8 @@ class MessageLookup extends MessageLookupByLibrary { "Запросіть людей, і ви побачите всі фотографії, якими вони поділилися, тут"), "searchPersonsEmptySection": MessageLookupByLibrary.simpleMessage( "Люди будуть показані тут після завершення оброблення та синхронізації"), - "searchResultCount": m63, - "searchSectionsLengthMismatch": m64, + "searchResultCount": m68, + "searchSectionsLengthMismatch": m69, "security": MessageLookupByLibrary.simpleMessage("Безпека"), "seePublicAlbumLinksInApp": MessageLookupByLibrary.simpleMessage( "Посилання на публічні альбоми в застосунку"), @@ -1642,7 +1640,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage( "Вибрані елементи будуть видалені з усіх альбомів і переміщені в смітник."), "selectedPhotos": m6, - "selectedPhotosWithYours": m65, + "selectedPhotosWithYours": m70, "send": MessageLookupByLibrary.simpleMessage("Надіслати"), "sendEmail": MessageLookupByLibrary.simpleMessage( "Надіслати електронного листа"), @@ -1679,16 +1677,16 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Поділитися альбомом зараз"), "shareLink": MessageLookupByLibrary.simpleMessage("Поділитися посиланням"), - "shareMyVerificationID": m66, + "shareMyVerificationID": m71, "shareOnlyWithThePeopleYouWant": MessageLookupByLibrary.simpleMessage( "Поділіться тільки з тими людьми, якими ви хочете"), "shareTextConfirmOthersVerificationID": m7, "shareTextRecommendUsingEnte": MessageLookupByLibrary.simpleMessage( "Завантажте Ente для того, щоб легко поділитися фотографіями оригінальної якості та відео\n\nhttps://ente.io"), - "shareTextReferralCode": m67, + "shareTextReferralCode": m72, "shareWithNonenteUsers": MessageLookupByLibrary.simpleMessage( "Поділитися з користувачами без Ente"), - "shareWithPeopleSectionTitle": m68, + "shareWithPeopleSectionTitle": m73, "shareYourFirstAlbum": MessageLookupByLibrary.simpleMessage( "Поділитися вашим першим альбомом"), "sharedAlbumSectionDescription": MessageLookupByLibrary.simpleMessage( @@ -1699,7 +1697,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Нові спільні фотографії"), "sharedPhotoNotificationsExplanation": MessageLookupByLibrary.simpleMessage( "Отримувати сповіщення, коли хтось додасть фото до спільного альбому, в якому ви перебуваєте"), - "sharedWith": m69, + "sharedWith": m74, "sharedWithMe": MessageLookupByLibrary.simpleMessage("Поділитися зі мною"), "sharedWithYou": @@ -1716,11 +1714,11 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Вийти на інших пристроях"), "signUpTerms": MessageLookupByLibrary.simpleMessage( "Я приймаю умови використання і політику приватності"), - "singleFileDeleteFromDevice": m70, + "singleFileDeleteFromDevice": m75, "singleFileDeleteHighlight": MessageLookupByLibrary.simpleMessage( "Воно буде видалено з усіх альбомів."), - "singleFileInBothLocalAndRemote": m71, - "singleFileInRemoteOnly": m72, + "singleFileInBothLocalAndRemote": m76, + "singleFileInRemoteOnly": m77, "skip": MessageLookupByLibrary.simpleMessage("Пропустити"), "social": MessageLookupByLibrary.simpleMessage("Соцмережі"), "someItemsAreInBothEnteAndYourDevice": @@ -1771,10 +1769,10 @@ class MessageLookup extends MessageLookupByLibrary { "storageInGB": m1, "storageLimitExceeded": MessageLookupByLibrary.simpleMessage("Перевищено ліміт сховища"), - "storageUsageInfo": m73, + "storageUsageInfo": m78, "strongStrength": MessageLookupByLibrary.simpleMessage("Надійний"), - "subAlreadyLinkedErrMessage": m74, - "subWillBeCancelledOn": m75, + "subAlreadyLinkedErrMessage": m79, + "subWillBeCancelledOn": m80, "subscribe": MessageLookupByLibrary.simpleMessage("Передплачувати"), "subscribeToEnableSharing": MessageLookupByLibrary.simpleMessage( "Вам потрібна активна передплата, щоб увімкнути спільне поширення."), @@ -1791,7 +1789,7 @@ class MessageLookup extends MessageLookupByLibrary { "suggestFeatures": MessageLookupByLibrary.simpleMessage("Запропонувати нові функції"), "support": MessageLookupByLibrary.simpleMessage("Підтримка"), - "syncProgress": m76, + "syncProgress": m81, "syncStopped": MessageLookupByLibrary.simpleMessage("Синхронізацію зупинено"), "syncing": MessageLookupByLibrary.simpleMessage("Синхронізуємо..."), @@ -1804,7 +1802,7 @@ class MessageLookup extends MessageLookupByLibrary { "Торкніться, щоби розблокувати"), "tapToUpload": MessageLookupByLibrary.simpleMessage("Натисніть, щоб завантажити"), - "tapToUploadIsIgnoredDue": m77, + "tapToUploadIsIgnoredDue": m82, "tempErrorContactSupportIfPersists": MessageLookupByLibrary.simpleMessage( "Схоже, що щось пішло не так. Спробуйте ще раз через деякий час. Якщо помилка не зникне, зв\'яжіться з нашою командою підтримки."), "terminate": MessageLookupByLibrary.simpleMessage("Припинити"), @@ -1843,7 +1841,7 @@ class MessageLookup extends MessageLookupByLibrary { "Ця поштова адреса вже використовується"), "thisImageHasNoExifData": MessageLookupByLibrary.simpleMessage( "Це зображення не має даних exif"), - "thisIsPersonVerificationId": m78, + "thisIsPersonVerificationId": m83, "thisIsYourVerificationId": MessageLookupByLibrary.simpleMessage( "Це ваш Ідентифікатор підтвердження"), "thisWillLogYouOutOfTheFollowingDevice": @@ -1868,11 +1866,11 @@ class MessageLookup extends MessageLookupByLibrary { "total": MessageLookupByLibrary.simpleMessage("всього"), "totalSize": MessageLookupByLibrary.simpleMessage("Загальний розмір"), "trash": MessageLookupByLibrary.simpleMessage("Смітник"), - "trashDaysLeft": m79, + "trashDaysLeft": m84, "trim": MessageLookupByLibrary.simpleMessage("Вирізати"), "trustedContacts": MessageLookupByLibrary.simpleMessage("Довірені контакти"), - "trustedInviteBody": m80, + "trustedInviteBody": m85, "tryAgain": MessageLookupByLibrary.simpleMessage("Спробувати знову"), "turnOnBackupForAutoUpload": MessageLookupByLibrary.simpleMessage( "Увімкніть резервну копію для автоматичного завантаження файлів, доданих до теки пристрою в Ente."), @@ -1890,7 +1888,7 @@ class MessageLookup extends MessageLookupByLibrary { "Двоетапну перевірку успішно скинуто"), "twofactorSetup": MessageLookupByLibrary.simpleMessage( "Налаштування двоетапної перевірки"), - "typeOfGallerGallerytypeIsNotSupportedForRename": m81, + "typeOfGallerGallerytypeIsNotSupportedForRename": m86, "unarchive": MessageLookupByLibrary.simpleMessage("Розархівувати"), "unarchiveAlbum": MessageLookupByLibrary.simpleMessage("Розархівувати альбом"), @@ -1913,10 +1911,10 @@ class MessageLookup extends MessageLookupByLibrary { "updatingFolderSelection": MessageLookupByLibrary.simpleMessage("Оновлення вибору теки..."), "upgrade": MessageLookupByLibrary.simpleMessage("Покращити"), - "uploadIsIgnoredDueToIgnorereason": m82, + "uploadIsIgnoredDueToIgnorereason": m87, "uploadingFilesToAlbum": MessageLookupByLibrary.simpleMessage( "Завантажуємо файли до альбому..."), - "uploadingMultipleMemories": m83, + "uploadingMultipleMemories": m88, "uploadingSingleMemory": MessageLookupByLibrary.simpleMessage("Зберігаємо 1 спогад..."), "upto50OffUntil4thDec": @@ -1935,7 +1933,7 @@ class MessageLookup extends MessageLookupByLibrary { "useSelectedPhoto": MessageLookupByLibrary.simpleMessage("Використати вибране фото"), "usedSpace": MessageLookupByLibrary.simpleMessage("Використано місця"), - "validTill": m84, + "validTill": m89, "verificationFailedPleaseTryAgain": MessageLookupByLibrary.simpleMessage( "Перевірка не вдалася, спробуйте ще раз"), @@ -1944,7 +1942,7 @@ class MessageLookup extends MessageLookupByLibrary { "verify": MessageLookupByLibrary.simpleMessage("Підтвердити"), "verifyEmail": MessageLookupByLibrary.simpleMessage("Підтвердити пошту"), - "verifyEmailID": m85, + "verifyEmailID": m90, "verifyIDLabel": MessageLookupByLibrary.simpleMessage("Підтвердження"), "verifyPasskey": MessageLookupByLibrary.simpleMessage("Підтвердити ключ доступу"), @@ -1971,7 +1969,7 @@ class MessageLookup extends MessageLookupByLibrary { "viewRecoveryKey": MessageLookupByLibrary.simpleMessage( "Переглянути ключ відновлення"), "viewer": MessageLookupByLibrary.simpleMessage("Глядач"), - "viewersSuccessfullyAdded": m86, + "viewersSuccessfullyAdded": m91, "visitWebToManage": MessageLookupByLibrary.simpleMessage( "Відвідайте web.ente.io, щоб керувати передплатою"), "waitingForVerification": @@ -1992,7 +1990,7 @@ class MessageLookup extends MessageLookupByLibrary { "Довірений контакт може допомогти у відновленні ваших даних."), "yearShort": MessageLookupByLibrary.simpleMessage("рік"), "yearly": MessageLookupByLibrary.simpleMessage("Щороку"), - "yearsAgo": m87, + "yearsAgo": m92, "yes": MessageLookupByLibrary.simpleMessage("Так"), "yesCancel": MessageLookupByLibrary.simpleMessage("Так, скасувати"), "yesConvertToViewer": @@ -2025,7 +2023,7 @@ class MessageLookup extends MessageLookupByLibrary { "Ви не можете поділитися із собою"), "youDontHaveAnyArchivedItems": MessageLookupByLibrary.simpleMessage( "У вас немає жодних архівних елементів."), - "youHaveSuccessfullyFreedUp": m88, + "youHaveSuccessfullyFreedUp": m93, "yourAccountHasBeenDeleted": MessageLookupByLibrary.simpleMessage( "Ваш обліковий запис видалено"), "yourMap": MessageLookupByLibrary.simpleMessage("Ваша мапа"), @@ -2046,9 +2044,6 @@ class MessageLookup extends MessageLookupByLibrary { "Вашу передплату успішно оновлено"), "yourVerificationCodeHasExpired": MessageLookupByLibrary.simpleMessage( "Термін дії коду підтвердження минув"), - "youveNoDuplicateFilesThatCanBeCleared": - MessageLookupByLibrary.simpleMessage( - "У вас немає дублікатів файлів, які можна очистити"), "youveNoFilesInThisAlbumThatCanBeDeleted": MessageLookupByLibrary.simpleMessage( "У цьому альбомі немає файлів, які можуть бути видалені"), diff --git a/mobile/lib/generated/intl/messages_vi.dart b/mobile/lib/generated/intl/messages_vi.dart index 647d8d1dd7..a6e6124926 100644 --- a/mobile/lib/generated/intl/messages_vi.dart +++ b/mobile/lib/generated/intl/messages_vi.dart @@ -20,37 +20,37 @@ typedef String MessageIfAbsent(String messageStr, List args); class MessageLookup extends MessageLookupByLibrary { String get localeName => 'vi'; - static String m9(count) => + static String m10(count) => "${Intl.plural(count, zero: 'Thêm cộng tác viên', one: 'Thêm cộng tác viên', other: 'Thêm cộng tác viên')}"; - static String m10(count) => + static String m11(count) => "${Intl.plural(count, one: 'Thêm mục', other: 'Thêm các mục')}"; - static String m11(storageAmount, endDate) => + static String m12(storageAmount, endDate) => "Gói bổ sung ${storageAmount} của bạn có hiệu lực đến ${endDate}"; - static String m12(count) => + static String m13(count) => "${Intl.plural(count, zero: 'Thêm người xem', one: 'Thêm người xem', other: 'Thêm người xem')}"; - static String m13(emailOrName) => "Được thêm bởi ${emailOrName}"; + static String m14(emailOrName) => "Được thêm bởi ${emailOrName}"; - static String m14(albumName) => "Đã thêm thành công vào ${albumName}"; + static String m15(albumName) => "Đã thêm thành công vào ${albumName}"; - static String m15(count) => + static String m16(count) => "${Intl.plural(count, zero: 'Không có người tham gia', one: '1 người tham gia', other: '${count} Người tham gia')}"; - static String m16(versionValue) => "Phiên bản: ${versionValue}"; + static String m17(versionValue) => "Phiên bản: ${versionValue}"; - static String m17(freeAmount, storageUnit) => + static String m18(freeAmount, storageUnit) => "${freeAmount} ${storageUnit} còn trống"; - static String m18(paymentProvider) => + static String m19(paymentProvider) => "Vui lòng hủy đăng ký hiện tại của bạn từ ${paymentProvider} trước"; static String m3(user) => "${user} sẽ không thể thêm ảnh mới vào album này\n\nHọ vẫn có thể xóa các ảnh đã thêm trước đó"; - static String m19(isFamilyMember, storageAmountInGb) => + static String m20(isFamilyMember, storageAmountInGb) => "${Intl.select(isFamilyMember, { 'true': 'Gia đình bạn đã yêu cầu ${storageAmountInGb} GB cho đến nay', @@ -58,213 +58,213 @@ class MessageLookup extends MessageLookupByLibrary { 'other': 'Bạn đã yêu cầu ${storageAmountInGb} GB cho đến nay!', })}"; - static String m20(albumName) => + static String m21(albumName) => "Liên kết hợp tác đã được tạo cho ${albumName}"; - static String m21(count) => + static String m22(count) => "${Intl.plural(count, zero: 'Đã thêm 0 cộng tác viên', one: 'Đã thêm 1 cộng tác viên', other: 'Đã thêm ${count} cộng tác viên')}"; - static String m22(email, numOfDays) => + static String m23(email, numOfDays) => "Bạn sắp thêm ${email} làm liên hệ tin cậy. Họ sẽ có thể khôi phục tài khoản của bạn nếu bạn không hoạt động trong ${numOfDays} ngày."; - static String m23(familyAdminEmail) => + static String m24(familyAdminEmail) => "Vui lòng liên hệ với ${familyAdminEmail} để quản lý đăng ký của bạn"; - static String m24(provider) => + static String m25(provider) => "Vui lòng liên hệ với chúng tôi tại support@ente.io để quản lý đăng ký ${provider} của bạn."; - static String m25(endpoint) => "Đã kết nối với ${endpoint}"; + static String m26(endpoint) => "Đã kết nối với ${endpoint}"; - static String m26(count) => + static String m27(count) => "${Intl.plural(count, one: 'Xóa ${count} mục', other: 'Xóa ${count} mục')}"; - static String m27(currentlyDeleting, totalCount) => + static String m28(currentlyDeleting, totalCount) => "Đang xóa ${currentlyDeleting} / ${totalCount}"; - static String m28(albumName) => + static String m29(albumName) => "Điều này sẽ xóa liên kết công khai để truy cập \"${albumName}\"."; - static String m29(supportEmail) => + static String m30(supportEmail) => "Vui lòng gửi email đến ${supportEmail} từ địa chỉ email đã đăng ký của bạn"; - static String m30(count, storageSaved) => + static String m31(count, storageSaved) => "Bạn đã dọn dẹp ${Intl.plural(count, one: '${count} tệp trùng lặp', other: '${count} tệp trùng lặp')}, tiết kiệm (${storageSaved}!)"; - static String m31(count, formattedSize) => + static String m32(count, formattedSize) => "${count} tệp, ${formattedSize} mỗi tệp"; - static String m32(newEmail) => "Email đã được thay đổi thành ${newEmail}"; + static String m33(newEmail) => "Email đã được thay đổi thành ${newEmail}"; - static String m33(email) => + static String m35(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 m34(text) => "Extra photos found for ${text}"; + static String m36(text) => "Extra photos found for ${text}"; - static String m35(count, formattedNumber) => + static String m37(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 m36(count, formattedNumber) => + static String m38(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 m4(storageAmountInGB) => "${storageAmountInGB} GB mỗi khi ai đó đăng ký gói trả phí và áp dụng mã của bạn"; - static String m37(endDate) => "Dùng thử miễn phí có hiệu lực đến ${endDate}"; + static String m39(endDate) => "Dùng thử miễn phí có hiệu lực đến ${endDate}"; - static String m38(count) => + static String m40(count) => "Bạn vẫn có thể truy cập ${Intl.plural(count, one: 'nó', other: 'chúng')} trên Ente miễn là bạn có một đăng ký hoạt động"; - static String m39(sizeInMBorGB) => "Giải phóng ${sizeInMBorGB}"; + static String m41(sizeInMBorGB) => "Giải phóng ${sizeInMBorGB}"; - static String m40(count, formattedSize) => + static String m42(count, formattedSize) => "${Intl.plural(count, one: 'Nó có thể được xóa khỏi thiết bị để giải phóng ${formattedSize}', other: 'Chúng có thể được xóa khỏi thiết bị để giải phóng ${formattedSize}')}"; - static String m41(currentlyProcessing, totalCount) => + static String m43(currentlyProcessing, totalCount) => "Đang xử lý ${currentlyProcessing} / ${totalCount}"; - static String m42(count) => + static String m44(count) => "${Intl.plural(count, one: '${count} mục', other: '${count} mục')}"; - static String m43(email) => + static String m45(email) => "${email} đã mời bạn trở thành một liên hệ tin cậy"; - static String m44(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 m5(count, formattedCount) => "${Intl.plural(count, zero: 'không có kỷ niệm', one: '${formattedCount} kỷ niệm', other: '${formattedCount} kỷ niệm')}"; - static String m45(count) => + static String m49(count) => "${Intl.plural(count, one: 'Di chuyển mục', other: 'Di chuyển các mục')}"; - static String m46(albumName) => "Đã di chuyển thành công đến ${albumName}"; + static String m50(albumName) => "Đã di chuyển thành công đến ${albumName}"; - static String m47(personName) => "Không có gợi ý cho ${personName}"; + static String m51(personName) => "Không có gợi ý cho ${personName}"; - static String m48(name) => "Không phải ${name}?"; + static String m52(name) => "Không phải ${name}?"; - static String m49(familyAdminEmail) => + static String m53(familyAdminEmail) => "Vui lòng liên hệ ${familyAdminEmail} để thay đổi mã của bạn."; static String m0(passwordStrengthValue) => "Độ mạnh mật khẩu: ${passwordStrengthValue}"; - static String m50(providerName) => + static String m54(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 m51(count) => + static String m55(count) => "${Intl.plural(count, zero: 'không có hình ảnh', one: '1 hình ảnh', other: '${count} hình ảnh')}"; - static String m52(endDate) => + static String m56(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 m53(toEmail) => + static String m57(toEmail) => "Vui lòng gửi email cho chúng tôi tại ${toEmail}"; - static String m54(toEmail) => "Vui lòng gửi nhật ký đến \n${toEmail}"; + static String m58(toEmail) => "Vui lòng gửi nhật ký đến \n${toEmail}"; - static String m55(folderName) => "Đang xử lý ${folderName}..."; + static String m59(folderName) => "Đang xử lý ${folderName}..."; - static String m56(storeName) => "Đánh giá chúng tôi trên ${storeName}"; + static String m60(storeName) => "Đánh giá chúng tôi trên ${storeName}"; - static String m57(days, email) => + static String m62(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 m58(email) => + static String m63(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 m59(email) => + static String m64(email) => "${email} đang cố gắng khôi phục tài khoản của bạn."; - static String m60(storageInGB) => + static String m65(storageInGB) => "3. Cả hai bạn đều nhận ${storageInGB} GB* miễn phí"; - static String m61(userEmail) => + static String m66(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 m62(endDate) => "Đăng ký sẽ được gia hạn vào ${endDate}"; + static String m67(endDate) => "Đăng ký sẽ được gia hạn vào ${endDate}"; - static String m63(count) => + static String m68(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 m64(snapshotLength, searchLength) => + static String m69(snapshotLength, searchLength) => "Độ dài các phần không khớp: ${snapshotLength} != ${searchLength}"; static String m6(count) => "${count} đã chọn"; - static String m65(count, yourCount) => + static String m70(count, yourCount) => "${count} đã chọn (${yourCount} của bạn)"; - static String m66(verificationID) => + static String m71(verificationID) => "Đây là ID xác minh của tôi: ${verificationID} cho ente.io."; static String m7(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 m67(referralCode, referralStorageInGB) => + static String m72(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 m68(numberOfPeople) => + static String m73(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 m69(emailIDs) => "Chia sẻ với ${emailIDs}"; + static String m74(emailIDs) => "Chia sẻ với ${emailIDs}"; - static String m70(fileType) => + static String m75(fileType) => "Tệp ${fileType} này sẽ bị xóa khỏi thiết bị của bạn."; - static String m71(fileType) => + static String m76(fileType) => "Tệp ${fileType} này có trong cả Ente và thiết bị của bạn."; - static String m72(fileType) => "Tệp ${fileType} này sẽ bị xóa khỏi Ente."; + static String m77(fileType) => "Tệp ${fileType} này sẽ bị xóa khỏi Ente."; static String m1(storageAmountInGB) => "${storageAmountInGB} GB"; - static String m73( + static String m78( usedAmount, usedStorageUnit, totalAmount, totalStorageUnit) => "${usedAmount} ${usedStorageUnit} trong tổng số ${totalAmount} ${totalStorageUnit} đã sử dụng"; - static String m74(id) => + static String m79(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 m75(endDate) => "Đăng ký của bạn sẽ bị hủy vào ${endDate}"; + static String m80(endDate) => "Đăng ký của bạn sẽ bị hủy vào ${endDate}"; - static String m76(completed, total) => + static String m81(completed, total) => "${completed}/${total} kỷ niệm đã được lưu giữ"; - static String m77(ignoreReason) => + static String m82(ignoreReason) => "Nhấn để tải lên, tải lên hiện tại bị bỏ qua do ${ignoreReason}"; static String m8(storageAmountInGB) => "Họ cũng nhận được ${storageAmountInGB} GB"; - static String m78(email) => "Đây là ID xác minh của ${email}"; + static String m83(email) => "Đây là ID xác minh của ${email}"; - static String m79(count) => + static String m84(count) => "${Intl.plural(count, zero: 'Soon', one: '1 day', other: '${count} days')}"; - static String m80(email) => + static String m85(email) => "Bạn đã được mời làm người liên hệ thừa kế bởi ${email}."; - static String m81(galleryType) => + static String m86(galleryType) => "Loại thư viện ${galleryType} không được hỗ trợ để đổi tên"; - static String m82(ignoreReason) => "Tải lên bị bỏ qua do ${ignoreReason}"; + static String m87(ignoreReason) => "Tải lên bị bỏ qua do ${ignoreReason}"; - static String m83(count) => "Đang lưu giữ ${count} kỷ niệm..."; + static String m88(count) => "Đang lưu giữ ${count} kỷ niệm..."; - static String m84(endDate) => "Có hiệu lực đến ${endDate}"; + static String m89(endDate) => "Có hiệu lực đến ${endDate}"; - static String m85(email) => "Xác minh ${email}"; + static String m90(email) => "Xác minh ${email}"; - static String m86(count) => + static String m91(count) => "${Intl.plural(count, zero: 'Đã thêm 0 người xem', one: 'Đã thêm 1 người xem', other: 'Đã thêm ${count} người xem')}"; static String m2(email) => "Chúng tôi đã gửi một email đến ${email}"; - static String m87(count) => + static String m92(count) => "${Intl.plural(count, one: '${count} năm trước', other: '${count} năm trước')}"; - static String m88(storageSaved) => + static String m93(storageSaved) => "Bạn đã giải phóng thành công ${storageSaved}!"; final messages = _notInlinedMessages(_notInlinedMessages); @@ -289,11 +289,11 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Thêm một email mới"), "addCollaborator": MessageLookupByLibrary.simpleMessage("Thêm cộng tác viên"), - "addCollaborators": m9, + "addCollaborators": m10, "addFiles": MessageLookupByLibrary.simpleMessage("Thêm tệp"), "addFromDevice": MessageLookupByLibrary.simpleMessage("Thêm từ thiết bị"), - "addItem": m10, + "addItem": m11, "addLocation": MessageLookupByLibrary.simpleMessage("Thêm vị trí"), "addLocationButton": MessageLookupByLibrary.simpleMessage("Thêm"), "addMore": MessageLookupByLibrary.simpleMessage("Thêm nữa"), @@ -304,7 +304,7 @@ class MessageLookup extends MessageLookupByLibrary { "addNewPerson": MessageLookupByLibrary.simpleMessage("Thêm người mới"), "addOnPageSubtitle": MessageLookupByLibrary.simpleMessage( "Chi tiết về tiện ích mở rộng"), - "addOnValidTill": m11, + "addOnValidTill": m12, "addOns": MessageLookupByLibrary.simpleMessage("Tiện ích mở rộng"), "addPhotos": MessageLookupByLibrary.simpleMessage("Thêm ảnh"), "addSelected": MessageLookupByLibrary.simpleMessage("Thêm đã chọn"), @@ -315,12 +315,12 @@ class MessageLookup extends MessageLookupByLibrary { "addTrustedContact": MessageLookupByLibrary.simpleMessage("Thêm liên hệ tin cậy"), "addViewer": MessageLookupByLibrary.simpleMessage("Thêm người xem"), - "addViewers": m12, + "addViewers": m13, "addYourPhotosNow": MessageLookupByLibrary.simpleMessage( "Thêm ảnh của bạn ngay bây giờ"), "addedAs": MessageLookupByLibrary.simpleMessage("Đã thêm như"), - "addedBy": m13, - "addedSuccessfullyTo": m14, + "addedBy": m14, + "addedSuccessfullyTo": m15, "addingToFavorites": MessageLookupByLibrary.simpleMessage( "Đang thêm vào mục yêu thích..."), "advanced": MessageLookupByLibrary.simpleMessage("Nâng cao"), @@ -331,7 +331,7 @@ class MessageLookup extends MessageLookupByLibrary { "after1Week": MessageLookupByLibrary.simpleMessage("Sau 1 tuần"), "after1Year": MessageLookupByLibrary.simpleMessage("Sau 1 năm"), "albumOwner": MessageLookupByLibrary.simpleMessage("Chủ sở hữu"), - "albumParticipantsCount": m15, + "albumParticipantsCount": m16, "albumTitle": MessageLookupByLibrary.simpleMessage("Tiêu đề album"), "albumUpdated": MessageLookupByLibrary.simpleMessage("Album đã được cập nhật"), @@ -380,7 +380,7 @@ class MessageLookup extends MessageLookupByLibrary { "appLock": MessageLookupByLibrary.simpleMessage("Khóa ứng dụng"), "appLockDescriptions": MessageLookupByLibrary.simpleMessage( "Chọn giữa màn hình khóa mặc định của thiết bị và màn hình khóa tùy chỉnh với PIN hoặc mật khẩu."), - "appVersion": m16, + "appVersion": m17, "appleId": MessageLookupByLibrary.simpleMessage("ID Apple"), "apply": MessageLookupByLibrary.simpleMessage("Áp dụng"), "applyCodeTitle": MessageLookupByLibrary.simpleMessage("Áp dụng mã"), @@ -462,7 +462,7 @@ class MessageLookup extends MessageLookupByLibrary { "autoPairDesc": MessageLookupByLibrary.simpleMessage( "Ghép nối tự động chỉ hoạt động với các thiết bị hỗ trợ Chromecast."), "available": MessageLookupByLibrary.simpleMessage("Có sẵn"), - "availableStorageSpace": m17, + "availableStorageSpace": m18, "backedUpFolders": MessageLookupByLibrary.simpleMessage("Thư mục đã sao lưu"), "backup": MessageLookupByLibrary.simpleMessage("Sao lưu"), @@ -502,7 +502,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Hủy khôi phục"), "cancelAccountRecoveryBody": MessageLookupByLibrary.simpleMessage( "Bạn có chắc chắn muốn hủy khôi phục không?"), - "cancelOtherSubscription": m18, + "cancelOtherSubscription": m19, "cancelSubscription": MessageLookupByLibrary.simpleMessage("Hủy đăng ký"), "cannotAddMorePhotosAfterBecomingViewer": m3, @@ -553,7 +553,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Yêu cầu lưu trữ miễn phí"), "claimMore": MessageLookupByLibrary.simpleMessage("Yêu cầu thêm!"), "claimed": MessageLookupByLibrary.simpleMessage("Đã yêu cầu"), - "claimedStorageSoFar": m19, + "claimedStorageSoFar": m20, "cleanUncategorized": MessageLookupByLibrary.simpleMessage("Dọn dẹp chưa phân loại"), "cleanUncategorizedDescription": MessageLookupByLibrary.simpleMessage( @@ -582,12 +582,12 @@ class MessageLookup extends MessageLookupByLibrary { "Tạo một liên kết để cho phép mọi người thêm và xem ảnh trong album chia sẻ của bạn mà không cần ứng dụng hoặc tài khoản Ente. Tuyệt vời để thu thập ảnh sự kiện."), "collaborativeLink": MessageLookupByLibrary.simpleMessage("Liên kết hợp tác"), - "collaborativeLinkCreatedFor": m20, + "collaborativeLinkCreatedFor": m21, "collaborator": MessageLookupByLibrary.simpleMessage("Cộng tác viên"), "collaboratorsCanAddPhotosAndVideosToTheSharedAlbum": MessageLookupByLibrary.simpleMessage( "Cộng tác viên có thể thêm ảnh và video vào album chia sẻ."), - "collaboratorsSuccessfullyAdded": m21, + "collaboratorsSuccessfullyAdded": m22, "collageLayout": MessageLookupByLibrary.simpleMessage("Bố cục"), "collageSaved": MessageLookupByLibrary.simpleMessage( "Ảnh ghép đã được lưu vào thư viện"), @@ -604,7 +604,7 @@ class MessageLookup extends MessageLookupByLibrary { "Bạn có chắc chắn muốn vô hiệu hóa xác thực hai yếu tố không?"), "confirmAccountDeletion": MessageLookupByLibrary.simpleMessage("Xác nhận xóa tài khoản"), - "confirmAddingTrustedContact": m22, + "confirmAddingTrustedContact": m23, "confirmDeletePrompt": MessageLookupByLibrary.simpleMessage( "Có, tôi muốn xóa vĩnh viễn tài khoản này và dữ liệu của nó trên tất cả các ứng dụng."), "confirmPassword": @@ -617,10 +617,10 @@ class MessageLookup extends MessageLookupByLibrary { "Xác nhận khóa khôi phục của bạn"), "connectToDevice": MessageLookupByLibrary.simpleMessage("Kết nối với thiết bị"), - "contactFamilyAdmin": m23, + "contactFamilyAdmin": m24, "contactSupport": MessageLookupByLibrary.simpleMessage("Liên hệ hỗ trợ"), - "contactToManageSubscription": m24, + "contactToManageSubscription": m25, "contacts": MessageLookupByLibrary.simpleMessage("Danh bạ"), "contents": MessageLookupByLibrary.simpleMessage("Nội dung"), "continueLabel": MessageLookupByLibrary.simpleMessage("Tiếp tục"), @@ -664,7 +664,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Sử dụng hiện tại là "), "currentlyRunning": MessageLookupByLibrary.simpleMessage("đang chạy"), "custom": MessageLookupByLibrary.simpleMessage("Tùy chỉnh"), - "customEndpoint": m25, + "customEndpoint": m26, "darkTheme": MessageLookupByLibrary.simpleMessage("Tối"), "dayToday": MessageLookupByLibrary.simpleMessage("Hôm nay"), "dayYesterday": MessageLookupByLibrary.simpleMessage("Hôm qua"), @@ -700,10 +700,10 @@ class MessageLookup extends MessageLookupByLibrary { "deleteFromDevice": MessageLookupByLibrary.simpleMessage("Xóa khỏi thiết bị"), "deleteFromEnte": MessageLookupByLibrary.simpleMessage("Xóa khỏi Ente"), - "deleteItemCount": m26, + "deleteItemCount": m27, "deleteLocation": MessageLookupByLibrary.simpleMessage("Xóa vị trí"), "deletePhotos": MessageLookupByLibrary.simpleMessage("Xóa ảnh"), - "deleteProgress": m27, + "deleteProgress": m28, "deleteReason1": MessageLookupByLibrary.simpleMessage( "Nó thiếu một tính năng quan trọng mà tôi cần"), "deleteReason2": MessageLookupByLibrary.simpleMessage( @@ -741,7 +741,7 @@ class MessageLookup extends MessageLookupByLibrary { "Người xem vẫn có thể chụp màn hình hoặc lưu bản sao ảnh của bạn bằng các công cụ bên ngoài"), "disableDownloadWarningTitle": MessageLookupByLibrary.simpleMessage("Xin lưu ý"), - "disableLinkMessage": m28, + "disableLinkMessage": m29, "disableTwofactor": MessageLookupByLibrary.simpleMessage( "Vô hiệu hóa xác thực hai yếu tố"), "disablingTwofactorAuthentication": @@ -783,9 +783,9 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Tải xuống thất bại"), "downloading": MessageLookupByLibrary.simpleMessage("Đang tải xuống..."), - "dropSupportEmail": m29, - "duplicateFileCountWithStorageSaved": m30, - "duplicateItemsGroup": m31, + "dropSupportEmail": m30, + "duplicateFileCountWithStorageSaved": m31, + "duplicateItemsGroup": m32, "edit": MessageLookupByLibrary.simpleMessage("Chỉnh sửa"), "editLocation": MessageLookupByLibrary.simpleMessage("Chỉnh sửa vị trí"), @@ -801,8 +801,8 @@ class MessageLookup extends MessageLookupByLibrary { "email": MessageLookupByLibrary.simpleMessage("Email"), "emailAlreadyRegistered": MessageLookupByLibrary.simpleMessage("Email đã được đăng kí."), - "emailChangedTo": m32, - "emailNoEnteAccount": m33, + "emailChangedTo": m33, + "emailNoEnteAccount": m35, "emailNotRegistered": MessageLookupByLibrary.simpleMessage("Email chưa được đăng kí."), "emailVerificationToggle": @@ -883,7 +883,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": m34, + "extraPhotosFoundFor": m36, "faceNotClusteredYet": MessageLookupByLibrary.simpleMessage( "Khuôn mặt chưa được phân cụm, vui lòng quay lại sau"), "faceRecognition": @@ -932,8 +932,8 @@ class MessageLookup extends MessageLookupByLibrary { "fileTypes": MessageLookupByLibrary.simpleMessage("Loại tệp"), "fileTypesAndNames": MessageLookupByLibrary.simpleMessage("Loại tệp và tên"), - "filesBackedUpFromDevice": m35, - "filesBackedUpInAlbum": m36, + "filesBackedUpFromDevice": m37, + "filesBackedUpInAlbum": m38, "filesDeleted": MessageLookupByLibrary.simpleMessage("Tệp đã bị xóa"), "filesSavedToGallery": MessageLookupByLibrary.simpleMessage( "Các tệp đã được lưu vào thư viện"), @@ -953,23 +953,23 @@ class MessageLookup extends MessageLookupByLibrary { "freeStorageUsable": MessageLookupByLibrary.simpleMessage( "Lưu trữ miễn phí có thể sử dụng"), "freeTrial": MessageLookupByLibrary.simpleMessage("Dùng thử miễn phí"), - "freeTrialValidTill": m37, - "freeUpAccessPostDelete": m38, - "freeUpAmount": m39, + "freeTrialValidTill": m39, + "freeUpAccessPostDelete": m40, + "freeUpAmount": m41, "freeUpDeviceSpace": MessageLookupByLibrary.simpleMessage( "Giải phóng không gian thiết bị"), "freeUpDeviceSpaceDesc": MessageLookupByLibrary.simpleMessage( "Tiết kiệm không gian trên thiết bị của bạn bằng cách xóa các tệp đã được sao lưu."), "freeUpSpace": MessageLookupByLibrary.simpleMessage("Giải phóng không gian"), - "freeUpSpaceSaving": m40, + "freeUpSpaceSaving": m42, "gallery": MessageLookupByLibrary.simpleMessage("Thư viện"), "galleryMemoryLimitInfo": MessageLookupByLibrary.simpleMessage( "Tối đa 1000 kỷ niệm được hiển thị trong thư viện"), "general": MessageLookupByLibrary.simpleMessage("Chung"), "generatingEncryptionKeys": MessageLookupByLibrary.simpleMessage("Đang tạo khóa mã hóa..."), - "genericProgress": m41, + "genericProgress": m43, "goToSettings": MessageLookupByLibrary.simpleMessage("Đi đến cài đặt"), "googlePlayId": MessageLookupByLibrary.simpleMessage("ID Google Play"), "grantFullAccessPrompt": MessageLookupByLibrary.simpleMessage( @@ -1050,7 +1050,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": m44, "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"), @@ -1080,7 +1080,7 @@ class MessageLookup extends MessageLookupByLibrary { "legacy": MessageLookupByLibrary.simpleMessage("Thừa kế"), "legacyAccounts": MessageLookupByLibrary.simpleMessage("Tài khoản thừa kế"), - "legacyInvite": m43, + "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( @@ -1093,7 +1093,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": m44, + "linkExpiresOn": m46, "linkExpiry": MessageLookupByLibrary.simpleMessage("Hết hạn liên kết"), "linkHasExpired": MessageLookupByLibrary.simpleMessage("Liên kết đã hết hạn"), @@ -1213,11 +1213,11 @@ class MessageLookup extends MessageLookupByLibrary { "moreDetails": MessageLookupByLibrary.simpleMessage("Thêm chi tiết"), "mostRecent": MessageLookupByLibrary.simpleMessage("Mới nhất"), "mostRelevant": MessageLookupByLibrary.simpleMessage("Liên quan nhất"), - "moveItem": m45, + "moveItem": m49, "moveToAlbum": MessageLookupByLibrary.simpleMessage("Chuyển đến album"), "moveToHiddenAlbum": MessageLookupByLibrary.simpleMessage("Di chuyển đến album ẩn"), - "movedSuccessfullyTo": m46, + "movedSuccessfullyTo": m50, "movedToTrash": MessageLookupByLibrary.simpleMessage("Đã chuyển vào thùng rác"), "movingFilesToAlbum": MessageLookupByLibrary.simpleMessage( @@ -1270,10 +1270,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": m47, + "noSuggestionsForPerson": m51, "noSystemLockFound": MessageLookupByLibrary.simpleMessage( "Không tìm thấy khóa hệ thống"), - "notPersonLabel": m48, + "notPersonLabel": m52, "nothingSharedWithYouYet": MessageLookupByLibrary.simpleMessage( "Chưa có gì được chia sẻ với bạn"), "nothingToSeeHere": MessageLookupByLibrary.simpleMessage( @@ -1283,7 +1283,7 @@ class MessageLookup extends MessageLookupByLibrary { "onDevice": MessageLookupByLibrary.simpleMessage("Trên thiết bị"), "onEnte": MessageLookupByLibrary.simpleMessage( "Trên ente"), - "onlyFamilyAdminCanChangeCode": m49, + "onlyFamilyAdminCanChangeCode": m53, "onlyThem": MessageLookupByLibrary.simpleMessage("Chỉ họ"), "oops": MessageLookupByLibrary.simpleMessage("Ôi"), "oopsCouldNotSaveEdits": @@ -1305,8 +1305,6 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Hoặc hợp nhất với hiện có"), "orPickAnExistingOne": MessageLookupByLibrary.simpleMessage("Hoặc chọn một cái có sẵn"), - "orPickFromYourContacts": - MessageLookupByLibrary.simpleMessage("or pick from your contacts"), "pair": MessageLookupByLibrary.simpleMessage("Ghép nối"), "pairWithPin": MessageLookupByLibrary.simpleMessage("Ghép nối với PIN"), "pairingComplete": @@ -1333,7 +1331,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": m50, + "paymentFailedTalkToProvider": m54, "pendingItems": MessageLookupByLibrary.simpleMessage("Các mục đang chờ"), "pendingSync": @@ -1356,13 +1354,13 @@ class MessageLookup extends MessageLookupByLibrary { "photosAddedByYouWillBeRemovedFromTheAlbum": MessageLookupByLibrary.simpleMessage( "Ảnh bạn đã thêm sẽ bị xóa khỏi album"), - "photosCount": m51, + "photosCount": m55, "pickCenterPoint": MessageLookupByLibrary.simpleMessage("Chọn điểm trung tâm"), "pinAlbum": MessageLookupByLibrary.simpleMessage("Ghim album"), "pinLock": MessageLookupByLibrary.simpleMessage("Khóa PIN"), "playOnTv": MessageLookupByLibrary.simpleMessage("Phát album trên TV"), - "playStoreFreeTrialValidTill": m52, + "playStoreFreeTrialValidTill": m56, "playstoreSubscription": MessageLookupByLibrary.simpleMessage("Đăng ký PlayStore"), "pleaseCheckYourInternetConnectionAndTryAgain": @@ -1374,14 +1372,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": m53, + "pleaseEmailUsAt": m57, "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": m54, + "pleaseSendTheLogsTo": m58, "pleaseTryAgain": MessageLookupByLibrary.simpleMessage("Vui lòng thử lại"), "pleaseVerifyTheCodeYouHaveEntered": @@ -1410,7 +1408,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": m55, + "processingImport": m59, "publicLinkCreated": MessageLookupByLibrary.simpleMessage( "Liên kết công khai đã được tạo"), "publicLinkEnabled": MessageLookupByLibrary.simpleMessage( @@ -1420,7 +1418,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": m56, + "rateUsOnStore": m60, "recover": MessageLookupByLibrary.simpleMessage("Khôi phục"), "recoverAccount": MessageLookupByLibrary.simpleMessage("Khôi phục tài khoản"), @@ -1429,7 +1427,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": m57, + "recoveryInitiatedDesc": m62, "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"), @@ -1443,12 +1441,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": m58, + "recoveryReady": m63, "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": m59, + "recoveryWarningBody": m64, "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": @@ -1463,7 +1461,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": m60, + "referralStep3": m65, "referrals": MessageLookupByLibrary.simpleMessage("Giới thiệu"), "referralsAreCurrentlyPaused": MessageLookupByLibrary.simpleMessage( "Giới thiệu hiện đang tạm dừng"), @@ -1492,7 +1490,7 @@ class MessageLookup extends MessageLookupByLibrary { "removeLink": MessageLookupByLibrary.simpleMessage("Xóa liên kết"), "removeParticipant": MessageLookupByLibrary.simpleMessage("Xóa người tham gia"), - "removeParticipantBody": m61, + "removeParticipantBody": m66, "removePersonLabel": MessageLookupByLibrary.simpleMessage("Xóa nhãn người"), "removePublicLink": @@ -1511,7 +1509,7 @@ class MessageLookup extends MessageLookupByLibrary { "renameFile": MessageLookupByLibrary.simpleMessage("Đổi tên tệp"), "renewSubscription": MessageLookupByLibrary.simpleMessage("Gia hạn đăng ký"), - "renewsOn": m62, + "renewsOn": m67, "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"), @@ -1587,8 +1585,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": m63, - "searchSectionsLengthMismatch": m64, + "searchResultCount": m68, + "searchSectionsLengthMismatch": m69, "security": MessageLookupByLibrary.simpleMessage("Bảo mật"), "seePublicAlbumLinksInApp": MessageLookupByLibrary.simpleMessage( "Xem liên kết album công khai trong ứng dụng"), @@ -1622,7 +1620,7 @@ class MessageLookup extends MessageLookupByLibrary { 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": m6, - "selectedPhotosWithYours": m65, + "selectedPhotosWithYours": m70, "send": MessageLookupByLibrary.simpleMessage("Gửi"), "sendEmail": MessageLookupByLibrary.simpleMessage("Gửi email"), "sendInvite": MessageLookupByLibrary.simpleMessage("Gửi lời mời"), @@ -1653,16 +1651,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": m66, + "shareMyVerificationID": m71, "shareOnlyWithThePeopleYouWant": MessageLookupByLibrary.simpleMessage( "Chia sẻ chỉ với những người bạn muốn"), "shareTextConfirmOthersVerificationID": m7, "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": m67, + "shareTextReferralCode": m72, "shareWithNonenteUsers": MessageLookupByLibrary.simpleMessage( "Chia sẻ với người dùng không phải Ente"), - "shareWithPeopleSectionTitle": m68, + "shareWithPeopleSectionTitle": m73, "shareYourFirstAlbum": MessageLookupByLibrary.simpleMessage( "Chia sẻ album đầu tiên của bạn"), "sharedAlbumSectionDescription": MessageLookupByLibrary.simpleMessage( @@ -1674,7 +1672,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": m69, + "sharedWith": m74, "sharedWithMe": MessageLookupByLibrary.simpleMessage("Chia sẻ với tôi"), "sharedWithYou": MessageLookupByLibrary.simpleMessage("Được chia sẻ với bạn"), @@ -1690,11 +1688,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": m70, + "singleFileDeleteFromDevice": m75, "singleFileDeleteHighlight": MessageLookupByLibrary.simpleMessage( "Nó sẽ bị xóa khỏi tất cả các album."), - "singleFileInBothLocalAndRemote": m71, - "singleFileInRemoteOnly": m72, + "singleFileInBothLocalAndRemote": m76, + "singleFileInRemoteOnly": m77, "skip": MessageLookupByLibrary.simpleMessage("Bỏ qua"), "social": MessageLookupByLibrary.simpleMessage("Xã hội"), "someItemsAreInBothEnteAndYourDevice": @@ -1744,10 +1742,10 @@ class MessageLookup extends MessageLookupByLibrary { "storageInGB": m1, "storageLimitExceeded": MessageLookupByLibrary.simpleMessage("Vượt quá giới hạn lưu trữ"), - "storageUsageInfo": m73, + "storageUsageInfo": m78, "strongStrength": MessageLookupByLibrary.simpleMessage("Mạnh"), - "subAlreadyLinkedErrMessage": m74, - "subWillBeCancelledOn": m75, + "subAlreadyLinkedErrMessage": m79, + "subWillBeCancelledOn": m80, "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ẻ."), @@ -1764,7 +1762,7 @@ class MessageLookup extends MessageLookupByLibrary { "suggestFeatures": MessageLookupByLibrary.simpleMessage("Gợi ý tính năng"), "support": MessageLookupByLibrary.simpleMessage("Hỗ trợ"), - "syncProgress": m76, + "syncProgress": m81, "syncStopped": MessageLookupByLibrary.simpleMessage("Đồng bộ hóa đã dừng"), "syncing": MessageLookupByLibrary.simpleMessage("Đang đồng bộ hóa..."), @@ -1774,7 +1772,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": m77, + "tapToUploadIsIgnoredDue": m82, "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"), @@ -1814,7 +1812,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": m78, + "thisIsPersonVerificationId": m83, "thisIsYourVerificationId": MessageLookupByLibrary.simpleMessage("Đây là ID xác minh của bạn"), "thisWillLogYouOutOfTheFollowingDevice": @@ -1838,11 +1836,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": m79, + "trashDaysLeft": m84, "trim": MessageLookupByLibrary.simpleMessage("Cắt"), "trustedContacts": MessageLookupByLibrary.simpleMessage("Liên hệ tin cậy"), - "trustedInviteBody": m80, + "trustedInviteBody": m85, "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."), @@ -1861,7 +1859,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": m81, + "typeOfGallerGallerytypeIsNotSupportedForRename": m86, "unarchive": MessageLookupByLibrary.simpleMessage("Khôi phục"), "unarchiveAlbum": MessageLookupByLibrary.simpleMessage("Khôi phục album"), @@ -1885,10 +1883,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": m82, + "uploadIsIgnoredDueToIgnorereason": m87, "uploadingFilesToAlbum": MessageLookupByLibrary.simpleMessage("Đang tải tệp lên album..."), - "uploadingMultipleMemories": m83, + "uploadingMultipleMemories": m88, "uploadingSingleMemory": MessageLookupByLibrary.simpleMessage("Đang lưu giữ 1 kỷ niệm..."), "upto50OffUntil4thDec": MessageLookupByLibrary.simpleMessage( @@ -1906,14 +1904,14 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Sử dụng ảnh đã chọn"), "usedSpace": MessageLookupByLibrary.simpleMessage("Không gian đã sử dụng"), - "validTill": m84, + "validTill": m89, "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": m85, + "verifyEmailID": m90, "verifyIDLabel": MessageLookupByLibrary.simpleMessage("Xác minh"), "verifyPasskey": MessageLookupByLibrary.simpleMessage("Xác minh mã khóa"), @@ -1939,7 +1937,7 @@ class MessageLookup extends MessageLookupByLibrary { "viewRecoveryKey": MessageLookupByLibrary.simpleMessage("Xem khóa khôi phục"), "viewer": MessageLookupByLibrary.simpleMessage("Người xem"), - "viewersSuccessfullyAdded": m86, + "viewersSuccessfullyAdded": m91, "visitWebToManage": MessageLookupByLibrary.simpleMessage( "Vui lòng truy cập web.ente.io để quản lý đăng ký của bạn"), "waitingForVerification": @@ -1961,7 +1959,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": m87, + "yearsAgo": m92, "yes": MessageLookupByLibrary.simpleMessage("Có"), "yesCancel": MessageLookupByLibrary.simpleMessage("Có, hủy"), "yesConvertToViewer": @@ -1993,7 +1991,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": m88, + "youHaveSuccessfullyFreedUp": m93, "yourAccountHasBeenDeleted": MessageLookupByLibrary.simpleMessage("Tài khoản của bạn đã bị xóa"), "yourMap": MessageLookupByLibrary.simpleMessage("Bản đồ của bạn"), @@ -2014,9 +2012,6 @@ class MessageLookup extends MessageLookupByLibrary { "Đăng ký của bạn đã được cập nhật thành công"), "yourVerificationCodeHasExpired": MessageLookupByLibrary.simpleMessage( "Mã xác minh của bạn đã hết hạn"), - "youveNoDuplicateFilesThatCanBeCleared": - MessageLookupByLibrary.simpleMessage( - "Bạn không có tệp trùng lặp nào có thể được xóa"), "youveNoFilesInThisAlbumThatCanBeDeleted": MessageLookupByLibrary.simpleMessage( "Bạn không có tệp nào trong album này có thể bị xóa"), diff --git a/mobile/lib/generated/intl/messages_zh.dart b/mobile/lib/generated/intl/messages_zh.dart index e79aef918c..9d875cc114 100644 --- a/mobile/lib/generated/intl/messages_zh.dart +++ b/mobile/lib/generated/intl/messages_zh.dart @@ -20,224 +20,234 @@ typedef String MessageIfAbsent(String messageStr, List args); class MessageLookup extends MessageLookupByLibrary { String get localeName => 'zh'; - static String m9(count) => - "${Intl.plural(count, zero: '添加协作者', one: '添加协作者', other: '添加协作者')}"; + static String m9(title) => "${title} (我)"; static String m10(count) => + "${Intl.plural(count, zero: '添加协作者', one: '添加协作者', other: '添加协作者')}"; + + static String m11(count) => "${Intl.plural(count, one: '添加一个项目', other: '添加一些项目')}"; - static String m11(storageAmount, endDate) => + static String m12(storageAmount, endDate) => "您的 ${storageAmount} 插件有效期至 ${endDate}"; - static String m12(count) => + static String m13(count) => "${Intl.plural(count, zero: '添加查看者', one: '添加查看者', other: '添加查看者')}"; - static String m13(emailOrName) => "由 ${emailOrName} 添加"; + static String m14(emailOrName) => "由 ${emailOrName} 添加"; - static String m14(albumName) => "成功添加到 ${albumName}"; + static String m15(albumName) => "成功添加到 ${albumName}"; - static String m15(count) => + static String m16(count) => "${Intl.plural(count, zero: '无参与者', one: '1个参与者', other: '${count} 个参与者')}"; - static String m16(versionValue) => "版本: ${versionValue}"; + static String m17(versionValue) => "版本: ${versionValue}"; - static String m17(freeAmount, storageUnit) => + static String m18(freeAmount, storageUnit) => "${freeAmount} ${storageUnit} 空闲"; - static String m18(paymentProvider) => "请先取消您现有的订阅 ${paymentProvider}"; + static String m19(paymentProvider) => "请先取消您现有的订阅 ${paymentProvider}"; static String m3(user) => "${user} 将无法添加更多照片到此相册\n\n他们仍然能够删除他们添加的现有照片"; - static String m19(isFamilyMember, storageAmountInGb) => + static String m20(isFamilyMember, storageAmountInGb) => "${Intl.select(isFamilyMember, { 'true': '到目前为止,您的家庭已经领取了 ${storageAmountInGb} GB', 'false': '到目前为止,您已经领取了 ${storageAmountInGb} GB', 'other': '到目前为止,您已经领取了${storageAmountInGb} GB', })}"; - static String m20(albumName) => "为 ${albumName} 创建了协作链接"; + static String m21(albumName) => "为 ${albumName} 创建了协作链接"; - static String m21(count) => + static String m22(count) => "${Intl.plural(count, zero: '已添加 0 位协作者', one: '已添加 1 位协作者', other: '已添加 ${count} 位协作者')}"; - static String m22(email, numOfDays) => + static String m23(email, numOfDays) => "您即将添加 ${email} 作为可信联系人。如果您离开了 ${numOfDays} 天,他们将能够恢复您的帐户。"; - static String m23(familyAdminEmail) => + static String m24(familyAdminEmail) => "请联系 ${familyAdminEmail} 来管理您的订阅"; - static String m24(provider) => + static String m25(provider) => "请通过support@ente.io 用英语联系我们来管理您的 ${provider} 订阅。"; - static String m25(endpoint) => "已连接至 ${endpoint}"; + static String m26(endpoint) => "已连接至 ${endpoint}"; - static String m26(count) => + static String m27(count) => "${Intl.plural(count, one: '删除 ${count} 个项目', other: '删除 ${count} 个项目')}"; - static String m27(currentlyDeleting, totalCount) => + static String m28(currentlyDeleting, totalCount) => "正在删除 ${currentlyDeleting} /共 ${totalCount}"; - static String m28(albumName) => "这将删除用于访问\"${albumName}\"的公开链接。"; + static String m29(albumName) => "这将删除用于访问\"${albumName}\"的公开链接。"; - static String m29(supportEmail) => "请从您注册的邮箱发送一封邮件到 ${supportEmail}"; + static String m30(supportEmail) => "请从您注册的邮箱发送一封邮件到 ${supportEmail}"; - static String m30(count, storageSaved) => + static String m31(count, storageSaved) => "您已经清理了 ${Intl.plural(count, other: '${count} 个重复文件')}, 释放了 (${storageSaved}!)"; - static String m31(count, formattedSize) => + static String m32(count, formattedSize) => "${count} 个文件,每个文件 ${formattedSize}"; - static String m32(newEmail) => "电子邮件已更改为 ${newEmail}"; + static String m33(newEmail) => "电子邮件已更改为 ${newEmail}"; - static String m33(email) => "${email} 没有 Ente 帐户。\n\n向他们发出共享照片的邀请。"; + static String m34(email) => "${email} 没有 Ente 账户。"; - static String m34(text) => "为 ${text} 找到额外照片"; + static String m35(email) => "${email} 没有 Ente 帐户。\n\n向他们发出共享照片的邀请。"; - static String m35(count, formattedNumber) => + static String m36(text) => "为 ${text} 找到额外照片"; + + static String m37(count, formattedNumber) => "此设备上的 ${Intl.plural(count, one: '1 个文件', other: '${formattedNumber} 个文件')} 已安全备份"; - static String m36(count, formattedNumber) => + static String m38(count, formattedNumber) => "此相册中的 ${Intl.plural(count, one: '1 个文件', other: '${formattedNumber} 个文件')} 已安全备份"; static String m4(storageAmountInGB) => "每当有人使用您的代码注册付费计划时您将获得${storageAmountInGB} GB"; - static String m37(endDate) => "免费试用有效期至 ${endDate}"; + static String m39(endDate) => "免费试用有效期至 ${endDate}"; - static String m38(count) => + static String m40(count) => "只要您有有效的订阅,您仍然可以在 Ente 上访问 ${Intl.plural(count, one: '它', other: '它们')}"; - static String m39(sizeInMBorGB) => "释放 ${sizeInMBorGB}"; + static String m41(sizeInMBorGB) => "释放 ${sizeInMBorGB}"; - static String m40(count, formattedSize) => + static String m42(count, formattedSize) => "${Intl.plural(count, one: '它可以从设备中删除以释放 ${formattedSize}', other: '它们可以从设备中删除以释放 ${formattedSize}')}"; - static String m41(currentlyProcessing, totalCount) => + static String m43(currentlyProcessing, totalCount) => "正在处理 ${currentlyProcessing} / ${totalCount}"; - static String m42(count) => + static String m44(count) => "${Intl.plural(count, one: '${count} 个项目', other: '${count} 个项目')}"; - static String m43(email) => "${email} 已邀请您成为可信联系人"; + static String m45(email) => "${email} 已邀请您成为可信联系人"; - static String m44(expiryTime) => "链接将在 ${expiryTime} 过期"; + static String m46(expiryTime) => "链接将在 ${expiryTime} 过期"; + + static String m47(email) => "将人员链接到 ${email}"; + + static String m48(personName, email) => "这将会将 ${personName} 链接到 ${email}"; static String m5(count, formattedCount) => "${Intl.plural(count, zero: '没有回忆', one: '${formattedCount} 个回忆', other: '${formattedCount} 个回忆')}"; - static String m45(count) => + static String m49(count) => "${Intl.plural(count, one: '移动一个项目', other: '移动一些项目')}"; - static String m46(albumName) => "成功移动到 ${albumName}"; + static String m50(albumName) => "成功移动到 ${albumName}"; - static String m47(personName) => "没有针对 ${personName} 的建议"; + static String m51(personName) => "没有针对 ${personName} 的建议"; - static String m48(name) => "不是 ${name}?"; + static String m52(name) => "不是 ${name}?"; - static String m49(familyAdminEmail) => "请联系${familyAdminEmail} 以更改您的代码。"; + static String m53(familyAdminEmail) => "请联系${familyAdminEmail} 以更改您的代码。"; static String m0(passwordStrengthValue) => "密码强度: ${passwordStrengthValue}"; - static String m50(providerName) => "如果您被收取费用,请用英语与 ${providerName} 的客服聊天"; + static String m54(providerName) => "如果您被收取费用,请用英语与 ${providerName} 的客服聊天"; - static String m51(count) => + static String m55(count) => "${Intl.plural(count, zero: '0 张照片', one: '1 张照片', other: '${count} 张照片')}"; - static String m52(endDate) => "免费试用有效期至 ${endDate}。\n在此之后您可以选择付费计划。"; + static String m56(endDate) => "免费试用有效期至 ${endDate}。\n在此之后您可以选择付费计划。"; - static String m53(toEmail) => "请给我们发送电子邮件至 ${toEmail}"; + static String m57(toEmail) => "请给我们发送电子邮件至 ${toEmail}"; - static String m54(toEmail) => "请将日志发送至 \n${toEmail}"; + static String m58(toEmail) => "请将日志发送至 \n${toEmail}"; - static String m55(folderName) => "正在处理 ${folderName}..."; + static String m59(folderName) => "正在处理 ${folderName}..."; - static String m56(storeName) => "在 ${storeName} 上给我们评分"; + static String m60(storeName) => "在 ${storeName} 上给我们评分"; - static String m57(days, email) => "您可以在 ${days} 天后访问该账户。通知将发送至 ${email}。"; + static String m61(name) => "已将您重新分配给 ${name}"; - static String m58(email) => "您现在可以通过设置新密码来恢复 ${email} 的账户。"; + static String m62(days, email) => "您可以在 ${days} 天后访问该账户。通知将发送至 ${email}。"; - static String m59(email) => "${email} 正在尝试恢复您的账户。"; + static String m63(email) => "您现在可以通过设置新密码来恢复 ${email} 的账户。"; - static String m60(storageInGB) => "3. 你和朋友都将免费获得 ${storageInGB} GB*"; + static String m64(email) => "${email} 正在尝试恢复您的账户。"; - static String m61(userEmail) => + static String m65(storageInGB) => "3. 你和朋友都将免费获得 ${storageInGB} GB*"; + + static String m66(userEmail) => "${userEmail} 将从这个共享相册中删除\n\nTA们添加的任何照片也将从相册中删除"; - static String m62(endDate) => "在 ${endDate} 前续费"; + static String m67(endDate) => "在 ${endDate} 前续费"; - static String m63(count) => + static String m68(count) => "${Intl.plural(count, other: '已找到 ${count} 个结果')}"; - static String m64(snapshotLength, searchLength) => + static String m69(snapshotLength, searchLength) => "部分长度不匹配:${snapshotLength} != ${searchLength}"; static String m6(count) => "已选择 ${count} 个"; - static String m65(count, yourCount) => "选择了 ${count} 个 (您的 ${yourCount} 个)"; + static String m70(count, yourCount) => "选择了 ${count} 个 (您的 ${yourCount} 个)"; - static String m66(verificationID) => "这是我的ente.io 的验证 ID: ${verificationID}。"; + static String m71(verificationID) => "这是我的ente.io 的验证 ID: ${verificationID}。"; static String m7(verificationID) => "嘿,你能确认这是你的 ente.io 验证 ID吗:${verificationID}"; - static String m67(referralCode, referralStorageInGB) => + static String m72(referralCode, referralStorageInGB) => "Ente 推荐代码:${referralCode}\n\n在 \"设置\"→\"通用\"→\"推荐 \"中应用它,即可在注册付费计划后免费获得 ${referralStorageInGB} GB 存储空间\n\nhttps://ente.io"; - static String m68(numberOfPeople) => + static String m73(numberOfPeople) => "${Intl.plural(numberOfPeople, zero: '与特定人员共享', one: '与 1 人共享', other: '与 ${numberOfPeople} 人共享')}"; - static String m69(emailIDs) => "与 ${emailIDs} 共享"; + static String m74(emailIDs) => "与 ${emailIDs} 共享"; - static String m70(fileType) => "此 ${fileType} 将从您的设备中删除。"; + static String m75(fileType) => "此 ${fileType} 将从您的设备中删除。"; - static String m71(fileType) => "${fileType} 已同时存在于 Ente 和您的设备中。"; + static String m76(fileType) => "${fileType} 已同时存在于 Ente 和您的设备中。"; - static String m72(fileType) => "${fileType} 将从 Ente 中删除。"; + static String m77(fileType) => "${fileType} 将从 Ente 中删除。"; static String m1(storageAmountInGB) => "${storageAmountInGB} GB"; - static String m73( + static String m78( usedAmount, usedStorageUnit, totalAmount, totalStorageUnit) => "已使用 ${usedAmount} ${usedStorageUnit} / ${totalAmount} ${totalStorageUnit}"; - static String m74(id) => + static String m79(id) => "您的 ${id} 已链接到另一个 Ente 账户。\n如果您想在此账户中使用您的 ${id} ,请联系我们的支持人员"; - static String m75(endDate) => "您的订阅将于 ${endDate} 取消"; + static String m80(endDate) => "您的订阅将于 ${endDate} 取消"; - static String m76(completed, total) => "已保存的回忆 ${completed}/共 ${total}"; + static String m81(completed, total) => "已保存的回忆 ${completed}/共 ${total}"; - static String m77(ignoreReason) => "点按上传,由于${ignoreReason},目前上传已被忽略"; + static String m82(ignoreReason) => "点按上传,由于${ignoreReason},目前上传已被忽略"; static String m8(storageAmountInGB) => "他们也会获得 ${storageAmountInGB} GB"; - static String m78(email) => "这是 ${email} 的验证ID"; + static String m83(email) => "这是 ${email} 的验证ID"; - static String m79(count) => + static String m84(count) => "${Intl.plural(count, zero: '马上', one: '1 天', other: '${count} 天')}"; - static String m80(email) => "您已受邀通过 ${email} 成为遗产联系人。"; + static String m85(email) => "您已受邀通过 ${email} 成为遗产联系人。"; - static String m81(galleryType) => "相册类型 ${galleryType} 不支持重命名"; + static String m86(galleryType) => "相册类型 ${galleryType} 不支持重命名"; - static String m82(ignoreReason) => "由于 ${ignoreReason},上传被忽略"; + static String m87(ignoreReason) => "由于 ${ignoreReason},上传被忽略"; - static String m83(count) => "正在保存 ${count} 个回忆..."; + static String m88(count) => "正在保存 ${count} 个回忆..."; - static String m84(endDate) => "有效期至 ${endDate}"; + static String m89(endDate) => "有效期至 ${endDate}"; - static String m85(email) => "验证 ${email}"; + static String m90(email) => "验证 ${email}"; - static String m86(count) => + static String m91(count) => "${Intl.plural(count, zero: '已添加 0 位查看者', one: '已添加 1 位查看者', other: '已添加 ${count} 位查看者')}"; static String m2(email) => "我们已经发送邮件到 ${email}"; - static String m87(count) => + static String m92(count) => "${Intl.plural(count, one: '${count} 年前', other: '${count} 年前')}"; - static String m88(storageSaved) => "您已成功释放了 ${storageSaved}!"; + static String m93(storageSaved) => "您已成功释放了 ${storageSaved}!"; final messages = _notInlinedMessages(_notInlinedMessages); static Map _notInlinedMessages(_) => { @@ -248,6 +258,7 @@ class MessageLookup extends MessageLookupByLibrary { "account": MessageLookupByLibrary.simpleMessage("账户"), "accountIsAlreadyConfigured": MessageLookupByLibrary.simpleMessage("账户已配置。"), + "accountOwnerPersonAppbarTitle": m9, "accountWelcomeBack": MessageLookupByLibrary.simpleMessage("欢迎回来!"), "ackPasswordLostWarning": MessageLookupByLibrary.simpleMessage( "我明白,如果我丢失密码,我可能会丢失我的数据,因为我的数据是 端到端加密的。"), @@ -256,10 +267,10 @@ class MessageLookup extends MessageLookupByLibrary { "addAName": MessageLookupByLibrary.simpleMessage("添加一个名称"), "addANewEmail": MessageLookupByLibrary.simpleMessage("添加新的电子邮件"), "addCollaborator": MessageLookupByLibrary.simpleMessage("添加协作者"), - "addCollaborators": m9, + "addCollaborators": m10, "addFiles": MessageLookupByLibrary.simpleMessage("添加文件"), "addFromDevice": MessageLookupByLibrary.simpleMessage("从设备添加"), - "addItem": m10, + "addItem": m11, "addLocation": MessageLookupByLibrary.simpleMessage("添加地点"), "addLocationButton": MessageLookupByLibrary.simpleMessage("添加"), "addMore": MessageLookupByLibrary.simpleMessage("添加更多"), @@ -268,7 +279,7 @@ class MessageLookup extends MessageLookupByLibrary { "addNew": MessageLookupByLibrary.simpleMessage("新建"), "addNewPerson": MessageLookupByLibrary.simpleMessage("添加新人物"), "addOnPageSubtitle": MessageLookupByLibrary.simpleMessage("附加组件详情"), - "addOnValidTill": m11, + "addOnValidTill": m12, "addOns": MessageLookupByLibrary.simpleMessage("附加组件"), "addPhotos": MessageLookupByLibrary.simpleMessage("添加照片"), "addSelected": MessageLookupByLibrary.simpleMessage("添加所选项"), @@ -277,11 +288,11 @@ class MessageLookup extends MessageLookupByLibrary { "addToHiddenAlbum": MessageLookupByLibrary.simpleMessage("添加到隐藏相册"), "addTrustedContact": MessageLookupByLibrary.simpleMessage("添加可信联系人"), "addViewer": MessageLookupByLibrary.simpleMessage("添加查看者"), - "addViewers": m12, + "addViewers": m13, "addYourPhotosNow": MessageLookupByLibrary.simpleMessage("立即添加您的照片"), "addedAs": MessageLookupByLibrary.simpleMessage("已添加为"), - "addedBy": m13, - "addedSuccessfullyTo": m14, + "addedBy": m14, + "addedSuccessfullyTo": m15, "addingToFavorites": MessageLookupByLibrary.simpleMessage("正在添加到收藏..."), "advanced": MessageLookupByLibrary.simpleMessage("高级设置"), "advancedSettings": MessageLookupByLibrary.simpleMessage("高级设置"), @@ -291,7 +302,7 @@ class MessageLookup extends MessageLookupByLibrary { "after1Week": MessageLookupByLibrary.simpleMessage("1 周后"), "after1Year": MessageLookupByLibrary.simpleMessage("1 年后"), "albumOwner": MessageLookupByLibrary.simpleMessage("所有者"), - "albumParticipantsCount": m15, + "albumParticipantsCount": m16, "albumTitle": MessageLookupByLibrary.simpleMessage("相册标题"), "albumUpdated": MessageLookupByLibrary.simpleMessage("相册已更新"), "albums": MessageLookupByLibrary.simpleMessage("相册"), @@ -331,7 +342,7 @@ class MessageLookup extends MessageLookupByLibrary { "appLock": MessageLookupByLibrary.simpleMessage("应用锁"), "appLockDescriptions": MessageLookupByLibrary.simpleMessage( "在设备的默认锁定屏幕和带有 PIN 或密码的自定义锁定屏幕之间进行选择。"), - "appVersion": m16, + "appVersion": m17, "appleId": MessageLookupByLibrary.simpleMessage("Apple ID"), "apply": MessageLookupByLibrary.simpleMessage("应用"), "applyCodeTitle": MessageLookupByLibrary.simpleMessage("应用代码"), @@ -405,7 +416,7 @@ class MessageLookup extends MessageLookupByLibrary { "autoPairDesc": MessageLookupByLibrary.simpleMessage("自动配对仅适用于支持 Chromecast 的设备。"), "available": MessageLookupByLibrary.simpleMessage("可用"), - "availableStorageSpace": m17, + "availableStorageSpace": m18, "backedUpFolders": MessageLookupByLibrary.simpleMessage("已备份的文件夹"), "backup": MessageLookupByLibrary.simpleMessage("备份"), "backupFailed": MessageLookupByLibrary.simpleMessage("备份失败"), @@ -435,7 +446,7 @@ class MessageLookup extends MessageLookupByLibrary { "cancelAccountRecovery": MessageLookupByLibrary.simpleMessage("取消恢复"), "cancelAccountRecoveryBody": MessageLookupByLibrary.simpleMessage("您真的要取消恢复吗?"), - "cancelOtherSubscription": m18, + "cancelOtherSubscription": m19, "cancelSubscription": MessageLookupByLibrary.simpleMessage("取消订阅"), "cannotAddMorePhotosAfterBecomingViewer": m3, "cannotDeleteSharedFiles": @@ -477,7 +488,7 @@ class MessageLookup extends MessageLookupByLibrary { "claimFreeStorage": MessageLookupByLibrary.simpleMessage("领取免费存储"), "claimMore": MessageLookupByLibrary.simpleMessage("领取更多!"), "claimed": MessageLookupByLibrary.simpleMessage("已领取"), - "claimedStorageSoFar": m19, + "claimedStorageSoFar": m20, "cleanUncategorized": MessageLookupByLibrary.simpleMessage("清除未分类的"), "cleanUncategorizedDescription": MessageLookupByLibrary.simpleMessage("从“未分类”中删除其他相册中存在的所有文件"), @@ -499,11 +510,11 @@ class MessageLookup extends MessageLookupByLibrary { "collabLinkSectionDescription": MessageLookupByLibrary.simpleMessage( "创建一个链接来让他人无需 Ente 应用程序或账户即可在您的共享相册中添加和查看照片。非常适合收集活动照片。"), "collaborativeLink": MessageLookupByLibrary.simpleMessage("协作链接"), - "collaborativeLinkCreatedFor": m20, + "collaborativeLinkCreatedFor": m21, "collaborator": MessageLookupByLibrary.simpleMessage("协作者"), "collaboratorsCanAddPhotosAndVideosToTheSharedAlbum": MessageLookupByLibrary.simpleMessage("协作者可以将照片和视频添加到共享相册中。"), - "collaboratorsSuccessfullyAdded": m21, + "collaboratorsSuccessfullyAdded": m22, "collageLayout": MessageLookupByLibrary.simpleMessage("布局"), "collageSaved": MessageLookupByLibrary.simpleMessage("拼贴已保存到相册"), "collect": MessageLookupByLibrary.simpleMessage("收集"), @@ -518,7 +529,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("您确定要禁用双重认证吗?"), "confirmAccountDeletion": MessageLookupByLibrary.simpleMessage("确认删除账户"), - "confirmAddingTrustedContact": m22, + "confirmAddingTrustedContact": m23, "confirmDeletePrompt": MessageLookupByLibrary.simpleMessage("是的,我想永久删除此账户及其所有关联的应用程序的数据。"), "confirmPassword": MessageLookupByLibrary.simpleMessage("请确认密码"), @@ -527,9 +538,9 @@ class MessageLookup extends MessageLookupByLibrary { "confirmYourRecoveryKey": MessageLookupByLibrary.simpleMessage("确认您的恢复密钥"), "connectToDevice": MessageLookupByLibrary.simpleMessage("连接到设备"), - "contactFamilyAdmin": m23, + "contactFamilyAdmin": m24, "contactSupport": MessageLookupByLibrary.simpleMessage("联系支持"), - "contactToManageSubscription": m24, + "contactToManageSubscription": m25, "contacts": MessageLookupByLibrary.simpleMessage("联系人"), "contents": MessageLookupByLibrary.simpleMessage("内容"), "continueLabel": MessageLookupByLibrary.simpleMessage("继续"), @@ -563,7 +574,7 @@ class MessageLookup extends MessageLookupByLibrary { "currentUsageIs": MessageLookupByLibrary.simpleMessage("当前用量 "), "currentlyRunning": MessageLookupByLibrary.simpleMessage("目前正在运行"), "custom": MessageLookupByLibrary.simpleMessage("自定义"), - "customEndpoint": m25, + "customEndpoint": m26, "darkTheme": MessageLookupByLibrary.simpleMessage("深色"), "dayToday": MessageLookupByLibrary.simpleMessage("今天"), "dayYesterday": MessageLookupByLibrary.simpleMessage("昨天"), @@ -593,10 +604,10 @@ class MessageLookup extends MessageLookupByLibrary { "deleteFromBoth": MessageLookupByLibrary.simpleMessage("同时从两者中删除"), "deleteFromDevice": MessageLookupByLibrary.simpleMessage("从设备中删除"), "deleteFromEnte": MessageLookupByLibrary.simpleMessage("从 Ente 中删除"), - "deleteItemCount": m26, + "deleteItemCount": m27, "deleteLocation": MessageLookupByLibrary.simpleMessage("删除位置"), "deletePhotos": MessageLookupByLibrary.simpleMessage("删除照片"), - "deleteProgress": m27, + "deleteProgress": m28, "deleteReason1": MessageLookupByLibrary.simpleMessage("找不到我想要的功能"), "deleteReason2": MessageLookupByLibrary.simpleMessage("应用或某个功能没有按我的预期运行"), @@ -627,7 +638,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("查看者仍然可以使用外部工具截图或保存您的照片副本"), "disableDownloadWarningTitle": MessageLookupByLibrary.simpleMessage("请注意"), - "disableLinkMessage": m28, + "disableLinkMessage": m29, "disableTwofactor": MessageLookupByLibrary.simpleMessage("禁用双重认证"), "disablingTwofactorAuthentication": MessageLookupByLibrary.simpleMessage("正在禁用双重认证..."), @@ -655,14 +666,15 @@ class MessageLookup extends MessageLookupByLibrary { "doYouWantToDiscardTheEditsYouHaveMade": MessageLookupByLibrary.simpleMessage("您想要放弃您所做的编辑吗?"), "done": MessageLookupByLibrary.simpleMessage("已完成"), + "dontSave": MessageLookupByLibrary.simpleMessage("不保存"), "doubleYourStorage": MessageLookupByLibrary.simpleMessage("将您的存储空间增加一倍"), "download": MessageLookupByLibrary.simpleMessage("下载"), "downloadFailed": MessageLookupByLibrary.simpleMessage("下載失敗"), "downloading": MessageLookupByLibrary.simpleMessage("正在下载..."), - "dropSupportEmail": m29, - "duplicateFileCountWithStorageSaved": m30, - "duplicateItemsGroup": m31, + "dropSupportEmail": m30, + "duplicateFileCountWithStorageSaved": m31, + "duplicateItemsGroup": m32, "edit": MessageLookupByLibrary.simpleMessage("编辑"), "editLocation": MessageLookupByLibrary.simpleMessage("编辑位置"), "editLocationTagTitle": MessageLookupByLibrary.simpleMessage("编辑位置"), @@ -674,8 +686,9 @@ class MessageLookup extends MessageLookupByLibrary { "email": MessageLookupByLibrary.simpleMessage("电子邮件地址"), "emailAlreadyRegistered": MessageLookupByLibrary.simpleMessage("此电子邮件地址已被注册。"), - "emailChangedTo": m32, - "emailNoEnteAccount": m33, + "emailChangedTo": m33, + "emailDoesNotHaveEnteAccount": m34, + "emailNoEnteAccount": m35, "emailNotRegistered": MessageLookupByLibrary.simpleMessage("此电子邮件地址未被注册。"), "emailVerificationToggle": @@ -742,11 +755,12 @@ class MessageLookup extends MessageLookupByLibrary { "exportLogs": MessageLookupByLibrary.simpleMessage("导出日志"), "exportYourData": MessageLookupByLibrary.simpleMessage("导出您的数据"), "extraPhotosFound": MessageLookupByLibrary.simpleMessage("发现额外照片"), - "extraPhotosFoundFor": m34, + "extraPhotosFoundFor": m36, "faceNotClusteredYet": MessageLookupByLibrary.simpleMessage("人脸尚未聚类,请稍后再来"), "faceRecognition": MessageLookupByLibrary.simpleMessage("人脸识别"), "faces": MessageLookupByLibrary.simpleMessage("人脸"), + "failed": MessageLookupByLibrary.simpleMessage("失败"), "failedToApplyCode": MessageLookupByLibrary.simpleMessage("无法使用此代码"), "failedToCancel": MessageLookupByLibrary.simpleMessage("取消失败"), "failedToDownloadVideo": MessageLookupByLibrary.simpleMessage("视频下载失败"), @@ -779,8 +793,8 @@ class MessageLookup extends MessageLookupByLibrary { "fileSavedToGallery": MessageLookupByLibrary.simpleMessage("文件已保存到相册"), "fileTypes": MessageLookupByLibrary.simpleMessage("文件类型"), "fileTypesAndNames": MessageLookupByLibrary.simpleMessage("文件类型和名称"), - "filesBackedUpFromDevice": m35, - "filesBackedUpInAlbum": m36, + "filesBackedUpFromDevice": m37, + "filesBackedUpInAlbum": m38, "filesDeleted": MessageLookupByLibrary.simpleMessage("文件已删除"), "filesSavedToGallery": MessageLookupByLibrary.simpleMessage("多个文件已保存到相册"), @@ -794,21 +808,21 @@ class MessageLookup extends MessageLookupByLibrary { "freeStorageOnReferralSuccess": m4, "freeStorageUsable": MessageLookupByLibrary.simpleMessage("可用的免费存储"), "freeTrial": MessageLookupByLibrary.simpleMessage("免费试用"), - "freeTrialValidTill": m37, - "freeUpAccessPostDelete": m38, - "freeUpAmount": m39, + "freeTrialValidTill": m39, + "freeUpAccessPostDelete": m40, + "freeUpAmount": m41, "freeUpDeviceSpace": MessageLookupByLibrary.simpleMessage("释放设备空间"), "freeUpDeviceSpaceDesc": MessageLookupByLibrary.simpleMessage("通过清除已备份的文件来节省设备空间。"), "freeUpSpace": MessageLookupByLibrary.simpleMessage("释放空间"), - "freeUpSpaceSaving": m40, + "freeUpSpaceSaving": m42, "gallery": MessageLookupByLibrary.simpleMessage("图库"), "galleryMemoryLimitInfo": MessageLookupByLibrary.simpleMessage("在图库中显示最多1000个回忆"), "general": MessageLookupByLibrary.simpleMessage("通用"), "generatingEncryptionKeys": MessageLookupByLibrary.simpleMessage("正在生成加密密钥..."), - "genericProgress": m41, + "genericProgress": m43, "goToSettings": MessageLookupByLibrary.simpleMessage("前往设置"), "googlePlayId": MessageLookupByLibrary.simpleMessage("Google Play ID"), "grantFullAccessPrompt": @@ -860,6 +874,7 @@ class MessageLookup extends MessageLookupByLibrary { "indexedItems": MessageLookupByLibrary.simpleMessage("已索引项目"), "indexingIsPaused": MessageLookupByLibrary.simpleMessage("索引已暂停。当设备准备就绪时,它将自动恢复。"), + "ineligible": MessageLookupByLibrary.simpleMessage("不合格"), "info": MessageLookupByLibrary.simpleMessage("详情"), "insecureDevice": MessageLookupByLibrary.simpleMessage("设备不安全"), "installManually": MessageLookupByLibrary.simpleMessage("手动安装"), @@ -879,13 +894,15 @@ class MessageLookup extends MessageLookupByLibrary { "itLooksLikeSomethingWentWrongPleaseRetryAfterSome": MessageLookupByLibrary.simpleMessage( "看起来出了点问题。 请稍后重试。 如果错误仍然存在,请联系我们的支持团队。"), - "itemCount": m42, + "itemCount": m44, "itemsShowTheNumberOfDaysRemainingBeforePermanentDeletion": MessageLookupByLibrary.simpleMessage("项目显示永久删除前剩余的天数"), "itemsWillBeRemovedFromAlbum": MessageLookupByLibrary.simpleMessage("所选项目将从此相册中移除"), "join": MessageLookupByLibrary.simpleMessage("加入"), "joinAlbum": MessageLookupByLibrary.simpleMessage("加入相册"), + "joinAlbumConfirmationDialogBody": + MessageLookupByLibrary.simpleMessage("加入相册将使相册的参与者可以看到您的电子邮件地址。"), "joinAlbumSubtext": MessageLookupByLibrary.simpleMessage("来查看和添加您的照片"), "joinAlbumSubtextViewer": MessageLookupByLibrary.simpleMessage("来将其添加到共享相册"), @@ -903,22 +920,30 @@ class MessageLookup extends MessageLookupByLibrary { "left": MessageLookupByLibrary.simpleMessage("向左"), "legacy": MessageLookupByLibrary.simpleMessage("遗产"), "legacyAccounts": MessageLookupByLibrary.simpleMessage("遗产账户"), - "legacyInvite": m43, + "legacyInvite": m45, "legacyPageDesc": MessageLookupByLibrary.simpleMessage("遗产允许信任的联系人在您不在时访问您的账户。"), "legacyPageDesc2": MessageLookupByLibrary.simpleMessage( "可信联系人可以启动账户恢复,如果 30 天内没有被阻止,则可以重置密码并访问您的账户。"), "light": MessageLookupByLibrary.simpleMessage("亮度"), "lightTheme": MessageLookupByLibrary.simpleMessage("浅色"), + "link": MessageLookupByLibrary.simpleMessage("链接"), "linkCopiedToClipboard": MessageLookupByLibrary.simpleMessage("链接已复制到剪贴板"), "linkDeviceLimit": MessageLookupByLibrary.simpleMessage("设备限制"), + "linkEmail": MessageLookupByLibrary.simpleMessage("链接邮箱"), + "linkEmailToContactBannerCaption": + MessageLookupByLibrary.simpleMessage("来实现更快的共享"), "linkEnabled": MessageLookupByLibrary.simpleMessage("已启用"), "linkExpired": MessageLookupByLibrary.simpleMessage("已过期"), - "linkExpiresOn": m44, + "linkExpiresOn": m46, "linkExpiry": MessageLookupByLibrary.simpleMessage("链接过期"), "linkHasExpired": MessageLookupByLibrary.simpleMessage("链接已过期"), "linkNeverExpires": MessageLookupByLibrary.simpleMessage("永不"), + "linkPerson": MessageLookupByLibrary.simpleMessage("链接人员"), + "linkPersonCaption": MessageLookupByLibrary.simpleMessage("来感受更好的共享体验"), + "linkPersonToEmail": m47, + "linkPersonToEmailConfirmation": m48, "livePhotos": MessageLookupByLibrary.simpleMessage("实况照片"), "loadMessage1": MessageLookupByLibrary.simpleMessage("您可以与家庭分享您的订阅"), "loadMessage2": @@ -990,6 +1015,7 @@ class MessageLookup extends MessageLookupByLibrary { "maps": MessageLookupByLibrary.simpleMessage("地图"), "mastodon": MessageLookupByLibrary.simpleMessage("Mastodon"), "matrix": MessageLookupByLibrary.simpleMessage("Matrix"), + "me": MessageLookupByLibrary.simpleMessage("我"), "memoryCount": m5, "merchandise": MessageLookupByLibrary.simpleMessage("商品"), "mergeWithExisting": MessageLookupByLibrary.simpleMessage("与现有的合并"), @@ -1015,10 +1041,10 @@ class MessageLookup extends MessageLookupByLibrary { "moreDetails": MessageLookupByLibrary.simpleMessage("更多详情"), "mostRecent": MessageLookupByLibrary.simpleMessage("最近"), "mostRelevant": MessageLookupByLibrary.simpleMessage("最相关"), - "moveItem": m45, + "moveItem": m49, "moveToAlbum": MessageLookupByLibrary.simpleMessage("移动到相册"), "moveToHiddenAlbum": MessageLookupByLibrary.simpleMessage("移至隐藏相册"), - "movedSuccessfullyTo": m46, + "movedSuccessfullyTo": m50, "movedToTrash": MessageLookupByLibrary.simpleMessage("已移至回收站"), "movingFilesToAlbum": MessageLookupByLibrary.simpleMessage("正在将文件移动到相册..."), @@ -1043,6 +1069,8 @@ class MessageLookup extends MessageLookupByLibrary { "noDeviceThatCanBeDeleted": MessageLookupByLibrary.simpleMessage("您在此设备上没有可被删除的文件"), "noDuplicates": MessageLookupByLibrary.simpleMessage("✨ 没有重复内容"), + "noEnteAccountExclamation": + MessageLookupByLibrary.simpleMessage("没有 Ente 账户!"), "noExifData": MessageLookupByLibrary.simpleMessage("无 EXIF 数据"), "noFacesFound": MessageLookupByLibrary.simpleMessage("未找到任何面部"), "noHiddenPhotosOrVideos": @@ -1059,9 +1087,9 @@ class MessageLookup extends MessageLookupByLibrary { "由于我们端到端加密协议的性质,如果没有您的密码或恢复密钥,您的数据将无法解密"), "noResults": MessageLookupByLibrary.simpleMessage("无结果"), "noResultsFound": MessageLookupByLibrary.simpleMessage("未找到任何结果"), - "noSuggestionsForPerson": m47, + "noSuggestionsForPerson": m51, "noSystemLockFound": MessageLookupByLibrary.simpleMessage("未找到系统锁"), - "notPersonLabel": m48, + "notPersonLabel": m52, "nothingSharedWithYouYet": MessageLookupByLibrary.simpleMessage("尚未与您共享任何内容"), "nothingToSeeHere": MessageLookupByLibrary.simpleMessage("这里空空如也! 👀"), @@ -1070,7 +1098,7 @@ class MessageLookup extends MessageLookupByLibrary { "onDevice": MessageLookupByLibrary.simpleMessage("在设备上"), "onEnte": MessageLookupByLibrary.simpleMessage( "在 ente 上"), - "onlyFamilyAdminCanChangeCode": m49, + "onlyFamilyAdminCanChangeCode": m53, "onlyThem": MessageLookupByLibrary.simpleMessage("仅限他们"), "oops": MessageLookupByLibrary.simpleMessage("哎呀"), "oopsCouldNotSaveEdits": @@ -1092,7 +1120,7 @@ class MessageLookup extends MessageLookupByLibrary { "orPickAnExistingOne": MessageLookupByLibrary.simpleMessage("或者选择一个现有的"), "orPickFromYourContacts": - MessageLookupByLibrary.simpleMessage("or pick from your contacts"), + MessageLookupByLibrary.simpleMessage("或从您的联系人中选择"), "pair": MessageLookupByLibrary.simpleMessage("配对"), "pairWithPin": MessageLookupByLibrary.simpleMessage("用 PIN 配对"), "pairingComplete": MessageLookupByLibrary.simpleMessage("配对完成"), @@ -1114,7 +1142,7 @@ class MessageLookup extends MessageLookupByLibrary { "paymentFailed": MessageLookupByLibrary.simpleMessage("支付失败"), "paymentFailedMessage": MessageLookupByLibrary.simpleMessage( "不幸的是,您的付款失败。请联系支持人员,我们将为您提供帮助!"), - "paymentFailedTalkToProvider": m50, + "paymentFailedTalkToProvider": m54, "pendingItems": MessageLookupByLibrary.simpleMessage("待处理项目"), "pendingSync": MessageLookupByLibrary.simpleMessage("正在等待同步"), "people": MessageLookupByLibrary.simpleMessage("人物"), @@ -1131,12 +1159,14 @@ class MessageLookup extends MessageLookupByLibrary { "photos": MessageLookupByLibrary.simpleMessage("照片"), "photosAddedByYouWillBeRemovedFromTheAlbum": MessageLookupByLibrary.simpleMessage("您添加的照片将从相册中移除"), - "photosCount": m51, + "photosCount": m55, "pickCenterPoint": MessageLookupByLibrary.simpleMessage("选择中心点"), "pinAlbum": MessageLookupByLibrary.simpleMessage("置顶相册"), "pinLock": MessageLookupByLibrary.simpleMessage("PIN 锁定"), "playOnTv": MessageLookupByLibrary.simpleMessage("在电视上播放相册"), - "playStoreFreeTrialValidTill": m52, + "playOriginal": MessageLookupByLibrary.simpleMessage("播放原内容"), + "playStoreFreeTrialValidTill": m56, + "playStream": MessageLookupByLibrary.simpleMessage("播放流"), "playstoreSubscription": MessageLookupByLibrary.simpleMessage("PlayStore 订阅"), "pleaseCheckYourInternetConnectionAndTryAgain": @@ -1146,12 +1176,12 @@ class MessageLookup extends MessageLookupByLibrary { "请用英语联系 support@ente.io ,我们将乐意提供帮助!"), "pleaseContactSupportIfTheProblemPersists": MessageLookupByLibrary.simpleMessage("如果问题仍然存在,请联系支持"), - "pleaseEmailUsAt": m53, + "pleaseEmailUsAt": m57, "pleaseGrantPermissions": MessageLookupByLibrary.simpleMessage("请授予权限"), "pleaseLoginAgain": MessageLookupByLibrary.simpleMessage("请重新登录"), "pleaseSelectQuickLinksToRemove": MessageLookupByLibrary.simpleMessage("请选择要删除的快速链接"), - "pleaseSendTheLogsTo": m54, + "pleaseSendTheLogsTo": m58, "pleaseTryAgain": MessageLookupByLibrary.simpleMessage("请重试"), "pleaseVerifyTheCodeYouHaveEntered": MessageLookupByLibrary.simpleMessage("请验证您输入的代码"), @@ -1172,21 +1202,27 @@ class MessageLookup extends MessageLookupByLibrary { "privateSharing": MessageLookupByLibrary.simpleMessage("私人分享"), "proceed": MessageLookupByLibrary.simpleMessage("继续"), "processed": MessageLookupByLibrary.simpleMessage("已处理"), - "processingImport": m55, + "processing": MessageLookupByLibrary.simpleMessage("正在处理"), + "processingImport": m59, + "processingVideos": MessageLookupByLibrary.simpleMessage("正在处理视频"), "publicLinkCreated": MessageLookupByLibrary.simpleMessage("公共链接已创建"), "publicLinkEnabled": MessageLookupByLibrary.simpleMessage("公开链接已启用"), + "queued": MessageLookupByLibrary.simpleMessage("已入列"), "quickLinks": MessageLookupByLibrary.simpleMessage("快速链接"), "radius": MessageLookupByLibrary.simpleMessage("半径"), "raiseTicket": MessageLookupByLibrary.simpleMessage("提升工单"), "rateTheApp": MessageLookupByLibrary.simpleMessage("为此应用评分"), "rateUs": MessageLookupByLibrary.simpleMessage("给我们评分"), - "rateUsOnStore": m56, + "rateUsOnStore": m60, + "reassignMe": MessageLookupByLibrary.simpleMessage("重新分配“我”"), + "reassignedToName": m61, + "reassigningLoading": MessageLookupByLibrary.simpleMessage("正在重新分配..."), "recover": MessageLookupByLibrary.simpleMessage("恢复"), "recoverAccount": MessageLookupByLibrary.simpleMessage("恢复账户"), "recoverButton": MessageLookupByLibrary.simpleMessage("恢复"), "recoveryAccount": MessageLookupByLibrary.simpleMessage("恢复账户"), "recoveryInitiated": MessageLookupByLibrary.simpleMessage("已启动恢复"), - "recoveryInitiatedDesc": m57, + "recoveryInitiatedDesc": m62, "recoveryKey": MessageLookupByLibrary.simpleMessage("恢复密钥"), "recoveryKeyCopiedToClipboard": MessageLookupByLibrary.simpleMessage("恢复密钥已复制到剪贴板"), @@ -1199,11 +1235,11 @@ class MessageLookup extends MessageLookupByLibrary { "recoveryKeyVerified": MessageLookupByLibrary.simpleMessage("恢复密钥已验证"), "recoveryKeyVerifyReason": MessageLookupByLibrary.simpleMessage( "如果您忘记了密码,恢复密钥是恢复照片的唯一方法。您可以在“设置”>“账户”中找到恢复密钥。\n\n请在此处输入恢复密钥,以验证您是否已正确保存。"), - "recoveryReady": m58, + "recoveryReady": m63, "recoverySuccessful": MessageLookupByLibrary.simpleMessage("恢复成功!"), "recoveryWarning": MessageLookupByLibrary.simpleMessage("一位可信联系人正在尝试访问您的账户"), - "recoveryWarningBody": m59, + "recoveryWarningBody": m64, "recreatePasswordBody": MessageLookupByLibrary.simpleMessage( "当前设备的功能不足以验证您的密码,但我们可以以适用于所有设备的方式重新生成。\n\n请使用您的恢复密钥登录并重新生成您的密码(如果您希望,可以再次使用相同的密码)。"), "recreatePasswordTitle": MessageLookupByLibrary.simpleMessage("重新创建密码"), @@ -1214,7 +1250,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("把我们推荐给你的朋友然后获得延长一倍的订阅计划"), "referralStep1": MessageLookupByLibrary.simpleMessage("1. 将此代码提供给您的朋友"), "referralStep2": MessageLookupByLibrary.simpleMessage("2. 他们注册一个付费计划"), - "referralStep3": m60, + "referralStep3": m65, "referrals": MessageLookupByLibrary.simpleMessage("推荐"), "referralsAreCurrentlyPaused": MessageLookupByLibrary.simpleMessage("推荐已暂停"), @@ -1237,7 +1273,7 @@ class MessageLookup extends MessageLookupByLibrary { "removeInvite": MessageLookupByLibrary.simpleMessage("移除邀请"), "removeLink": MessageLookupByLibrary.simpleMessage("移除链接"), "removeParticipant": MessageLookupByLibrary.simpleMessage("移除参与者"), - "removeParticipantBody": m61, + "removeParticipantBody": m66, "removePersonLabel": MessageLookupByLibrary.simpleMessage("移除人物标签"), "removePublicLink": MessageLookupByLibrary.simpleMessage("删除公开链接"), "removePublicLinks": MessageLookupByLibrary.simpleMessage("删除公开链接"), @@ -1252,7 +1288,7 @@ class MessageLookup extends MessageLookupByLibrary { "renameAlbum": MessageLookupByLibrary.simpleMessage("重命名相册"), "renameFile": MessageLookupByLibrary.simpleMessage("重命名文件"), "renewSubscription": MessageLookupByLibrary.simpleMessage("续费订阅"), - "renewsOn": m62, + "renewsOn": m67, "reportABug": MessageLookupByLibrary.simpleMessage("报告错误"), "reportBug": MessageLookupByLibrary.simpleMessage("报告错误"), "resendEmail": MessageLookupByLibrary.simpleMessage("重新发送电子邮件"), @@ -1275,6 +1311,8 @@ class MessageLookup extends MessageLookupByLibrary { "rotateRight": MessageLookupByLibrary.simpleMessage("向右旋转"), "safelyStored": MessageLookupByLibrary.simpleMessage("安全存储"), "save": MessageLookupByLibrary.simpleMessage("保存"), + "saveChangesBeforeLeavingQuestion": + MessageLookupByLibrary.simpleMessage("离开之前要保存更改吗?"), "saveCollage": MessageLookupByLibrary.simpleMessage("保存拼贴"), "saveCopy": MessageLookupByLibrary.simpleMessage("保存副本"), "saveKey": MessageLookupByLibrary.simpleMessage("保存密钥"), @@ -1312,8 +1350,8 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("邀请他人,您将在此看到他们分享的所有照片"), "searchPersonsEmptySection": MessageLookupByLibrary.simpleMessage("处理和同步完成后,人物将显示在此处"), - "searchResultCount": m63, - "searchSectionsLengthMismatch": m64, + "searchResultCount": m68, + "searchSectionsLengthMismatch": m69, "security": MessageLookupByLibrary.simpleMessage("安全"), "seePublicAlbumLinksInApp": MessageLookupByLibrary.simpleMessage("在应用程序中查看公开相册链接"), @@ -1330,7 +1368,9 @@ class MessageLookup extends MessageLookupByLibrary { "selectLanguage": MessageLookupByLibrary.simpleMessage("选择语言"), "selectMailApp": MessageLookupByLibrary.simpleMessage("选择邮件应用"), "selectMorePhotos": MessageLookupByLibrary.simpleMessage("选择更多照片"), + "selectPersonToLink": MessageLookupByLibrary.simpleMessage("选择要链接的人"), "selectReason": MessageLookupByLibrary.simpleMessage("选择原因"), + "selectYourFace": MessageLookupByLibrary.simpleMessage("选择你的脸"), "selectYourPlan": MessageLookupByLibrary.simpleMessage("选择您的计划"), "selectedFilesAreNotOnEnte": MessageLookupByLibrary.simpleMessage("所选文件不在 Ente 上"), @@ -1339,7 +1379,7 @@ class MessageLookup extends MessageLookupByLibrary { "selectedItemsWillBeDeletedFromAllAlbumsAndMoved": MessageLookupByLibrary.simpleMessage("所选项目将从所有相册中删除并移动到回收站。"), "selectedPhotos": m6, - "selectedPhotosWithYours": m65, + "selectedPhotosWithYours": m70, "send": MessageLookupByLibrary.simpleMessage("发送"), "sendEmail": MessageLookupByLibrary.simpleMessage("发送电子邮件"), "sendInvite": MessageLookupByLibrary.simpleMessage("发送邀请"), @@ -1362,16 +1402,16 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("打开相册并点击右上角的分享按钮进行分享"), "shareAnAlbumNow": MessageLookupByLibrary.simpleMessage("立即分享相册"), "shareLink": MessageLookupByLibrary.simpleMessage("分享链接"), - "shareMyVerificationID": m66, + "shareMyVerificationID": m71, "shareOnlyWithThePeopleYouWant": MessageLookupByLibrary.simpleMessage("仅与您想要的人分享"), "shareTextConfirmOthersVerificationID": m7, "shareTextRecommendUsingEnte": MessageLookupByLibrary.simpleMessage("下载 Ente,让我们轻松共享高质量的原始照片和视频"), - "shareTextReferralCode": m67, + "shareTextReferralCode": m72, "shareWithNonenteUsers": MessageLookupByLibrary.simpleMessage("与非 Ente 用户共享"), - "shareWithPeopleSectionTitle": m68, + "shareWithPeopleSectionTitle": m73, "shareYourFirstAlbum": MessageLookupByLibrary.simpleMessage("分享您的第一个相册"), "sharedAlbumSectionDescription": MessageLookupByLibrary.simpleMessage( @@ -1382,7 +1422,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("新共享的照片"), "sharedPhotoNotificationsExplanation": MessageLookupByLibrary.simpleMessage("当有人将照片添加到您所属的共享相册时收到通知"), - "sharedWith": m69, + "sharedWith": m74, "sharedWithMe": MessageLookupByLibrary.simpleMessage("与我共享"), "sharedWithYou": MessageLookupByLibrary.simpleMessage("已与您共享"), "sharing": MessageLookupByLibrary.simpleMessage("正在分享..."), @@ -1395,11 +1435,11 @@ class MessageLookup extends MessageLookupByLibrary { "signOutOtherDevices": MessageLookupByLibrary.simpleMessage("登出其他设备"), "signUpTerms": MessageLookupByLibrary.simpleMessage( "我同意 服务条款隐私政策"), - "singleFileDeleteFromDevice": m70, + "singleFileDeleteFromDevice": m75, "singleFileDeleteHighlight": MessageLookupByLibrary.simpleMessage("它将从所有相册中删除。"), - "singleFileInBothLocalAndRemote": m71, - "singleFileInRemoteOnly": m72, + "singleFileInBothLocalAndRemote": m76, + "singleFileInRemoteOnly": m77, "skip": MessageLookupByLibrary.simpleMessage("跳过"), "social": MessageLookupByLibrary.simpleMessage("社交"), "someItemsAreInBothEnteAndYourDevice": @@ -1437,10 +1477,11 @@ class MessageLookup extends MessageLookupByLibrary { "storageBreakupYou": MessageLookupByLibrary.simpleMessage("您"), "storageInGB": m1, "storageLimitExceeded": MessageLookupByLibrary.simpleMessage("已超出存储限制"), - "storageUsageInfo": m73, + "storageUsageInfo": m78, + "streamDetails": MessageLookupByLibrary.simpleMessage("流详情"), "strongStrength": MessageLookupByLibrary.simpleMessage("强"), - "subAlreadyLinkedErrMessage": m74, - "subWillBeCancelledOn": m75, + "subAlreadyLinkedErrMessage": m79, + "subWillBeCancelledOn": m80, "subscribe": MessageLookupByLibrary.simpleMessage("订阅"), "subscribeToEnableSharing": MessageLookupByLibrary.simpleMessage("您需要有效的付费订阅才能启用共享。"), @@ -1453,7 +1494,7 @@ class MessageLookup extends MessageLookupByLibrary { "successfullyUnhid": MessageLookupByLibrary.simpleMessage("已成功取消隐藏"), "suggestFeatures": MessageLookupByLibrary.simpleMessage("建议新功能"), "support": MessageLookupByLibrary.simpleMessage("支持"), - "syncProgress": m76, + "syncProgress": m81, "syncStopped": MessageLookupByLibrary.simpleMessage("同步已停止"), "syncing": MessageLookupByLibrary.simpleMessage("正在同步···"), "systemTheme": MessageLookupByLibrary.simpleMessage("适应系统"), @@ -1461,7 +1502,7 @@ class MessageLookup extends MessageLookupByLibrary { "tapToEnterCode": MessageLookupByLibrary.simpleMessage("点击以输入代码"), "tapToUnlock": MessageLookupByLibrary.simpleMessage("点击解锁"), "tapToUpload": MessageLookupByLibrary.simpleMessage("点按上传"), - "tapToUploadIsIgnoredDue": m77, + "tapToUploadIsIgnoredDue": m82, "tempErrorContactSupportIfPersists": MessageLookupByLibrary.simpleMessage( "看起来出了点问题。 请稍后重试。 如果错误仍然存在,请联系我们的支持团队。"), @@ -1495,7 +1536,8 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("这个邮箱地址已经被使用"), "thisImageHasNoExifData": MessageLookupByLibrary.simpleMessage("此图像没有Exif 数据"), - "thisIsPersonVerificationId": m78, + "thisIsMeExclamation": MessageLookupByLibrary.simpleMessage("这就是我!"), + "thisIsPersonVerificationId": m83, "thisIsYourVerificationId": MessageLookupByLibrary.simpleMessage("这是您的验证 ID"), "thisWillLogYouOutOfTheFollowingDevice": @@ -1515,10 +1557,10 @@ class MessageLookup extends MessageLookupByLibrary { "total": MessageLookupByLibrary.simpleMessage("总计"), "totalSize": MessageLookupByLibrary.simpleMessage("总大小"), "trash": MessageLookupByLibrary.simpleMessage("回收站"), - "trashDaysLeft": m79, + "trashDaysLeft": m84, "trim": MessageLookupByLibrary.simpleMessage("修剪"), "trustedContacts": MessageLookupByLibrary.simpleMessage("可信联系人"), - "trustedInviteBody": m80, + "trustedInviteBody": m85, "tryAgain": MessageLookupByLibrary.simpleMessage("请再试一次"), "turnOnBackupForAutoUpload": MessageLookupByLibrary.simpleMessage( "打开备份可自动上传添加到此设备文件夹的文件至 Ente。"), @@ -1533,7 +1575,7 @@ class MessageLookup extends MessageLookupByLibrary { "twofactorAuthenticationSuccessfullyReset": MessageLookupByLibrary.simpleMessage("成功重置双重认证"), "twofactorSetup": MessageLookupByLibrary.simpleMessage("双重认证设置"), - "typeOfGallerGallerytypeIsNotSupportedForRename": m81, + "typeOfGallerGallerytypeIsNotSupportedForRename": m86, "unarchive": MessageLookupByLibrary.simpleMessage("取消存档"), "unarchiveAlbum": MessageLookupByLibrary.simpleMessage("取消存档相册"), "unarchiving": MessageLookupByLibrary.simpleMessage("正在取消存档..."), @@ -1553,10 +1595,10 @@ class MessageLookup extends MessageLookupByLibrary { "updatingFolderSelection": MessageLookupByLibrary.simpleMessage("正在更新文件夹选择..."), "upgrade": MessageLookupByLibrary.simpleMessage("升级"), - "uploadIsIgnoredDueToIgnorereason": m82, + "uploadIsIgnoredDueToIgnorereason": m87, "uploadingFilesToAlbum": MessageLookupByLibrary.simpleMessage("正在将文件上传到相册..."), - "uploadingMultipleMemories": m83, + "uploadingMultipleMemories": m88, "uploadingSingleMemory": MessageLookupByLibrary.simpleMessage("正在保存 1 个回忆..."), "upto50OffUntil4thDec": @@ -1571,13 +1613,13 @@ class MessageLookup extends MessageLookupByLibrary { "useRecoveryKey": MessageLookupByLibrary.simpleMessage("使用恢复密钥"), "useSelectedPhoto": MessageLookupByLibrary.simpleMessage("使用所选照片"), "usedSpace": MessageLookupByLibrary.simpleMessage("已用空间"), - "validTill": m84, + "validTill": m89, "verificationFailedPleaseTryAgain": MessageLookupByLibrary.simpleMessage("验证失败,请重试"), "verificationId": MessageLookupByLibrary.simpleMessage("验证 ID"), "verify": MessageLookupByLibrary.simpleMessage("验证"), "verifyEmail": MessageLookupByLibrary.simpleMessage("验证电子邮件"), - "verifyEmailID": m85, + "verifyEmailID": m90, "verifyIDLabel": MessageLookupByLibrary.simpleMessage("验证"), "verifyPasskey": MessageLookupByLibrary.simpleMessage("验证通行密钥"), "verifyPassword": MessageLookupByLibrary.simpleMessage("验证密码"), @@ -1586,6 +1628,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("正在验证恢复密钥..."), "videoInfo": MessageLookupByLibrary.simpleMessage("视频详情"), "videoSmallCase": MessageLookupByLibrary.simpleMessage("视频"), + "videoStreaming": MessageLookupByLibrary.simpleMessage("影音流"), "videos": MessageLookupByLibrary.simpleMessage("视频"), "viewActiveSessions": MessageLookupByLibrary.simpleMessage("查看活动会话"), "viewAddOnButton": MessageLookupByLibrary.simpleMessage("查看附加组件"), @@ -1597,7 +1640,7 @@ class MessageLookup extends MessageLookupByLibrary { "viewLogs": MessageLookupByLibrary.simpleMessage("查看日志"), "viewRecoveryKey": MessageLookupByLibrary.simpleMessage("查看恢复密钥"), "viewer": MessageLookupByLibrary.simpleMessage("查看者"), - "viewersSuccessfullyAdded": m86, + "viewersSuccessfullyAdded": m91, "visitWebToManage": MessageLookupByLibrary.simpleMessage("请访问 web.ente.io 来管理您的订阅"), "waitingForVerification": @@ -1615,7 +1658,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("可信联系人可以帮助恢复您的数据。"), "yearShort": MessageLookupByLibrary.simpleMessage("年"), "yearly": MessageLookupByLibrary.simpleMessage("每年"), - "yearsAgo": m87, + "yearsAgo": m92, "yes": MessageLookupByLibrary.simpleMessage("是"), "yesCancel": MessageLookupByLibrary.simpleMessage("是的,取消"), "yesConvertToViewer": MessageLookupByLibrary.simpleMessage("是的,转换为查看者"), @@ -1642,7 +1685,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("莫开玩笑,您不能与自己分享"), "youDontHaveAnyArchivedItems": MessageLookupByLibrary.simpleMessage("您没有任何存档的项目。"), - "youHaveSuccessfullyFreedUp": m88, + "youHaveSuccessfullyFreedUp": m93, "yourAccountHasBeenDeleted": MessageLookupByLibrary.simpleMessage("您的账户已删除"), "yourMap": MessageLookupByLibrary.simpleMessage("您的地图"), @@ -1660,8 +1703,6 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("您的订阅已成功更新"), "yourVerificationCodeHasExpired": MessageLookupByLibrary.simpleMessage("您的验证码已过期"), - "youveNoDuplicateFilesThatCanBeCleared": - MessageLookupByLibrary.simpleMessage("您没有可以被清除的重复文件"), "youveNoFilesInThisAlbumThatCanBeDeleted": MessageLookupByLibrary.simpleMessage("您在此相册中没有可以删除的文件"), "zoomOutToSeePhotos": MessageLookupByLibrary.simpleMessage("缩小以查看照片") diff --git a/mobile/lib/generated/l10n.dart b/mobile/lib/generated/l10n.dart index c7f32203dd..947d3c6060 100644 --- a/mobile/lib/generated/l10n.dart +++ b/mobile/lib/generated/l10n.dart @@ -3863,10 +3863,10 @@ class S { ); } - /// `You've no duplicate files that can be cleared` + /// `You don't have any duplicate files that can be cleared` String get youveNoDuplicateFilesThatCanBeCleared { return Intl.message( - 'You\'ve no duplicate files that can be cleared', + 'You don\'t have any duplicate files that can be cleared', name: 'youveNoDuplicateFilesThatCanBeCleared', desc: '', args: [], @@ -11230,6 +11230,16 @@ class S { args: [], ); } + + /// `Please wait, this will take a while.` + String get pleaseWaitThisWillTakeAWhile { + return Intl.message( + 'Please wait, this will take a while.', + name: 'pleaseWaitThisWillTakeAWhile', + desc: '', + args: [], + ); + } } class AppLocalizationDelegate extends LocalizationsDelegate { diff --git a/mobile/lib/l10n/intl_ar.arb b/mobile/lib/l10n/intl_ar.arb index cc65c1038f..86273581a6 100644 --- a/mobile/lib/l10n/intl_ar.arb +++ b/mobile/lib/l10n/intl_ar.arb @@ -23,6 +23,5 @@ "noRecoveryKeyNoDecryption": "لا يمكن فك تشفير بياناتك دون كلمة المرور أو مفتاح الاسترداد بسبب طبيعة بروتوكول التشفير الخاص بنا من النهاية إلى النهاية", "verifyEmail": "التحقق من البريد الإلكتروني", "toResetVerifyEmail": "لإعادة تعيين كلمة المرور، يرجى التحقق من بريدك الإلكتروني أولاً.", - "ackPasswordLostWarning": "أُدركُ أنّني فقدتُ كلمة مروري، فقد أفقد بياناتي لأن بياناتي مشفرة تشفيرًا تامًّا من النهاية إلى النهاية.", - "orPickFromYourContacts": "or pick from your contacts" + "ackPasswordLostWarning": "أُدركُ أنّني فقدتُ كلمة مروري، فقد أفقد بياناتي لأن بياناتي مشفرة تشفيرًا تامًّا من النهاية إلى النهاية." } \ No newline at end of file diff --git a/mobile/lib/l10n/intl_be.arb b/mobile/lib/l10n/intl_be.arb index 2339063ba5..d6b7f03339 100644 --- a/mobile/lib/l10n/intl_be.arb +++ b/mobile/lib/l10n/intl_be.arb @@ -199,6 +199,5 @@ "darkTheme": "Цёмная", "systemTheme": "Сістэма", "freeTrial": "Бясплатная пробная версія", - "faqs": "Частыя пытанні", - "orPickFromYourContacts": "or pick from your contacts" + "faqs": "Частыя пытанні" } \ No newline at end of file diff --git a/mobile/lib/l10n/intl_bg.arb b/mobile/lib/l10n/intl_bg.arb index 1a5f45c37d..c8494661c6 100644 --- a/mobile/lib/l10n/intl_bg.arb +++ b/mobile/lib/l10n/intl_bg.arb @@ -1,4 +1,3 @@ { - "@@locale ": "en", - "orPickFromYourContacts": "or pick from your contacts" + "@@locale ": "en" } \ No newline at end of file diff --git a/mobile/lib/l10n/intl_ca.arb b/mobile/lib/l10n/intl_ca.arb index 1a5f45c37d..c8494661c6 100644 --- a/mobile/lib/l10n/intl_ca.arb +++ b/mobile/lib/l10n/intl_ca.arb @@ -1,4 +1,3 @@ { - "@@locale ": "en", - "orPickFromYourContacts": "or pick from your contacts" + "@@locale ": "en" } \ No newline at end of file diff --git a/mobile/lib/l10n/intl_cs.arb b/mobile/lib/l10n/intl_cs.arb index 78b8961406..2bd9d2da70 100644 --- a/mobile/lib/l10n/intl_cs.arb +++ b/mobile/lib/l10n/intl_cs.arb @@ -2,6 +2,5 @@ "@@locale ": "en", "askDeleteReason": "Jaký je váš hlavní důvod, proč mažete svůj účet?", "incorrectRecoveryKeyBody": "", - "checkInboxAndSpamFolder": "Zkontrolujte prosím svou doručenou poštu (a spam) pro dokončení ověření", - "orPickFromYourContacts": "or pick from your contacts" + "checkInboxAndSpamFolder": "Zkontrolujte prosím svou doručenou poštu (a spam) pro dokončení ověření" } \ No newline at end of file diff --git a/mobile/lib/l10n/intl_da.arb b/mobile/lib/l10n/intl_da.arb index 93a3c2bde1..d29d7c345e 100644 --- a/mobile/lib/l10n/intl_da.arb +++ b/mobile/lib/l10n/intl_da.arb @@ -242,6 +242,5 @@ "longPressAnEmailToVerifyEndToEndEncryption": "Langt tryk på en e-mail for at bekræfte slutningen af krypteringen.", "developerSettingsWarning": "Er du sikker på, at du vil ændre udviklerindstillingerne?", "next": "Næste", - "enterPin": "Indtast PIN", - "orPickFromYourContacts": "or pick from your contacts" + "enterPin": "Indtast PIN" } \ No newline at end of file diff --git a/mobile/lib/l10n/intl_de.arb b/mobile/lib/l10n/intl_de.arb index eccc1c014e..d76bc3ecb0 100644 --- a/mobile/lib/l10n/intl_de.arb +++ b/mobile/lib/l10n/intl_de.arb @@ -525,7 +525,6 @@ "viewLargeFiles": "Große Dateien", "viewLargeFilesDesc": "Dateien anzeigen, die den meisten Speicherplatz belegen.", "noDuplicates": "✨ Keine Duplikate", - "youveNoDuplicateFilesThatCanBeCleared": "Du hast keine Duplikate, die gelöscht werden können", "success": "Abgeschlossen", "rateUs": "Bewerte uns", "remindToEmptyDeviceTrash": "Lösche auch Dateien aus \"Kürzlich gelöscht\" unter \"Einstellungen\" -> \"Speicher\" um freien Speicher zu erhalten", @@ -965,6 +964,14 @@ "syncStopped": "Synchronisierung angehalten", "syncProgress": "{completed}/{total} Erinnerungsstücke gesichert", "uploadingMultipleMemories": "Sichere {count} Erinnerungsstücke...", + "@uploadingMultipleMemories": { + "description": "Text to tell user how many memories are being preserved", + "placeholders": { + "count": { + "type": "String" + } + } + }, "uploadingSingleMemory": "Sichere ein Erinnerungsstück...", "@syncProgress": { "description": "Text to tell user how many memories have been preserved", @@ -1403,6 +1410,7 @@ "description": "Number of viewers that were successfully added to an album." }, "collaboratorsSuccessfullyAdded": "{count, plural, =0 {0 Mitarbeiter hinzugefügt} =1 {1 Mitarbeiter hinzugefügt} other {{count} Mitarbeiter hinzugefügt}}", + "@collaboratorsSuccessfullyAdded": { "placeholders": { "count": { @@ -1590,7 +1598,80 @@ "joinAlbumSubtext": "um deine Fotos anzuzeigen und hinzuzufügen", "joinAlbumSubtextViewer": "um dies zu geteilten Alben hinzuzufügen", "join": "Beitreten", - "link": "Link", + "linkEmail": "E-Mail-Adresse verknüpfen", + "link": "Verknüpfen", "noEnteAccountExclamation": "Kein Ente-Konto!", - "orPickFromYourContacts": "or pick from your contacts" + "emailDoesNotHaveEnteAccount": "{email} hat kein Ente-Konto.", + "@emailDoesNotHaveEnteAccount": { + "description": "Shown when email doesn't have an Ente account", + "placeholders": { + "email": { + "type": "String" + } + } + }, + "accountOwnerPersonAppbarTitle": "{title} (Ich)", + "@accountOwnerPersonAppbarTitle": { + "description": "Title of appbar for account owner person", + "placeholders": { + "title": { + "type": "String" + } + } + }, + "reassignMe": "\"Ich\" neu zuweisen", + "me": "Ich", + "linkEmailToContactBannerCaption": "für schnelleres Teilen", + "@linkEmailToContactBannerCaption": { + "description": "Caption for the 'Link email' title. It should be a continuation of the 'Link email' title. Just like how 'Link email' + 'for faster sharing' forms a proper sentence in English, the combination of these two strings should also be a proper sentence in other languages." + }, + "selectPersonToLink": "Person zum Verknüpfen auswählen", + "linkPersonToEmail": "Person mit {email} verknüpfen", + "@linkPersonToEmail": { + "placeholders": { + "email": { + "type": "String" + } + } + }, + "linkPersonToEmailConfirmation": "Dies wird {personName} mit {email} verknüpfen", + "@linkPersonToEmailConfirmation": { + "description": "Confirmation message when linking a person to an email", + "placeholders": { + "personName": { + "type": "String" + }, + "email": { + "type": "String" + } + } + }, + "selectYourFace": "Wähle dein Gesicht", + "reassigningLoading": "Ordne neu zu...", + "reassignedToName": "Du wurdest an {name} neu zugewiesen", + "@reassignedToName": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "saveChangesBeforeLeavingQuestion": "Änderungen vor dem Verlassen speichern?", + "dontSave": "Nicht speichern", + "thisIsMeExclamation": "Das bin ich!", + "linkPerson": "Person verknüpfen", + "linkPersonCaption": "um besseres Teilen zu ermöglichen", + "@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", + "processingVideos": "Verarbeite Videos", + "streamDetails": "Stream-Details", + "processing": "In Bearbeitung", + "queued": "In der Warteschlange", + "ineligible": "Unzulässig", + "failed": "Fehlgeschlagen", + "playStream": "Stream abspielen", + "playOriginal": "Original abspielen", + "joinAlbumConfirmationDialogBody": "Wenn du einem Album beitrittst, wird deine E-Mail-Adresse für seine Teilnehmer sichtbar." } \ No newline at end of file diff --git a/mobile/lib/l10n/intl_el.arb b/mobile/lib/l10n/intl_el.arb index a3e70ec1ee..ce8b1a1a54 100644 --- a/mobile/lib/l10n/intl_el.arb +++ b/mobile/lib/l10n/intl_el.arb @@ -1,5 +1,4 @@ { "@@locale ": "en", - "enterYourEmailAddress": "Εισάγετε την διεύθυνση ηλ. ταχυδρομείου σας", - "orPickFromYourContacts": "or pick from your contacts" + "enterYourEmailAddress": "Εισάγετε την διεύθυνση ηλ. ταχυδρομείου σας" } \ No newline at end of file diff --git a/mobile/lib/l10n/intl_en.arb b/mobile/lib/l10n/intl_en.arb index 9c29390b16..58e80714fe 100644 --- a/mobile/lib/l10n/intl_en.arb +++ b/mobile/lib/l10n/intl_en.arb @@ -525,7 +525,7 @@ "viewLargeFiles": "Large files", "viewLargeFilesDesc": "View files that are consuming the most amount of storage.", "noDuplicates": "✨ No duplicates", - "youveNoDuplicateFilesThatCanBeCleared": "You've no duplicate files that can be cleared", + "youveNoDuplicateFilesThatCanBeCleared": "You don't have any duplicate files that can be cleared", "success": "Success", "rateUs": "Rate us", "remindToEmptyDeviceTrash": "Also empty \"Recently Deleted\" from \"Settings\" -> \"Storage\" to claim the freed space", @@ -1674,5 +1674,6 @@ "failed": "Failed", "playStream": "Play stream", "playOriginal": "Play original", - "joinAlbumConfirmationDialogBody" : "Joining an album will make your email visible to its participants." + "joinAlbumConfirmationDialogBody" : "Joining an album will make your email visible to its participants.", + "pleaseWaitThisWillTakeAWhile": "Please wait, this will take a while." } \ No newline at end of file diff --git a/mobile/lib/l10n/intl_es.arb b/mobile/lib/l10n/intl_es.arb index 4c070013e1..766f32f639 100644 --- a/mobile/lib/l10n/intl_es.arb +++ b/mobile/lib/l10n/intl_es.arb @@ -525,7 +525,6 @@ "viewLargeFiles": "Archivos grandes", "viewLargeFilesDesc": "Ver los archivos que consumen la mayor cantidad de almacenamiento.", "noDuplicates": "✨ Sin duplicados", - "youveNoDuplicateFilesThatCanBeCleared": "No tienes archivos duplicados que puedan ser borrados", "success": "Éxito", "rateUs": "Califícanos", "remindToEmptyDeviceTrash": "También vacía \"Eliminado Recientemente\" de \"Configuración\" -> \"Almacenamiento\" para reclamar el espacio libre", @@ -965,6 +964,14 @@ "syncStopped": "Sincronización detenida", "syncProgress": "{completed}/{total} recuerdos conservados", "uploadingMultipleMemories": "Preservando {count} memorias...", + "@uploadingMultipleMemories": { + "description": "Text to tell user how many memories are being preserved", + "placeholders": { + "count": { + "type": "String" + } + } + }, "uploadingSingleMemory": "Preservando 1 memoria...", "@syncProgress": { "description": "Text to tell user how many memories have been preserved", @@ -1593,5 +1600,78 @@ "linkEmail": "Vincular correo electrónico", "link": "Enlace", "noEnteAccountExclamation": "¡No existe una cuenta de Ente!", - "orPickFromYourContacts": "or pick from your contacts" + "orPickFromYourContacts": "o elige de entre tus contactos", + "emailDoesNotHaveEnteAccount": "{email} no tiene una cuenta de Ente.", + "@emailDoesNotHaveEnteAccount": { + "description": "Shown when email doesn't have an Ente account", + "placeholders": { + "email": { + "type": "String" + } + } + }, + "accountOwnerPersonAppbarTitle": "{title} (Yo)", + "@accountOwnerPersonAppbarTitle": { + "description": "Title of appbar for account owner person", + "placeholders": { + "title": { + "type": "String" + } + } + }, + "reassignMe": "Reasignar \"Yo\"", + "me": "Yo", + "linkEmailToContactBannerCaption": "para compartir más rápido", + "@linkEmailToContactBannerCaption": { + "description": "Caption for the 'Link email' title. It should be a continuation of the 'Link email' title. Just like how 'Link email' + 'for faster sharing' forms a proper sentence in English, the combination of these two strings should also be a proper sentence in other languages." + }, + "selectPersonToLink": "Selecciona persona a vincular", + "linkPersonToEmail": "Enlazar persona a {email}", + "@linkPersonToEmail": { + "placeholders": { + "email": { + "type": "String" + } + } + }, + "linkPersonToEmailConfirmation": "Esto enlazará a {personName} a {email}", + "@linkPersonToEmailConfirmation": { + "description": "Confirmation message when linking a person to an email", + "placeholders": { + "personName": { + "type": "String" + }, + "email": { + "type": "String" + } + } + }, + "selectYourFace": "Selecciona tu cara", + "reassigningLoading": "Reasignando...", + "reassignedToName": "Te has reasignado a {name}", + "@reassignedToName": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "saveChangesBeforeLeavingQuestion": "¿Guardar cambios antes de salir?", + "dontSave": "No guardar", + "thisIsMeExclamation": "¡Este soy yo!", + "linkPerson": "Vincular persona", + "linkPersonCaption": "para una mejor experiencia compartida", + "@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", + "queued": "En cola", + "ineligible": "Inelegible", + "failed": "Fallido", + "playStream": "Reproducir transmisión", + "playOriginal": "Reproducir original", + "joinAlbumConfirmationDialogBody": "Unirse a un álbum hará visible tu correo electrónico a sus participantes." } \ No newline at end of file diff --git a/mobile/lib/l10n/intl_et.arb b/mobile/lib/l10n/intl_et.arb index 85e9a51736..dfe1fba1a4 100644 --- a/mobile/lib/l10n/intl_et.arb +++ b/mobile/lib/l10n/intl_et.arb @@ -218,6 +218,5 @@ "storageBreakupYou": "Sina", "@storageBreakupYou": { "description": "Label to indicate how much storage you are using when you are part of a family plan" - }, - "orPickFromYourContacts": "or pick from your contacts" + } } \ No newline at end of file diff --git a/mobile/lib/l10n/intl_fa.arb b/mobile/lib/l10n/intl_fa.arb index 7229950a22..ba9d203a8f 100644 --- a/mobile/lib/l10n/intl_fa.arb +++ b/mobile/lib/l10n/intl_fa.arb @@ -308,6 +308,5 @@ "developerSettings": "تنظیمات توسعه‌دهنده", "search": "جستجو", "whatsNew": "تغییرات جدید", - "reviewSuggestions": "مرور پیشنهادها", - "orPickFromYourContacts": "or pick from your contacts" + "reviewSuggestions": "مرور پیشنهادها" } \ No newline at end of file diff --git a/mobile/lib/l10n/intl_fr.arb b/mobile/lib/l10n/intl_fr.arb index adff93b6db..214aa6e930 100644 --- a/mobile/lib/l10n/intl_fr.arb +++ b/mobile/lib/l10n/intl_fr.arb @@ -2,8 +2,8 @@ "@@locale ": "en", "enterYourEmailAddress": "Entrez votre adresse e-mail", "accountWelcomeBack": "Bon retour parmi nous !", - "emailAlreadyRegistered": "E-mail déjà enregistré.", - "emailNotRegistered": "E-mail non enregistré.", + "emailAlreadyRegistered": "Email déjà enregistré.", + "emailNotRegistered": "Email inconnu.", "email": "E-mail", "cancel": "Annuler", "verify": "Vérifier", @@ -50,12 +50,12 @@ "noRecoveryKey": "Aucune clé de récupération ?", "sorry": "Désolé", "noRecoveryKeyNoDecryption": "En raison de notre protocole de chiffrement de bout en bout, vos données ne peuvent pas être déchiffré sans votre mot de passe ou clé de récupération", - "verifyEmail": "Vérifier l'e-mail", - "toResetVerifyEmail": "Pour réinitialiser votre mot de passe, veuillez d'abord vérifier votre e-mail.", - "checkInboxAndSpamFolder": "Veuillez consulter votre boîte de réception (ainsi que les indésirables) pour compléter la vérification", + "verifyEmail": "Vérifier l'email", + "toResetVerifyEmail": "Pour réinitialiser votre mot de passe, vérifiez d'abord votre email.", + "checkInboxAndSpamFolder": "Consultez votre boîte de réception (et les indésirables) pour finaliser la vérification", "tapToEnterCode": "Appuyez pour entrer le code", - "resendEmail": "Renvoyer l'e-mail", - "weHaveSendEmailTo": "Nous avons envoyé un e-mail à {email}", + "resendEmail": "Renvoyer l'email", + "weHaveSendEmailTo": "Nous avons envoyé un email à {email}", "@weHaveSendEmailTo": { "description": "Text to indicate that we have sent a mail to the user", "placeholders": { @@ -72,7 +72,7 @@ "encryptionKeys": "Clés de chiffrement", "passwordWarning": "Nous ne stockons pas ce mot de passe, donc si vous l'oubliez, nous ne pouvons pas déchiffrer vos données", "enterPasswordToEncrypt": "Entrez un mot de passe que nous pouvons utiliser pour chiffrer vos données", - "enterNewPasswordToEncrypt": "Saisir un nouveau mot de passe pour l'utiliser pour chiffrer vos données", + "enterNewPasswordToEncrypt": "Saisissez votre nouveau mot de passe qui sera utilisé pour chiffrer vos données", "weakStrength": "Securité Faible", "strongStrength": "Forte", "moderateStrength": "Moyen", @@ -114,7 +114,7 @@ "verifyPassword": "Vérifier le mot de passe", "recoveryKey": "Clé de secours", "recoveryKeyOnForgotPassword": "Si vous oubliez votre mot de passe, la seule façon de récupérer vos données sera grâce à cette clé.", - "recoveryKeySaveDescription": "Nous ne stockons pas cette clé, veuillez garder cette clé de 24 mots dans un endroit sûr.", + "recoveryKeySaveDescription": "Nous ne la stockons pas, veuillez la conserver en lieu endroit sûr.", "doThisLater": "Plus tard", "saveKey": "Enregistrer la clé", "recoveryKeyCopiedToClipboard": "Clé de secours copiée dans le presse-papiers", @@ -142,7 +142,7 @@ "setupComplete": "Configuration terminée", "saveYourRecoveryKeyIfYouHaventAlready": "Enregistrez votre clé de récupération si vous ne l'avez pas déjà fait", "thisCanBeUsedToRecoverYourAccountIfYou": "Cela peut être utilisé pour récupérer votre compte si vous perdez votre deuxième facteur", - "twofactorAuthenticationPageTitle": "Authentification à deux facteurs", + "twofactorAuthenticationPageTitle": "Authentification à deux facteurs (A2F)", "lostDevice": "Appareil perdu ?", "verifyingRecoveryKey": "Vérification de la clé de récupération...", "recoveryKeyVerified": "Clé de récupération vérifiée", @@ -159,7 +159,7 @@ "addANewEmail": "Ajouter un nouvel email", "orPickAnExistingOne": "Ou sélectionner un email existant", "collaboratorsCanAddPhotosAndVideosToTheSharedAlbum": "Les collaborateurs peuvent ajouter des photos et des vidéos à l'album partagé.", - "enterEmail": "Entrer e-mail", + "enterEmail": "Entrer un email", "albumOwner": "Propriétaire", "@albumOwner": { "description": "Role of the album owner" @@ -186,7 +186,7 @@ "description": "Switch button to enable uploading photos to a public link" }, "allowAddPhotosDescription": "Autorisez les personnes ayant le lien à ajouter des photos dans l'album partagé.", - "passwordLock": "Mot de passe verrou", + "passwordLock": "Verrouillage par mot de passe", "canNotOpenTitle": "Impossible d'ouvrir cet album", "canNotOpenBody": "Désolé, cet album ne peut pas être ouvert dans l'application.", "disableDownloadWarningTitle": "Veuillez remarquer", @@ -252,7 +252,7 @@ }, "thisIsYourVerificationId": "Ceci est votre ID de vérification", "someoneSharingAlbumsWithYouShouldSeeTheSameId": "Quelqu'un qui partage des albums avec vous devrait voir le même ID sur son appareil.", - "howToViewShareeVerificationID": "Demandez-leur d'appuyer longuement sur leur adresse e-mail sur l'écran des paramètres et de vérifier que les identifiants des deux appareils correspondent.", + "howToViewShareeVerificationID": "Demandez-leur d'appuyer longuement sur leur adresse email dans l'écran des paramètres pour vérifier que les identifiants des deux appareils correspondent.", "thisIsPersonVerificationId": "Ceci est l'ID de vérification de {email}", "@thisIsPersonVerificationId": { "placeholders": { @@ -272,10 +272,10 @@ "shareTextRecommendUsingEnte": "Téléchargez Ente pour pouvoir facilement partager des photos et vidéos en qualité originale\n\nhttps://ente.io", "done": "Terminé", "applyCodeTitle": "Utiliser le code", - "enterCodeDescription": "Entrez le code fourni par votre ami pour réclamer de l'espace de stockage gratuit pour vous deux", + "enterCodeDescription": "Entrez le code fourni par votre ami·e pour débloquer l'espace de stockage gratuit", "apply": "Appliquer", "failedToApplyCode": "Impossible d'appliquer le code", - "enterReferralCode": "Entrez le code de parrainage", + "enterReferralCode": "Code de parrainage", "codeAppliedPageTitle": "Code appliqué", "changeYourReferralCode": "Modifier votre code de parrainage", "change": "Modifier", @@ -292,15 +292,15 @@ "theyAlsoGetXGb": "Ils obtiennent aussi {storageAmountInGB} Go", "freeStorageOnReferralSuccess": "{storageAmountInGB} Go chaque fois que quelqu'un s'inscrit à une offre payante et applique votre code", "shareTextReferralCode": "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", - "claimFreeStorage": "Stockage gratuit obtenu", - "inviteYourFriends": "Invitez vos ami(e)s", + "claimFreeStorage": "Obtenez du stockage gratuit", + "inviteYourFriends": "Parrainez vos ami·e·s", "failedToFetchReferralDetails": "Impossible de récupérer les détails du parrainage. Veuillez réessayer plus tard.", - "referralStep1": "1. Donnez ce code à vos amis", - "referralStep2": "2. Ils s'inscrivent à une offre payante", - "referralStep3": "3. Vous recevez tous les deux {storageInGB} GB* gratuits", + "referralStep1": "1. Donnez ce code à vos ami·e·s", + "referralStep2": "2. Ils souscrivent à une offre payante", + "referralStep3": "3. Vous recevez tous les deux {storageInGB} Go* gratuits", "referralsAreCurrentlyPaused": "Les recommandations sont actuellement en pause", "youCanAtMaxDoubleYourStorage": "* Vous pouvez au maximum doubler votre espace de stockage", - "claimedStorageSoFar": "{isFamilyMember, select, true {Votre famille a demandé {storageAmountInGb} GB jusqu'à présent} false {Vous avez réclamé {storageAmountInGb} GB jusqu'à présent} other {Vous avez réclamé {storageAmountInGb} GB jusqu'à présent!}}", + "claimedStorageSoFar": "{isFamilyMember, select, true {Votre famille a obtenu {storageAmountInGb} Go jusqu'à présent} false {Vous avez obtenu {storageAmountInGb} Go jusqu'à présent} other {Vous avez obtenu {storageAmountInGb} Go jusqu'à présent !}}", "@claimedStorageSoFar": { "placeholders": { "isFamilyMember": { @@ -314,15 +314,15 @@ } }, "faq": "FAQ", - "help": "Aide", + "help": "Documentation", "oopsSomethingWentWrong": "Oups, une erreur est arrivée", - "peopleUsingYourCode": "Personnes utilisant votre code", + "peopleUsingYourCode": "Filleul·e·s utilisant votre code", "eligible": "éligible", "total": "total", "codeUsedByYou": "Code utilisé par vous", "freeStorageClaimed": "Stockage gratuit obtenu", - "freeStorageUsable": "Stockage gratuit utilisable", - "usableReferralStorageInfo": "Le stockage utilisable est limité par votre offre actuelle. Le stockage excédentaire deviendra automatiquement utilisable lorsque vous mettez à niveau votre offre.", + "freeStorageUsable": "Stockage gratuit disponible", + "usableReferralStorageInfo": "Le stockage gratuit possible est limité par votre offre actuelle. Vous pouvez au maximum doubler votre espace de stockage gratuitement, le stockage supplémentaire deviendra donc automatiquement utilisable lorsque vous mettrez à niveau votre offre.", "removeFromAlbumTitle": "Retirer de l'album ?", "removeFromAlbum": "Retirer de l'album", "itemsWillBeRemovedFromAlbum": "Les éléments sélectionnés seront supprimés de cet album", @@ -419,12 +419,12 @@ "photoGridSize": "Taille de la grille photo", "manageDeviceStorage": "Gérer le cache de l'appareil", "manageDeviceStorageDesc": "Examiner et vider le cache.", - "machineLearning": "Apprentissage automatique", + "machineLearning": "Apprentissage automatique (IA locale)", "mlConsent": "Activer l'apprentissage automatique", "mlConsentTitle": "Activer l'apprentissage automatique ?", - "mlConsentDescription": "Si vous activez l'apprentissage automatique, Ente extraira des informations comme la géométrie des visages, incluant les photos partagées avec vous. \nCela se fera sur votre appareil, avec un cryptage de bout-en-bout de toutes les données biométriques générées.", + "mlConsentDescription": "Si vous activez l'apprentissage automatique Ente extraira des informations comme la géométrie des visages, y compris dans les photos partagées avec vous. \nCela se fera localement sur votre appareil et avec un chiffrement bout-en-bout de toutes les données biométriques générées.", "mlConsentPrivacy": "Veuillez cliquer ici pour plus de détails sur cette fonctionnalité dans notre politique de confidentialité", - "mlConsentConfirmation": "Je comprends, et souhaite activer l'apprentissage automatique", + "mlConsentConfirmation": "Je comprends et je souhaite activer l'apprentissage automatique", "magicSearch": "Recherche magique", "discover": "Découverte", "@discover": { @@ -445,15 +445,15 @@ "discover_sunset": "Coucher du soleil", "discover_hills": "Montagnes", "discover_greenery": "Plantes", - "mlIndexingDescription": "Veuillez noter que l'apprentissage automatique entraînera une augmentation de l'utilisation de la bande passante et de la batterie, jusqu'à ce que tous les éléments soient indexés. \nEnvisagez d'utiliser l'application de bureau pour une indexation plus rapide, tous les résultats seront automatiquement synchronisés.", + "mlIndexingDescription": "Veuillez noter que l'apprentissage automatique entraînera une augmentation de l'utilisation de la connexion Internet et de la batterie jusqu'à ce que tous les souvenirs soient indexés. \nVous pouvez utiliser l'application de bureau Ente pour accélérer cette étape, tous les résultats seront synchronisés.", "loadingModel": "Téléchargement des modèles...", "waitingForWifi": "En attente de connexion Wi-Fi...", "status": "État", "indexedItems": "Éléments indexés", "pendingItems": "Éléments en attente", "clearIndexes": "Effacer les index", - "selectFoldersForBackup": "Sélectionnez les dossiers à sauvegarder", - "selectedFoldersWillBeEncryptedAndBackedUp": "Les dossiers sélectionnés seront cryptés et sauvegardés", + "selectFoldersForBackup": "Dossiers à sauvegarder", + "selectedFoldersWillBeEncryptedAndBackedUp": "Les dossiers sélectionnés seront chiffrés et sauvegardés", "unselectAll": "Désélectionner tout", "selectAll": "Tout sélectionner", "skip": "Ignorer", @@ -483,10 +483,10 @@ "backupOverMobileData": "Sauvegarder avec les données mobiles", "backupVideos": "Sauvegarde des vidéos", "disableAutoLock": "Désactiver le verrouillage automatique", - "deviceLockExplanation": "Désactiver le verrouillage de l'écran de l'appareil lorsque ente est au premier plan et il y a une sauvegarde en cours. Ce n'est normalement pas nécessaire, mais peut aider les gros téléchargements et les premières importations de grandes bibliothèques plus rapidement.", - "about": "À propos", + "deviceLockExplanation": "Désactiver le verrouillage de l'écran lorsque Ente est au premier plan et qu'une sauvegarde est en cours. Ce n'est normalement pas nécessaire mais cela peut faciliter les gros téléchargements et les premières importations de grandes bibliothèques.", + "about": "À propos d'Ente", "weAreOpenSource": "Nous sommes open source !", - "privacy": "Confidentialité", + "privacy": "Politique de confidentialité", "terms": "Conditions", "checkForUpdates": "Vérifier les mises à jour", "checkStatus": "Vérifier le statut", @@ -494,11 +494,11 @@ "youAreOnTheLatestVersion": "Vous êtes sur la dernière version", "account": "Compte", "manageSubscription": "Gérer l'abonnement", - "authToChangeYourEmail": "Veuillez vous authentifier pour modifier votre adresse e-mail", + "authToChangeYourEmail": "Authentifiez-vous pour modifier votre adresse email", "changePassword": "Modifier le mot de passe", "authToChangeYourPassword": "Veuillez vous authentifier pour modifier votre mot de passe", - "emailVerificationToggle": "Vérification de l'adresse e-mail", - "authToChangeEmailVerificationSetting": "Veuillez vous authentifier pour modifier votre adresse e-mail", + "emailVerificationToggle": "Authentification à deux facteurs par email", + "authToChangeEmailVerificationSetting": "Authentifiez-vous pour modifier l'authentification à deux facteurs par email", "exportYourData": "Exportez vos données", "logout": "Déconnexion", "authToInitiateAccountDeletion": "Veuillez vous authentifier pour débuter la suppression du compte", @@ -525,7 +525,6 @@ "viewLargeFiles": "Fichiers volumineux", "viewLargeFilesDesc": "Affichez les fichiers qui consomment le plus de stockage.", "noDuplicates": "✨ Aucun doublon", - "youveNoDuplicateFilesThatCanBeCleared": "Vous n'avez aucun fichier dédupliqué pouvant être nettoyé", "success": "Succès", "rateUs": "Évaluez-nous", "remindToEmptyDeviceTrash": "Également vide \"récemment supprimé\" de \"Paramètres\" -> \"Stockage\" pour réclamer l'espace libéré", @@ -561,23 +560,23 @@ "referrals": "Parrainages", "notifications": "Notifications", "sharedPhotoNotifications": "Nouvelles photos partagées", - "sharedPhotoNotificationsExplanation": "Recevoir des notifications quand quelqu'un ajoute une photo à un album partagé dont vous faites partie", + "sharedPhotoNotificationsExplanation": "Recevoir des notifications quand quelqu'un·e ajoute une photo à un album partagé dont vous faites partie", "advanced": "Avancé", "general": "Général", "security": "Sécurité", "authToViewYourRecoveryKey": "Veuillez vous authentifier pour afficher votre clé de récupération", - "twofactor": "Double authentification", + "twofactor": "Authentification à deux facteurs (A2F)", "authToConfigureTwofactorAuthentication": "Veuillez vous authentifier pour configurer l'authentification à deux facteurs", "lockscreen": "Écran de verrouillage", "authToChangeLockscreenSetting": "Veuillez vous authentifier pour modifier les paramètres de l'écran de verrouillage", - "viewActiveSessions": "Afficher les sessions actives", - "authToViewYourActiveSessions": "Veuillez vous authentifier pour voir vos sessions actives", - "disableTwofactor": "Désactiver la double-authentification", + "viewActiveSessions": "Afficher les connexions actives", + "authToViewYourActiveSessions": "Authentifiez-vous pour voir les connexions actives", + "disableTwofactor": "Désactiver l'authentification à deux facteurs", "confirm2FADisable": "Voulez-vous vraiment désactiver l'authentification à deux facteurs ?", "no": "Non", "yes": "Oui", - "social": "Réseaux sociaux", - "rateUsOnStore": "Notez-nous sur {storeName}", + "social": "Retrouvez nous", + "rateUsOnStore": "Laissez une note sur {storeName}", "blog": "Blog", "merchandise": "Boutique", "twitter": "Twitter", @@ -586,9 +585,9 @@ "discord": "Discord", "reddit": "Reddit", "yourStorageDetailsCouldNotBeFetched": "Vos informations de stockage n'ont pas pu être récupérées", - "reportABug": "Signaler un bug", - "reportBug": "Signaler un bug", - "suggestFeatures": "Suggérer des fonctionnalités", + "reportABug": "Signaler un bogue", + "reportBug": "Signaler un bogue", + "suggestFeatures": "Suggérer une fonctionnalité", "support": "Support", "theme": "Thème", "lightTheme": "Clair", @@ -596,7 +595,7 @@ "systemTheme": "Système", "freeTrial": "Essai gratuit", "selectYourPlan": "Sélectionner votre offre", - "enteSubscriptionPitch": "Ente conserve vos souvenirs, ils sont donc toujours disponibles pour vous, même si vous perdez votre appareil.", + "enteSubscriptionPitch": "Ente conserve vos souvenirs pour qu'ils soient toujours disponible, même si vous perdez cet appareil.", "enteSubscriptionShareWithFamily": "Vous pouvez également ajouter votre famille à votre forfait.", "currentUsageIs": "L'utilisation actuelle est de ", "@currentUsageIs": { @@ -663,7 +662,7 @@ "playstoreSubscription": "Abonnement au PlayStore", "appstoreSubscription": "Abonnement à l'AppStore", "subAlreadyLinkedErrMessage": "Votre {id} est déjà lié à un autre compte Ente.\nSi vous souhaitez utiliser votre {id} avec ce compte, veuillez contacter notre support", - "visitWebToManage": "Veuillez visiter web.ente.io pour gérer votre abonnement", + "visitWebToManage": "Vous pouvez gérer votre abonnement sur web.ente.io", "couldNotUpdateSubscription": "Impossible de mettre à jour l’abonnement", "pleaseContactSupportAndWeWillBeHappyToHelp": "Veuillez contacter support@ente.io et nous serons heureux de vous aider!", "paymentFailed": "Échec du paiement", @@ -821,7 +820,7 @@ "sharedWithMe": "Partagés avec moi", "sharedByMe": "Partagé par moi", "doubleYourStorage": "Doublez votre espace de stockage", - "referFriendsAnd2xYourPlan": "Parrainez des amis et doublez votre abonnement", + "referFriendsAnd2xYourPlan": "Parrainez vos ami·e·s et doublez votre stockage", "shareAlbumHint": "Ouvrez un album et appuyez sur le bouton de partage en haut à droite pour le partager.", "itemsShowTheNumberOfDaysRemainingBeforePermanentDeletion": "Les éléments montrent le nombre de jours restants avant la suppression définitive", "trashDaysLeft": "{count, plural, =0 {Bientôt} =1 {1 jour} other {{count} jours}}", @@ -894,7 +893,7 @@ "totalSize": "Taille totale", "longpressOnAnItemToViewInFullscreen": "Appuyez longuement sur un élément pour le voir en plein écran", "decryptingVideo": "Déchiffrement de la vidéo...", - "authToViewYourMemories": "Veuillez vous authentifier pour voir vos souvenirs", + "authToViewYourMemories": "Authentifiez-vous pour voir vos souvenirs", "unlock": "Déverrouiller", "freeUpSpace": "Libérer de l'espace", "freeUpSpaceSaving": "{count, plural, 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}}}", @@ -955,17 +954,25 @@ "twofactorAuthenticationHasBeenDisabled": "L'authentification à deux facteurs a été désactivée", "sorryTheCodeYouveEnteredIsIncorrect": "Le code que vous avez saisi est incorrect", "yourVerificationCodeHasExpired": "Votre code de vérification a expiré", - "emailChangedTo": "L'e-mail a été changé en {newEmail}", + "emailChangedTo": "L'email a été changé par {newEmail}", "verifying": "Validation en cours...", - "disablingTwofactorAuthentication": "Désactiver la double-authentification...", - "allMemoriesPreserved": "Tous les souvenirs sont conservés", + "disablingTwofactorAuthentication": "Désactivation de l'authentification à deux facteurs...", + "allMemoriesPreserved": "Tous les souvenirs sont sauvegardés", "loadingGallery": "Chargement de la galerie...", "syncing": "En cours de synchronisation...", "encryptingBackup": "Chiffrement de la sauvegarde...", "syncStopped": "Synchronisation arrêtée ?", - "syncProgress": "{completed}/{total} souvenirs conservés", - "uploadingMultipleMemories": "Sauvegarde {count} souvenirs...", - "uploadingSingleMemory": "Sauvegarde 1 souvenir...", + "syncProgress": "{completed}/{total} souvenirs sauvegardés", + "uploadingMultipleMemories": "Sauvegarde de {count} souvenirs...", + "@uploadingMultipleMemories": { + "description": "Text to tell user how many memories are being preserved", + "placeholders": { + "count": { + "type": "String" + } + } + }, + "uploadingSingleMemory": "Sauvegarde d'un souvenir...", "@syncProgress": { "description": "Text to tell user how many memories have been preserved", "placeholders": { @@ -1010,13 +1017,13 @@ "localGallery": "Galerie locale", "todaysLogs": "Journaux du jour", "viewLogs": "Afficher les journaux", - "logsDialogBody": "Cela enverra des logs pour nous aider à déboguer votre problème. Veuillez noter que les noms de fichiers seront inclus pour aider à suivre les problèmes avec des fichiers spécifiques.", + "logsDialogBody": "Les journaux seront envoyés pour nous aider à déboguer votre problème. Les noms de fichiers seront inclus pour aider à identifier les problèmes.", "preparingLogs": "Préparation des journaux...", - "emailYourLogs": "Envoyez vos logs par e-mail", + "emailYourLogs": "Envoyez vos journaux par email", "pleaseSendTheLogsTo": "Envoyez les logs à {toEmail}", - "copyEmailAddress": "Copier l’adresse e-mail", + "copyEmailAddress": "Copier l’adresse email", "exportLogs": "Exporter les logs", - "pleaseEmailUsAt": "Merci de nous envoyer un e-mail à {toEmail}", + "pleaseEmailUsAt": "Merci de nous envoyer un email à {toEmail}", "dismiss": "Rejeter", "didYouKnow": "Le savais-tu ?", "loadingMessage": "Chargement de vos photos...", @@ -1036,7 +1043,7 @@ "searchFaceEmptySection": "Les personnes seront affichées ici une fois l'indexation terminée", "searchDatesEmptySection": "Recherche par date, mois ou année", "searchLocationEmptySection": "Grouper les photos qui sont prises dans un certain angle d'une photo", - "searchPeopleEmptySection": "Invitez des personnes, et vous verrez ici toutes les photos qu'elles partagent", + "searchPeopleEmptySection": "Invitez quelqu'un·e et vous verrez ici toutes les photos partagées", "searchAlbumsEmptySection": "Albums", "searchFileTypesAndNamesEmptySection": "Types et noms de fichiers", "searchCaptionEmptySection": "Ajoutez des descriptions comme \"#trip\" dans les infos photo pour les retrouver ici plus rapidement", @@ -1158,7 +1165,7 @@ "@map": { "description": "Label for the map view" }, - "maps": "Cartes", + "maps": "Carte", "enableMaps": "Activer la carte", "enableMapsDesc": "Vos photos seront affichées sur une carte du monde.\n\nCette carte est hébergée par Open Street Map, et les emplacements exacts de vos photos ne sont jamais partagés.\n\nVous pouvez désactiver cette fonction à tout moment dans les Paramètres.", "quickLinks": "Liens rapides", @@ -1177,14 +1184,14 @@ "noAlbumsSharedByYouYet": "Aucun album que vous avez partagé", "sharedWithYou": "Partagé avec vous", "sharedByYou": "Partagé par vous", - "inviteYourFriendsToEnte": "Invitez vos amis sur Ente", + "inviteYourFriendsToEnte": "Invitez vos ami·e·s sur Ente", "failedToDownloadVideo": "Échec du téléchargement de la vidéo", "hiding": "Masquage en cours...", "unhiding": "Démasquage en cours...", "successfullyHid": "Masquage réussi", "successfullyUnhid": "Masquage réussi", - "crashReporting": "Rapports d'erreurs", - "resumableUploads": "Reprise des chargements", + "crashReporting": "Rapport d'erreur", + "resumableUploads": "Reprise automatique des transferts", "addToHiddenAlbum": "Ajouter à un album masqué", "moveToHiddenAlbum": "Déplacer vers un album masqué", "fileTypes": "Types de fichiers", @@ -1239,13 +1246,13 @@ "cleanUncategorized": "Effacer les éléments non classés", "cleanUncategorizedDescription": "Supprimer tous les fichiers non-catégorisés étant présents dans d'autres albums", "waitingForVerification": "En attente de vérification...", - "passkey": "Code d'accès", - "passkeyAuthTitle": "Vérification du code d'accès", + "passkey": "Authentification à deux facteurs avec une clé de sécurité", + "passkeyAuthTitle": "Vérification de la clé de sécurité", "loginWithTOTP": "Se connecter avec TOTP", "passKeyPendingVerification": "La vérification est toujours en attente", "loginSessionExpired": "Session expirée", "loginSessionExpiredDetails": "Votre session a expiré. Veuillez vous reconnecter.", - "verifyPasskey": "Vérifier le code d'accès", + "verifyPasskey": "Vérifier la clé de sécurité", "playOnTv": "Lire l'album sur la TV", "pair": "Associer", "deviceNotFound": "Appareil non trouvé", @@ -1261,7 +1268,7 @@ "findPeopleByName": "Trouver des personnes rapidement par leur nom", "addViewers": "{count, plural, zero {Ajouter un observateur} one {Ajouter un observateur} other {Ajouter des observateurs}}", "addCollaborators": "{count, plural, zero {Ajouter un collaborateur} one {Ajouter un collaborateur} other {Ajouter des collaborateurs}}", - "longPressAnEmailToVerifyEndToEndEncryption": "Appuyez longuement sur un e-mail pour vérifier le chiffrement de bout en bout.", + "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", "serverEndpoint": "Point de terminaison serveur", @@ -1320,13 +1327,13 @@ "enable": "Activer", "enabled": "Activé", "moreDetails": "Plus de détails", - "enableMLIndexingDesc": "Ente prend en charge l'apprentissage automatique sur l'appareil pour la reconnaissance faciale, la recherche magique et d'autres fonctionnalités de recherche avancée", + "enableMLIndexingDesc": "Ente prend en charge l'apprentissage automatique sur l'appareil pour la reconnaissance des visages, la recherche magique et d'autres fonctionnalités de recherche avancée", "magicSearchHint": "La recherche magique permet de rechercher des photos par leur contenu, par exemple 'fleur', 'voiture rouge', 'documents d'identité'", "panorama": "Panorama", "reenterPassword": "Ressaisir le mot de passe", "reenterPin": "Ressaisir le code PIN", - "deviceLock": "Verrouillage de l'appareil", - "pinLock": "Verrouillage du code PIN", + "deviceLock": "Verrouillage par défaut de l'appareil", + "pinLock": "Verrouillage par code PIN", "next": "Suivant", "setNewPassword": "Définir un nouveau mot de passe", "enterPin": "Saisir le code PIN", @@ -1338,7 +1345,7 @@ "videoInfo": "Informations vidéo", "autoLock": "Verrouillage automatique", "immediately": "Immédiatement", - "autoLockFeatureDescription": "Délai après lequel l'application se verrouille une fois qu'elle a été mise en arrière-plan", + "autoLockFeatureDescription": "Délai après lequel l'application se verrouille une fois qu'elle est en arrière-plan", "hideContent": "Masquer le contenu", "hideContentDescriptionAndroid": "Masque le contenu de l'application dans le sélecteur d'applications et désactive les captures d'écran", "hideContentDescriptionIos": "Masque le contenu de l'application dans le sélecteur d'application", @@ -1353,7 +1360,7 @@ "collectPhotosDescription": "Créez un lien où vos amis peuvent ajouter des photos en qualité originale.", "collect": "Récupérer", "appLockDescriptions": "Choisissez entre l'écran de verrouillage par défaut de votre appareil et un écran de verrouillage personnalisé avec un code PIN ou un mot de passe.", - "toEnableAppLockPleaseSetupDevicePasscodeOrScreen": "Pour activer le verrouillage d'application, veuillez configurer le code d'accès de l'appareil ou le verrouillage de l'écran dans les paramètres de votre système.", + "toEnableAppLockPleaseSetupDevicePasscodeOrScreen": "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.", "authToViewPasskey": "Veuillez vous authentifier pour afficher votre clé de récupération", "loopVideoOn": "Vidéo en boucle activée", "loopVideoOff": "Vidéo en boucle désactivée", @@ -1382,14 +1389,14 @@ "extraPhotosFound": "Photos supplémentaires trouvées", "configuration": "Paramètres", "localIndexing": "Indexation locale", - "processed": "Traité", + "processed": "Appris", "resetPerson": "Réinitialiser", "areYouSureYouWantToResetThisPerson": "Êtes-vous certain de vouloir réinitialiser cette personne ?", "allPersonGroupingWillReset": "Tous les groupements pour cette personne seront réinitialisés, et vous perdrez toutes les suggestions faites pour cette personne", "yesResetPerson": "Oui, réinitialiser la personne", "onlyThem": "Seulement eux", "checkingModels": "Vérification des modèles...", - "enableMachineLearningBanner": "Activer l'apprentissage automatique pour la recherche magique et la reconnaissance faciale", + "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, =0 {0 observateur ajouté} =1 {1 observateur ajouté} other {{count} observateurs ajoutés}}", @@ -1417,7 +1424,7 @@ "@sessionIdMismatch": { "description": "In passkey page, deeplink is ignored because of session ID mismatch." }, - "failedToFetchActiveSessions": "Impossible de récupérer les sessions actives", + "failedToFetchActiveSessions": "Impossible de récupérer les connexions actives", "@failedToFetchActiveSessions": { "description": "In session page, warn user (in toast) that active sessions could not be fetched." }, @@ -1511,7 +1518,7 @@ "changeLogBackupStatusTitle": "Statut de la Sauvegarde", "changeLogBackupStatusContent": "Nous avons ajouté un journal de tous les fichiers qui ont été envoyés vers Ente, y compris les échecs et la file d'attente.", "changeLogDiscoverTitle": "Découverte", - "changeLogDiscoverContent": "Vous cherchez des photos de vos cartes d'identité, des notes ou même des memes? Allez dans l'onglet de recherche et découvrez Découverte. Sur la base de notre recherche sémantique, vous trouverez des photos qui pourraient être importantes pour vous.\\n\\nUniquement disponible si vous avez activé l'apprentissage automatique.", + "changeLogDiscoverContent": "Vous cherchez des photos de vos papiers d'identité, des notes ou même des \"memes\" ? Rendez-vous dans l'onglet de recherche et explorez la fonction Découverte. Grâce à notre recherche sémantique vous trouverez des photos qui pourraient être importantes pour vous.\\n\\nDisponible uniquement si vous avez activé l'apprentissage automatique.", "selectCoverPhoto": "Sélectionnez la photo de couverture", "newLocation": "Nouveau lieu", "faceNotClusteredYet": "Ce visage n'a pas encore été regroupé, veuillez revenir plus tard", @@ -1529,7 +1536,7 @@ "removeYourselfAsTrustedContact": "Retirez-vous comme contact de confiance", "legacy": "Héritage", "legacyPageDesc": "L'héritage permet aux contacts de confiance d'accéder à votre compte en votre absence.", - "legacyPageDesc2": "Les contacts de confiance peuvent initier la récupération du compte et, s'ils ne sont pas bloqués dans les 30 jours qui suivent, peuvent réinitialiser votre mot de passe et accéder à votre compte.", + "legacyPageDesc2": "Ces contacts peuvent initier la récupération du compte et, s'ils ne sont pas bloqués dans les 30 jours qui suivent, peuvent réinitialiser votre mot de passe et accéder à votre compte.", "legacyAccounts": "Comptes hérités", "trustedContacts": "Contacts de confiance", "addTrustedContact": "Ajouter un contact de confiance", @@ -1584,11 +1591,87 @@ "legacyInvite": "{email} vous a invité à être un contact de confiance", "authToManageLegacy": "Veuillez vous authentifier pour gérer vos contacts de confiance", "useDifferentPlayerInfo": "Vous avez des difficultés pour lire cette vidéo ? Appuyez longuement ici pour essayer un autre lecteur.", - "hideSharedItemsFromHomeGallery": "Masquer les éléments partagés de la galerie d'accueil", + "hideSharedItemsFromHomeGallery": "Masquer les éléments partagés avec vous dans la galerie", "gallery": "Galerie", "joinAlbum": "Rejoindre l'album", "joinAlbumSubtext": "pour afficher et ajouter vos photos", "joinAlbumSubtextViewer": "pour ajouter ceci aux albums partagés", "join": "Rejoindre", - "orPickFromYourContacts": "or pick from your contacts" + "linkEmail": "Lier l'email", + "link": "Lier", + "noEnteAccountExclamation": "Aucun compte Ente !", + "orPickFromYourContacts": "ou choisissez parmi vos contacts", + "emailDoesNotHaveEnteAccount": "{email} n'a pas de compte Ente.", + "@emailDoesNotHaveEnteAccount": { + "description": "Shown when email doesn't have an Ente account", + "placeholders": { + "email": { + "type": "String" + } + } + }, + "accountOwnerPersonAppbarTitle": "{title} (Moi)", + "@accountOwnerPersonAppbarTitle": { + "description": "Title of appbar for account owner person", + "placeholders": { + "title": { + "type": "String" + } + } + }, + "reassignMe": "Réassigner \"Moi\"", + "me": "Moi", + "linkEmailToContactBannerCaption": "pour un partage plus rapide", + "@linkEmailToContactBannerCaption": { + "description": "Caption for the 'Link email' title. It should be a continuation of the 'Link email' title. Just like how 'Link email' + 'for faster sharing' forms a proper sentence in English, the combination of these two strings should also be a proper sentence in other languages." + }, + "selectPersonToLink": "Sélectionnez la personne à associer", + "linkPersonToEmail": "Associer la personne à {email}", + "@linkPersonToEmail": { + "placeholders": { + "email": { + "type": "String" + } + } + }, + "linkPersonToEmailConfirmation": "Cela va associer {personName} à {email}", + "@linkPersonToEmailConfirmation": { + "description": "Confirmation message when linking a person to an email", + "placeholders": { + "personName": { + "type": "String" + }, + "email": { + "type": "String" + } + } + }, + "selectYourFace": "Sélectionnez votre visage", + "reassigningLoading": "Réassignation...", + "reassignedToName": "Vous a réassigné à {name}", + "@reassignedToName": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "saveChangesBeforeLeavingQuestion": "Enregistrer les modifications avant de quitter ?", + "dontSave": "Ne pas enregistrer", + "thisIsMeExclamation": "C'est moi !", + "linkPerson": "Lier la personne", + "linkPersonCaption": "pour une meilleure expérience de partage", + "@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", + "processingVideos": "Traitement des vidéos", + "streamDetails": "Détails du stream", + "processing": "Traitement en cours", + "queued": "En file d'attente", + "ineligible": "Non compatible", + "failed": "Échec", + "playStream": "Lire le stream", + "playOriginal": "Lire l'original", + "joinAlbumConfirmationDialogBody": "Rejoindre un album rendra votre e-mail visible à ses participants." } \ No newline at end of file diff --git a/mobile/lib/l10n/intl_gu.arb b/mobile/lib/l10n/intl_gu.arb index 1a5f45c37d..c8494661c6 100644 --- a/mobile/lib/l10n/intl_gu.arb +++ b/mobile/lib/l10n/intl_gu.arb @@ -1,4 +1,3 @@ { - "@@locale ": "en", - "orPickFromYourContacts": "or pick from your contacts" + "@@locale ": "en" } \ No newline at end of file diff --git a/mobile/lib/l10n/intl_he.arb b/mobile/lib/l10n/intl_he.arb index a75dd77c29..e1e5713b55 100644 --- a/mobile/lib/l10n/intl_he.arb +++ b/mobile/lib/l10n/intl_he.arb @@ -462,7 +462,6 @@ "noDeviceThatCanBeDeleted": "אין לך קבצים במכשיר הזה שניתן למחוק אותם", "removeDuplicates": "הסר כפילויות", "noDuplicates": "✨ אין כפילויות", - "youveNoDuplicateFilesThatCanBeCleared": "אין לך קבצים כפולים שניתן לנקות אותם", "success": "הצלחה", "rateUs": "דרג אותנו", "remindToEmptyDeviceTrash": "גם נקה \"נמחק לאחרונה\" מ-\"הגדרות\" -> \"אחסון\" על מנת לקבל המקום אחסון שהתפנה", @@ -815,6 +814,5 @@ "addPhotos": "הוסף תמונות", "create": "צור", "viewAll": "הצג הכל", - "hiding": "מחביא...", - "orPickFromYourContacts": "or pick from your contacts" + "hiding": "מחביא..." } \ No newline at end of file diff --git a/mobile/lib/l10n/intl_hi.arb b/mobile/lib/l10n/intl_hi.arb index d16db2abef..b79d9682f2 100644 --- a/mobile/lib/l10n/intl_hi.arb +++ b/mobile/lib/l10n/intl_hi.arb @@ -48,6 +48,5 @@ "sorry": "क्षमा करें!", "noRecoveryKeyNoDecryption": "हमारे एंड-टू-एंड एन्क्रिप्शन प्रोटोकॉल की प्रकृति के कारण, आपके डेटा को आपके पासवर्ड या रिकवरी कुंजी के बिना डिक्रिप्ट नहीं किया जा सकता है", "verifyEmail": "ईमेल सत्यापित करें", - "toResetVerifyEmail": "अपना पासवर्ड रीसेट करने के लिए, कृपया पहले अपना ईमेल सत्यापित करें।", - "orPickFromYourContacts": "or pick from your contacts" + "toResetVerifyEmail": "अपना पासवर्ड रीसेट करने के लिए, कृपया पहले अपना ईमेल सत्यापित करें।" } \ No newline at end of file diff --git a/mobile/lib/l10n/intl_hu.arb b/mobile/lib/l10n/intl_hu.arb index f960f6f7f6..92bdab9095 100644 --- a/mobile/lib/l10n/intl_hu.arb +++ b/mobile/lib/l10n/intl_hu.arb @@ -10,6 +10,5 @@ "deleteAccount": "Fiók törlése", "askDeleteReason": "Miért törli a fiókját?", "deleteAccountFeedbackPrompt": "Sajnáljuk, hogy távozik. Kérjük, ossza meg velünk visszajelzéseit, hogy segítsen nekünk a fejlődésben.", - "feedback": "Visszajelzés", - "orPickFromYourContacts": "or pick from your contacts" + "feedback": "Visszajelzés" } \ No newline at end of file diff --git a/mobile/lib/l10n/intl_id.arb b/mobile/lib/l10n/intl_id.arb index 5cf6af497a..fbee9bae17 100644 --- a/mobile/lib/l10n/intl_id.arb +++ b/mobile/lib/l10n/intl_id.arb @@ -494,7 +494,6 @@ "viewLargeFiles": "File berukuran besar", "viewLargeFilesDesc": "Tampilkan file yang paling besar mengonsumsi ruang penyimpanan.", "noDuplicates": "✨ Tak ada file duplikat", - "youveNoDuplicateFilesThatCanBeCleared": "Kamu tidak memiliki file duplikat yang dapat dihapus", "success": "Berhasil", "rateUs": "Beri kami nilai", "remindToEmptyDeviceTrash": "Kosongkan juga “Baru Dihapus” dari “Pengaturan” -> “Penyimpanan” untuk memperoleh ruang yang baru saja dibersihkan", @@ -1131,6 +1130,5 @@ "rotate": "Putar", "left": "Kiri", "right": "Kanan", - "whatsNew": "Hal yang baru", - "orPickFromYourContacts": "or pick from your contacts" + "whatsNew": "Hal yang baru" } \ No newline at end of file diff --git a/mobile/lib/l10n/intl_it.arb b/mobile/lib/l10n/intl_it.arb index 03f526fb67..fc131c597d 100644 --- a/mobile/lib/l10n/intl_it.arb +++ b/mobile/lib/l10n/intl_it.arb @@ -2,6 +2,8 @@ "@@locale ": "en", "enterYourEmailAddress": "Inserisci il tuo indirizzo email", "accountWelcomeBack": "Bentornato!", + "emailAlreadyRegistered": "Email già registrata.", + "emailNotRegistered": "Email non registrata.", "email": "Email", "cancel": "Annulla", "verify": "Verifica", @@ -523,7 +525,6 @@ "viewLargeFiles": "File di grandi dimensioni", "viewLargeFilesDesc": "Visualizza i file che stanno occupando la maggior parte dello spazio di archiviazione.", "noDuplicates": "✨ Nessun doppione", - "youveNoDuplicateFilesThatCanBeCleared": "Non hai file duplicati che possono essere cancellati", "success": "Operazione riuscita", "rateUs": "Lascia una recensione", "remindToEmptyDeviceTrash": "Vuota anche \"Cancellati di recente\" da \"Impostazioni\" -> \"Storage\" per avere più spazio libero", @@ -693,6 +694,7 @@ "preserveMore": "Salva più foto", "grantFullAccessPrompt": "Consenti l'accesso a tutte le foto nelle Impostazioni", "allowPermTitle": "Consenti l'accesso alle foto", + "allowPermBody": "Permetti l'accesso alle tue foto da Impostazioni in modo che Ente possa visualizzare e fare il backup della tua libreria.", "openSettings": "Apri Impostazioni", "selectMorePhotos": "Seleziona più foto", "existingUser": "Accedi", @@ -962,6 +964,14 @@ "syncStopped": "Sincronizzazione interrotta", "syncProgress": "{completed}/{total} ricordi conservati", "uploadingMultipleMemories": "Conservando {count} ricordi...", + "@uploadingMultipleMemories": { + "description": "Text to tell user how many memories are being preserved", + "placeholders": { + "count": { + "type": "String" + } + } + }, "uploadingSingleMemory": "Conservando 1 ricordo...", "@syncProgress": { "description": "Text to tell user how many memories have been preserved", @@ -1238,6 +1248,7 @@ "waitingForVerification": "In attesa di verifica...", "passkey": "Passkey", "passkeyAuthTitle": "Verifica della passkey", + "loginWithTOTP": "Login con TOTP", "passKeyPendingVerification": "La verifica è ancora in corso", "loginSessionExpired": "Sessione scaduta", "loginSessionExpiredDetails": "La sessione è scaduta. Si prega di accedere nuovamente.", @@ -1480,7 +1491,11 @@ "newLocation": "Nuova posizione", "faceNotClusteredYet": "Faccia non ancora raggruppata, per favore torna più tardi", "openFile": "Apri file", + "backupFile": "File di backup", + "openAlbumInBrowser": "Apri album nel browser", + "openAlbumInBrowserTitle": "Utilizza l'app web per aggiungere foto a questo album", "allow": "Consenti", + "allowAppToOpenSharedAlbumLinks": "Consenti all'app di aprire link all'album condiviso", "emergencyContacts": "Contatti di emergenza", "acceptTrustInvite": "Accetta l'invito", "declineTrustInvite": "Rifiuta l'invito", @@ -1544,5 +1559,45 @@ "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", - "orPickFromYourContacts": "or pick from your contacts" + "accountOwnerPersonAppbarTitle": "{title} (Io)", + "@accountOwnerPersonAppbarTitle": { + "description": "Title of appbar for account owner person", + "placeholders": { + "title": { + "type": "String" + } + } + }, + "reassignMe": "Riassegna \"Io\"", + "me": "Io", + "linkEmailToContactBannerCaption": "per una condivisione più veloce", + "@linkEmailToContactBannerCaption": { + "description": "Caption for the 'Link email' title. It should be a continuation of the 'Link email' title. Just like how 'Link email' + 'for faster sharing' forms a proper sentence in English, the combination of these two strings should also be a proper sentence in other languages." + }, + "selectPersonToLink": "Seleziona persona da collegare", + "linkPersonToEmail": "Collega persona a {email}", + "@linkPersonToEmail": { + "placeholders": { + "email": { + "type": "String" + } + } + }, + "reassigningLoading": "Riassegnando...", + "reassignedToName": "Riassegnato a {name}", + "@reassignedToName": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "dontSave": "Non salvare", + "thisIsMeExclamation": "Questo sono io!", + "linkPerson": "Collega persona", + "linkPersonCaption": "per una migliore esperienza di condivisione", + "@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" } \ No newline at end of file diff --git a/mobile/lib/l10n/intl_ja.arb b/mobile/lib/l10n/intl_ja.arb index a03a212a72..49664db37f 100644 --- a/mobile/lib/l10n/intl_ja.arb +++ b/mobile/lib/l10n/intl_ja.arb @@ -2,6 +2,8 @@ "@@locale ": "en", "enterYourEmailAddress": "Eメールアドレスを入力", "accountWelcomeBack": "おかえりなさい!", + "emailAlreadyRegistered": "このメールアドレスはすでに登録されています。", + "emailNotRegistered": "このメールアドレスはまだ登録されていません。", "email": "Eメール", "cancel": "キャンセル", "verify": "確認", @@ -185,6 +187,8 @@ }, "allowAddPhotosDescription": "リンクを持つ人が共有アルバムに写真を追加できるようにします。", "passwordLock": "パスワード保護", + "canNotOpenTitle": "このアルバムは開けません", + "canNotOpenBody": "申し訳ありません。このアルバムをアプリで開くことができませんでした。", "disableDownloadWarningTitle": "ご注意ください", "disableDownloadWarningBody": "ビューアーはスクリーンショットを撮ったり、外部ツールを使用して写真のコピーを保存したりすることができます", "allowDownloads": "ダウンロードを許可", @@ -351,6 +355,7 @@ "failedToLoadAlbums": "アルバムの読み込みに失敗しました", "hidden": "非表示", "authToViewYourHiddenFiles": "隠しファイルを表示するには認証してください", + "authToViewTrashedFiles": "削除したファイルを閲覧するには認証が必要です", "trash": "ゴミ箱", "uncategorized": "カテゴリなし", "videoSmallCase": "ビデオ", @@ -412,6 +417,8 @@ "description": "The text to display in the advanced settings section" }, "photoGridSize": "写真のグリッドサイズ", + "manageDeviceStorage": "端末のキャッシュを管理", + "manageDeviceStorageDesc": "端末上のキャッシュを確認・削除", "machineLearning": "機械学習", "mlConsent": "機械学習を有効にする", "mlConsentTitle": "機械学習を有効にしますか?", @@ -419,13 +426,13 @@ "mlConsentPrivacy": "この機能の詳細については、こちらをクリックしてください。", "mlConsentConfirmation": "機械学習を可能にしたい", "magicSearch": "マジックサーチ", - "discover": "ディスカバー", + "discover": "新たな発見", "@discover": { "description": "The text to display for the discover section under which we show receipts, screenshots, sunsets, greenery, etc." }, - "discover_identity": "アイデンティティ", + "discover_identity": "身分証", "discover_screenshots": "スクリーンショット", - "discover_receipts": "領収書", + "discover_receipts": "レシート", "discover_notes": "メモ", "discover_memes": "ミーム", "discover_visiting_cards": "訪問カード", @@ -518,7 +525,6 @@ "viewLargeFiles": "大きなファイル", "viewLargeFilesDesc": "最も多くのストレージを消費しているファイルを表示します。", "noDuplicates": "✨ 重複なし", - "youveNoDuplicateFilesThatCanBeCleared": "削除できる重複ファイルはありません", "success": "成功", "rateUs": "評価して下さい", "remindToEmptyDeviceTrash": "また、空き領域を取得するには、「設定」→「ストレージ」から「最近削除した項目」を空にします", @@ -687,6 +693,8 @@ "noPhotosAreBeingBackedUpRightNow": "現在バックアップされている写真はありません", "preserveMore": "もっと保存する", "grantFullAccessPrompt": "設定アプリで、すべての写真へのアクセスを許可してください", + "allowPermTitle": "写真へのアクセスを許可", + "allowPermBody": "Enteがライブラリを表示およびバックアップできるように、端末の設定から写真へのアクセスを許可してください。", "openSettings": "設定を開く", "selectMorePhotos": "さらに写真を選択", "existingUser": "既存のユーザー", @@ -733,7 +741,7 @@ "description": "The text displayed above albums backed up to Ente", "type": "text" }, - "onEnte": "Enteで保管", + "onEnte": "Enteが保管", "name": "名前順", "newest": "新しい順", "lastUpdated": "更新された順", @@ -815,6 +823,16 @@ "referFriendsAnd2xYourPlan": "友達に紹介して2倍", "shareAlbumHint": "アルバムを開いて右上のシェアボタンをタップ", "itemsShowTheNumberOfDaysRemainingBeforePermanentDeletion": "完全に削除されるまでの日数が項目に表示されます", + "trashDaysLeft": "{count, plural, =0 {} =1 {1日} other {{count} 日}}", + "@trashDaysLeft": { + "description": "Text to indicate number of days remaining before permanent deletion", + "placeholders": { + "count": { + "example": "1|2|3", + "type": "int" + } + } + }, "deleteAll": "全て削除", "renameAlbum": "アルバムの名前変更", "convertToAlbum": "アルバムに変換", @@ -946,6 +964,14 @@ "syncStopped": "同期が停止しました", "syncProgress": "{completed}/{total} のメモリが保存されました", "uploadingMultipleMemories": "{count} メモリを保存しています...", + "@uploadingMultipleMemories": { + "description": "Text to tell user how many memories are being preserved", + "placeholders": { + "count": { + "type": "String" + } + } + }, "uploadingSingleMemory": "1メモリを保存しています...", "@syncProgress": { "description": "Text to tell user how many memories have been preserved", @@ -1013,7 +1039,7 @@ "photoDescriptions": "写真の説明", "fileTypesAndNames": "ファイルの種類と名前", "location": "場所", - "moments": "モーメント", + "moments": "日々の瞬間", "searchFaceEmptySection": "学習が完了すると、ここに人が表示されます", "searchDatesEmptySection": "日付、月または年で検索", "searchLocationEmptySection": "当時の直近で撮影された写真をグループ化", @@ -1222,6 +1248,7 @@ "waitingForVerification": "確認を待っています...", "passkey": "パスキー", "passkeyAuthTitle": "パスキーの検証", + "loginWithTOTP": "TOTPでログイン", "passKeyPendingVerification": "検証はまだ保留中です", "loginSessionExpired": "セッション切れ", "loginSessionExpiredDetails": "セッションの有効期限が切れました。再度ログインしてください。", @@ -1362,5 +1389,289 @@ "extraPhotosFound": "追加の写真が見つかりました", "configuration": "設定", "localIndexing": "このデバイス上での実行", - "orPickFromYourContacts": "or pick from your contacts" + "processed": "処理完了", + "resetPerson": "削除", + "areYouSureYouWantToResetThisPerson": "この人を忘れてもよろしいですね?", + "allPersonGroupingWillReset": "この人のグループ化がリセットされ、この人かもしれない写真への提案もなくなります", + "yesResetPerson": "リセット", + "onlyThem": "それらのみ", + "checkingModels": "モデルを確認しています...", + "enableMachineLearningBanner": "マジック検索と顔認識のため、機械学習を有効にする", + "searchDiscoverEmptySection": "処理と同期が完了すると、画像がここに表示されます", + "searchPersonsEmptySection": "処理と同期が完了すると、ここに人々が表示されます", + "viewersSuccessfullyAdded": "{count, plural, =0 {{count}人のビューアーを追加} =1 {{count}人のビューアーを追加} other {{count}人のビューアーを追加}}", + "@viewersSuccessfullyAdded": { + "placeholders": { + "count": { + "type": "int", + "example": "2" + } + }, + "description": "Number of viewers that were successfully added to an album." + }, + "collaboratorsSuccessfullyAdded": "{count, plural, =0 {{count}人のコラボレーターを追加} =1 {{count}人のコラボレーターを追加} other {{count}人のコラボレーターを追加}}", + "@collaboratorsSuccessfullyAdded": { + "placeholders": { + "count": { + "type": "int", + "example": "2" + } + }, + "description": "Number of collaborators that were successfully added to an album." + }, + "accountIsAlreadyConfigured": "アカウントが既に設定されています", + "sessionIdMismatch": "セッションIDが一致しません", + "@sessionIdMismatch": { + "description": "In passkey page, deeplink is ignored because of session ID mismatch." + }, + "failedToFetchActiveSessions": "アクティブなセッションの取得に失敗しました", + "@failedToFetchActiveSessions": { + "description": "In session page, warn user (in toast) that active sessions could not be fetched." + }, + "failedToRefreshStripeSubscription": "サブスクリプションの更新に失敗しました", + "failedToPlayVideo": "動画の再生に失敗しました", + "uploadIsIgnoredDueToIgnorereason": "以下の理由によりアップロードは無視されます: {ignoreReason}", + "@uploadIsIgnoredDueToIgnorereason": { + "placeholders": { + "ignoreReason": { + "type": "String", + "example": "no network" + } + } + }, + "typeOfGallerGallerytypeIsNotSupportedForRename": "このギャラリーのタイプ {galleryType} は名前の変更には対応していません", + "@typeOfGallerGallerytypeIsNotSupportedForRename": { + "placeholders": { + "galleryType": { + "type": "String", + "example": "no network" + } + } + }, + "tapToUploadIsIgnoredDue": "アップロードするにはタップしてください。 以下の理由のためアップロードは現在無視されています: {ignoreReason}", + "@tapToUploadIsIgnoredDue": { + "description": "Shown in upload icon widet, inside a tooltip.", + "placeholders": { + "ignoreReason": { + "type": "String", + "example": "no network" + } + } + }, + "tapToUpload": "タップしてアップロード", + "@tapToUpload": { + "description": "Shown in upload icon widet, inside a tooltip." + }, + "info": "情報", + "addFiles": "ファイルを追加", + "castAlbum": "アルバムをキャスト", + "imageNotAnalyzed": "画像が分析されていません", + "noFacesFound": "顔が見つかりません", + "fileNotUploadedYet": "ファイルがまだアップロードされていません", + "noSuggestionsForPerson": "{personName} の候補はありません", + "@noSuggestionsForPerson": { + "placeholders": { + "personName": { + "type": "String", + "example": "Alice" + } + } + }, + "month": "月", + "yearShort": "年", + "@yearShort": { + "description": "Appears in pricing page (/yr)" + }, + "currentlyRunning": "現在実行中", + "ignored": "無視された", + "photosCount": "{count, plural, =0 {0枚の写真} =1 {1枚の写真} other {{count} 枚の写真}}", + "@photosCount": { + "placeholders": { + "count": { + "type": "int", + "example": "2" + } + } + }, + "file": "ファイル", + "searchSectionsLengthMismatch": "セクションの長さの不一致: {snapshotLength} != {searchLength}", + "@searchSectionsLengthMismatch": { + "description": "Appears in search tab page", + "placeholders": { + "snapshotLength": { + "type": "int", + "example": "1" + }, + "searchLength": { + "type": "int", + "example": "2" + } + } + }, + "selectMailApp": "メールアプリを選択", + "selectAllShort": "すべて", + "@selectAllShort": { + "description": "Text that appears in bottom right when you start to select multiple photos. When clicked, it selects all photos." + }, + "changeLogMagicSearchImprovementTitle": "マジック検索の改善", + "changeLogMagicSearchImprovementContent": "マジック検索が改善しました。もう、あなたが探しているものを見つけるのを待つ必要はありません。", + "changeLogBackupStatusTitle": "バックアップ ステータス", + "changeLogBackupStatusContent": "私たちは、失敗と待機中を含む、Enteにアップロードされたすべてのファイルのログを追加しました。", + "changeLogDiscoverTitle": "ディスカバー", + "changeLogDiscoverContent": "IDカード、メモなどの写真をお探しですか?検索タブに移動し、ディスカバーを確認してください。 \\n\\n機械学習を有効にしている場合にのみ利用できます。", + "selectCoverPhoto": "カバー写真を選択", + "newLocation": "新しいロケーション", + "faceNotClusteredYet": "顔がまだ集まっていません。後で戻ってきてください", + "theLinkYouAreTryingToAccessHasExpired": "アクセスしようとしているリンクの期限が切れています。", + "openFile": "ファイルを開く", + "backupFile": "バックアップファイル", + "openAlbumInBrowser": "ブラウザでアルバムを開く", + "openAlbumInBrowserTitle": "このアルバムに写真を追加するには、Webアプリを使用してください", + "allow": "許可", + "allowAppToOpenSharedAlbumLinks": "共有アルバムリンクを開くことをアプリに許可する", + "seePublicAlbumLinksInApp": "アプリ内で公開アルバムのリンクを見る", + "emergencyContacts": "緊急連絡先", + "acceptTrustInvite": "招待を受け入れる", + "declineTrustInvite": "招待を拒否する", + "removeYourselfAsTrustedContact": "あなた自身を信頼できる連絡先から削除", + "legacy": "レガシー", + "legacyPageDesc": "レガシーでは、信頼できる連絡先が不在時(あなたが亡くなった時など)にアカウントにアクセスできます。", + "legacyPageDesc2": "信頼できる連絡先はアカウントの回復を開始することができます。30日以内にあなたが拒否しない場合は、その信頼する人がパスワードをリセットしてあなたのアカウントにアクセスできるようになります。", + "legacyAccounts": "レガシーアカウント", + "trustedContacts": "信頼する連絡先", + "addTrustedContact": "信頼する連絡先を追加", + "removeInvite": "招待を削除", + "recoveryWarning": "信頼する連絡先の持ち主があなたのアカウントにアクセスしようとしています", + "rejectRecovery": "リカバリを拒否する", + "recoveryInitiated": "リカバリが開始されました", + "recoveryInitiatedDesc": "{days} 日後にアカウントにアクセスできます。通知は {email}に送信されます。", + "@recoveryInitiatedDesc": { + "placeholders": { + "days": { + "type": "int", + "example": "30" + }, + "email": { + "type": "String", + "example": "me@example.com" + } + } + }, + "cancelAccountRecovery": "リカバリをキャンセル", + "recoveryAccount": "アカウントを復元", + "cancelAccountRecoveryBody": "リカバリをキャンセルしてもよろしいですか?", + "startAccountRecoveryTitle": "リカバリを開始", + "whyAddTrustContact": "信頼する連絡先は、データの復旧が必要な際に役立ちます。", + "recoveryReady": "{email}のアカウントを復元できるようになりました。新しいパスワードを設定してください。", + "@recoveryReady": { + "placeholders": { + "email": { + "type": "String", + "example": "me@example.com" + } + } + }, + "recoveryWarningBody": "{email} はあなたのアカウントを復元しようとしています。", + "trustedInviteBody": "あなたは {email}から信頼する連絡先になってもらうよう、お願いされています。", + "warning": "警告", + "proceed": "続行", + "confirmAddingTrustedContact": "{email} を信頼する連絡先として追加しようとしています。 {numOfDays} 日間あなたの利用がなくなった場合、アカウントを復旧することができるようになります。", + "@confirmAddingTrustedContact": { + "placeholders": { + "email": { + "type": "String", + "example": "me@example.com" + }, + "numOfDays": { + "type": "int", + "example": "30" + } + } + }, + "legacyInvite": "{email} があなたを信頼する連絡先として招待しました", + "authToManageLegacy": "信頼する連絡先を管理するために認証してください", + "useDifferentPlayerInfo": "この動画の再生に問題がありますか?別のプレイヤーを試すには、ここを長押ししてください。", + "hideSharedItemsFromHomeGallery": "ホームギャラリーから共有された写真等を非表示", + "gallery": "ギャラリー", + "joinAlbum": "アルバムに参加", + "joinAlbumSubtext": "写真を表示したり、追加したりするために", + "joinAlbumSubtextViewer": "これを共有アルバムに追加するために", + "join": "参加する", + "linkEmail": "メールアドレスをリンクする", + "link": "リンク", + "noEnteAccountExclamation": "アカウントがありません!", + "orPickFromYourContacts": "または連絡先から選択", + "emailDoesNotHaveEnteAccount": "{email} は Ente アカウントを持っていません。", + "@emailDoesNotHaveEnteAccount": { + "description": "Shown when email doesn't have an Ente account", + "placeholders": { + "email": { + "type": "String" + } + } + }, + "accountOwnerPersonAppbarTitle": "{title} (私)", + "@accountOwnerPersonAppbarTitle": { + "description": "Title of appbar for account owner person", + "placeholders": { + "title": { + "type": "String" + } + } + }, + "reassignMe": "\"自分\" を再割り当て", + "me": "自分", + "linkEmailToContactBannerCaption": "共有を高速化するために", + "@linkEmailToContactBannerCaption": { + "description": "Caption for the 'Link email' title. It should be a continuation of the 'Link email' title. Just like how 'Link email' + 'for faster sharing' forms a proper sentence in English, the combination of these two strings should also be a proper sentence in other languages." + }, + "selectPersonToLink": "リンクする人を選択", + "linkPersonToEmail": "この人物を {email}に紐づけ", + "@linkPersonToEmail": { + "placeholders": { + "email": { + "type": "String" + } + } + }, + "linkPersonToEmailConfirmation": "{personName} を {email} に紐づけします", + "@linkPersonToEmailConfirmation": { + "description": "Confirmation message when linking a person to an email", + "placeholders": { + "personName": { + "type": "String" + }, + "email": { + "type": "String" + } + } + }, + "selectYourFace": "あなたの顔を選択", + "reassigningLoading": "再割り当て中...", + "reassignedToName": "あなたを {name} に紐づけました", + "@reassignedToName": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "saveChangesBeforeLeavingQuestion": "その前に変更を保存しますか?", + "dontSave": "保存しない", + "thisIsMeExclamation": "これは私です", + "linkPerson": "人を紐づけ", + "linkPersonCaption": "良い経験を分かち合うために", + "@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": "処理中", + "queued": "処理待ち", + "ineligible": "対象外", + "failed": "失敗", + "playStream": "再生", + "playOriginal": "元動画を再生", + "joinAlbumConfirmationDialogBody": "アルバムに参加すると、参加者にメールアドレスが公開されます。" } \ No newline at end of file diff --git a/mobile/lib/l10n/intl_km.arb b/mobile/lib/l10n/intl_km.arb index 1a5f45c37d..c8494661c6 100644 --- a/mobile/lib/l10n/intl_km.arb +++ b/mobile/lib/l10n/intl_km.arb @@ -1,4 +1,3 @@ { - "@@locale ": "en", - "orPickFromYourContacts": "or pick from your contacts" + "@@locale ": "en" } \ No newline at end of file diff --git a/mobile/lib/l10n/intl_ko.arb b/mobile/lib/l10n/intl_ko.arb index 45e987c9e6..06c81195f7 100644 --- a/mobile/lib/l10n/intl_ko.arb +++ b/mobile/lib/l10n/intl_ko.arb @@ -12,6 +12,5 @@ "feedback": "피드백", "confirmAccountDeletion": "계정 삭제 확인", "deleteAccountPermanentlyButton": "계정을 영구적으로 삭제", - "yourAccountHasBeenDeleted": "계정이 삭제되었습니다.", - "orPickFromYourContacts": "or pick from your contacts" + "yourAccountHasBeenDeleted": "계정이 삭제되었습니다." } \ No newline at end of file diff --git a/mobile/lib/l10n/intl_lt.arb b/mobile/lib/l10n/intl_lt.arb index 19b1ba3aab..4872089337 100644 --- a/mobile/lib/l10n/intl_lt.arb +++ b/mobile/lib/l10n/intl_lt.arb @@ -514,10 +514,16 @@ "cannotDeleteSharedFiles": "Negalima ištrinti bendrinamų failų.", "theDownloadCouldNotBeCompleted": "Atsisiuntimas negalėjo būti baigtas.", "retry": "Kartoti", + "backedUpFolders": "Sukurtos atsarginės aplankų kopijos", "backup": "Kurti atsarginę kopiją", + "freeUpDeviceSpace": "Atlaisvinti įrenginio vietą", + "freeUpDeviceSpaceDesc": "Sutaupykite vietos savo įrenginyje išvalydami failus, kurių atsarginės kopijos jau buvo sukurtos.", + "allClear": "✨ Viskas išvalyta", + "noDeviceThatCanBeDeleted": "Neturite šiame įrenginyje failų, kuriuos galima ištrinti.", "removeDuplicates": "Šalinti dublikatus", + "removeDuplicatesDesc": "Peržiūrėkite ir pašalinkite failus, kurie yra tiksliai dublikatai.", + "viewLargeFiles": "Dideli failai", "noDuplicates": "✨ Dublikatų nėra", - "youveNoDuplicateFilesThatCanBeCleared": "Neturite dubliuotų failų, kuriuos būtų galima išvalyti.", "success": "Sėkmė", "rateUs": "Vertinti mus", "remindToEmptyDeviceTrash": "Taip pat ištuštinkite Neseniai ištrinti iš Nustatymai -> Saugykla, kad atlaisvintumėte vietos.", @@ -547,6 +553,8 @@ "security": "Saugumas", "authToViewYourRecoveryKey": "Nustatykite tapatybę, kad peržiūrėtumėte savo atkūrimo raktą", "twofactor": "Dvigubas tapatybės nustatymas", + "lockscreen": "Ekrano užraktas", + "viewActiveSessions": "Peržiūrėti aktyvius seansus", "disableTwofactor": "Išjungti dvigubą tapatybės nustatymą", "confirm2FADisable": "Ar tikrai norite išjungti dvigubą tapatybės nustatymą?", "no": "Ne", @@ -564,7 +572,7 @@ "reportABug": "Pranešti apie riktą", "reportBug": "Pranešti apie riktą", "suggestFeatures": "Siūlyti funkcijas", - "support": "Palaikymas", + "support": "Pagalba", "theme": "Tema", "lightTheme": "Šviesi", "darkTheme": "Tamsi", @@ -589,6 +597,7 @@ "subscription": "Prenumerata", "paymentDetails": "Mokėjimo duomenys", "manageFamily": "Tvarkyti šeimą", + "contactToManageSubscription": "Susisiekite su mumis adresu support@ente.io, kad sutvarkytumėte savo {provider} prenumeratą.", "renewSubscription": "Atnaujinti prenumeratą", "cancelSubscription": "Atsisakyti prenumeratos", "yesCancel": "Taip, atsisakyti", @@ -626,6 +635,7 @@ "playstoreSubscription": "„PlayStore“ prenumerata", "subAlreadyLinkedErrMessage": "Jūsų {id} jau susietas su kita „Ente“ paskyra.\nJei norite naudoti savo {id} su šia paskyra, susisiekite su mūsų palaikymo komanda.", "visitWebToManage": "Aplankykite web.ente.io, kad tvarkytumėte savo prenumeratą", + "pleaseContactSupportAndWeWillBeHappyToHelp": "Susisiekite adresu support@ente.io ir mes mielai padėsime!", "paymentFailed": "Mokėjimas nepavyko", "paymentFailedTalkToProvider": "Kreipkitės į {providerName} palaikymo komandą, jei jums buvo nuskaičiuota.", "@paymentFailedTalkToProvider": { @@ -740,6 +750,8 @@ "description": "Page title while adding one or more items to album" }, "createOrSelectAlbum": "Kurkite arba pasirinkite albumą", + "searchByAlbumNameHint": "Albumo pavadinimas", + "albumTitle": "Albumo pavadinimas", "enterAlbumName": "Įveskite albumo pavadinimą", "restoringFiles": "Atkuriami failai...", "sharedWithMe": "Bendrinta su manimi", @@ -801,10 +813,13 @@ "theRecoveryKeyYouEnteredIsIncorrect": "Įvestas atkūrimo raktas yra neteisingas.", "pleaseVerifyTheCodeYouHaveEntered": "Patvirtinkite įvestą kodą.", "yourVerificationCodeHasExpired": "Jūsų patvirtinimo kodas nebegaliojantis.", + "emailChangedTo": "El. paštas pakeistas į {newEmail}", "verifying": "Patvirtinama...", + "disablingTwofactorAuthentication": "Išjungiamas dvigubas tapatybės nustatymas...", "allMemoriesPreserved": "Išsaugoti visi prisiminimai", "loadingGallery": "Įkeliama galerija...", "syncing": "Sinchronizuojama...", + "encryptingBackup": "Šifruojama atsarginė kopija...", "syncStopped": "Sinchronizavimas sustabdytas", "syncProgress": "{completed} / {total} išsaugomi prisiminimai", "@syncProgress": { @@ -1348,5 +1363,62 @@ "linkEmail": "Susieti el. paštą", "link": "Susieti", "noEnteAccountExclamation": "Nėra „Ente“ paskyros!", - "orPickFromYourContacts": "or pick from your contacts" + "orPickFromYourContacts": "arba pasirinkite iš savo kontaktų", + "emailDoesNotHaveEnteAccount": "{email} neturi „Ente“ paskyros.", + "@emailDoesNotHaveEnteAccount": { + "description": "Shown when email doesn't have an Ente account", + "placeholders": { + "email": { + "type": "String" + } + } + }, + "accountOwnerPersonAppbarTitle": "{title} (Aš)", + "@accountOwnerPersonAppbarTitle": { + "description": "Title of appbar for account owner person", + "placeholders": { + "title": { + "type": "String" + } + } + }, + "me": "Aš", + "linkPersonToEmailConfirmation": "Tai susies {personName} su {email}.", + "@linkPersonToEmailConfirmation": { + "description": "Confirmation message when linking a person to an email", + "placeholders": { + "personName": { + "type": "String" + }, + "email": { + "type": "String" + } + } + }, + "reassignedToName": "Perskirstė jus į {name}", + "@reassignedToName": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "saveChangesBeforeLeavingQuestion": "Išsaugoti pakeitimus prieš išeinant?", + "dontSave": "Neišsaugoti", + "thisIsMeExclamation": "Tai aš!", + "linkPerson": "Susiekite asmenį,", + "linkPersonCaption": "kad geriau bendrintumėte patirtį", + "@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", + "processingVideos": "Apdorojami vaizdo įrašai", + "streamDetails": "Srautinio perdavimo išsami informacija", + "processing": "Apdorojama", + "queued": "Įtraukta eilėje", + "ineligible": "Netinkami", + "failed": "Nepavyko", + "playStream": "Leisti srautinį perdavimą", + "playOriginal": "Leisti originalą", + "joinAlbumConfirmationDialogBody": "Prisijungus prie albumo, jūsų el. paštas bus matomas jo dalyviams." } \ No newline at end of file diff --git a/mobile/lib/l10n/intl_ml.arb b/mobile/lib/l10n/intl_ml.arb index 7c0a9b8a5f..7cfd3a2887 100644 --- a/mobile/lib/l10n/intl_ml.arb +++ b/mobile/lib/l10n/intl_ml.arb @@ -99,6 +99,5 @@ "nothingToSeeHere": "ഇവിടൊന്നും കാണ്മാനില്ല! 👀", "calculating": "കണക്കുകൂട്ടുന്നു...", "close": "അടക്കുക", - "count": "എണ്ണം", - "orPickFromYourContacts": "or pick from your contacts" + "count": "എണ്ണം" } \ No newline at end of file diff --git a/mobile/lib/l10n/intl_nl.arb b/mobile/lib/l10n/intl_nl.arb index 7ed11a22c2..607f64f44c 100644 --- a/mobile/lib/l10n/intl_nl.arb +++ b/mobile/lib/l10n/intl_nl.arb @@ -1,6 +1,6 @@ { "@@locale ": "en", - "enterYourEmailAddress": "Voer uw e-mailadres in", + "enterYourEmailAddress": "Voer je e-mailadres in", "accountWelcomeBack": "Welkom terug!", "emailAlreadyRegistered": "E-mail is al geregistreerd.", "emailNotRegistered": "E-mail niet geregistreerd.", @@ -478,7 +478,7 @@ "showMemories": "Toon herinneringen", "yearsAgo": "{count, plural, one{{count} jaar geleden} other{{count} jaar geleden}}", "backupSettings": "Back-up instellingen", - "backupStatus": "Back-upstatus", + "backupStatus": "Back-up status", "backupStatusDescription": "Items die zijn geback-upt, worden hier getoond", "backupOverMobileData": "Back-up maken via mobiele data", "backupVideos": "Back-up video's", @@ -525,7 +525,6 @@ "viewLargeFiles": "Grote bestanden", "viewLargeFilesDesc": "Bekijk bestanden die de meeste opslagruimte verbruiken.", "noDuplicates": "✨ Geen duplicaten", - "youveNoDuplicateFilesThatCanBeCleared": "Je hebt geen dubbele bestanden die kunnen worden gewist", "success": "Succes", "rateUs": "Beoordeel ons", "remindToEmptyDeviceTrash": "Leeg ook \"Onlangs verwijderd\" uit \"Instellingen\" -> \"Opslag\" om de vrij gekomen ruimte te benutten", @@ -965,6 +964,14 @@ "syncStopped": "Synchronisatie gestopt", "syncProgress": "{completed}/{total} herinneringen bewaard", "uploadingMultipleMemories": "{count} herinneringen veiligstellen...", + "@uploadingMultipleMemories": { + "description": "Text to tell user how many memories are being preserved", + "placeholders": { + "count": { + "type": "String" + } + } + }, "uploadingSingleMemory": "1 herinnering veiligstellen...", "@syncProgress": { "description": "Text to tell user how many memories have been preserved", @@ -1590,5 +1597,81 @@ "joinAlbumSubtext": "om je foto's te bekijken en toe te voegen", "joinAlbumSubtextViewer": "om dit aan gedeelde albums toe te voegen", "join": "Deelnemen", - "orPickFromYourContacts": "or pick from your contacts" + "linkEmail": "Link email", + "link": "Link", + "noEnteAccountExclamation": "Geen Ente account!", + "orPickFromYourContacts": "of kies uit je contacten", + "emailDoesNotHaveEnteAccount": "{email} heeft geen Ente account.", + "@emailDoesNotHaveEnteAccount": { + "description": "Shown when email doesn't have an Ente account", + "placeholders": { + "email": { + "type": "String" + } + } + }, + "accountOwnerPersonAppbarTitle": "{title} (Ik)", + "@accountOwnerPersonAppbarTitle": { + "description": "Title of appbar for account owner person", + "placeholders": { + "title": { + "type": "String" + } + } + }, + "reassignMe": "\"Ik\" opnieuw toewijzen", + "me": "Ik", + "linkEmailToContactBannerCaption": "voor sneller delen", + "@linkEmailToContactBannerCaption": { + "description": "Caption for the 'Link email' title. It should be a continuation of the 'Link email' title. Just like how 'Link email' + 'for faster sharing' forms a proper sentence in English, the combination of these two strings should also be a proper sentence in other languages." + }, + "selectPersonToLink": "Kies persoon om te linken", + "linkPersonToEmail": "Link persoon aan {email}", + "@linkPersonToEmail": { + "placeholders": { + "email": { + "type": "String" + } + } + }, + "linkPersonToEmailConfirmation": "Dit linkt {personName} aan {email}", + "@linkPersonToEmailConfirmation": { + "description": "Confirmation message when linking a person to an email", + "placeholders": { + "personName": { + "type": "String" + }, + "email": { + "type": "String" + } + } + }, + "selectYourFace": "Selecteer je gezicht", + "reassigningLoading": "Opnieuw toewijzen...", + "reassignedToName": "Toegewezen aan {name}", + "@reassignedToName": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "saveChangesBeforeLeavingQuestion": "Wijzigingen opslaan voor verlaten?", + "dontSave": "Niet opslaan", + "thisIsMeExclamation": "Dit ben ik!", + "linkPerson": "Link persoon", + "linkPersonCaption": "voor een betere ervaring met delen", + "@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", + "processingVideos": "Video's verwerken", + "streamDetails": "Stream details", + "processing": "Verwerken", + "queued": "In wachtrij", + "ineligible": "Ongerechtigd", + "failed": "Mislukt", + "playStream": "Stream afspelen", + "playOriginal": "Origineel afspelen", + "joinAlbumConfirmationDialogBody": "Deelnemen aan een album maakt je e-mail zichtbaar voor de deelnemers." } \ No newline at end of file diff --git a/mobile/lib/l10n/intl_no.arb b/mobile/lib/l10n/intl_no.arb index 0a13c2202c..5fd97c2369 100644 --- a/mobile/lib/l10n/intl_no.arb +++ b/mobile/lib/l10n/intl_no.arb @@ -370,6 +370,5 @@ "advanced": "Avansert", "general": "Generelt", "security": "Sikkerhet", - "authToViewYourRecoveryKey": "Vennligst autentiser deg for å se gjennopprettingsnøkkelen din", - "orPickFromYourContacts": "or pick from your contacts" + "authToViewYourRecoveryKey": "Vennligst autentiser deg for å se gjennopprettingsnøkkelen din" } \ No newline at end of file diff --git a/mobile/lib/l10n/intl_pl.arb b/mobile/lib/l10n/intl_pl.arb index c8a1bd4fa1..7f9d8186b4 100644 --- a/mobile/lib/l10n/intl_pl.arb +++ b/mobile/lib/l10n/intl_pl.arb @@ -2,6 +2,8 @@ "@@locale ": "en", "enterYourEmailAddress": "Podaj swój adres e-mail", "accountWelcomeBack": "Witaj ponownie!", + "emailAlreadyRegistered": "Adres e-mail jest już zarejestrowany.", + "emailNotRegistered": "Adres e-mail nie jest zarejestrowany.", "email": "Adres e-mail", "cancel": "Anuluj", "verify": "Zweryfikuj", @@ -185,6 +187,8 @@ }, "allowAddPhotosDescription": "Pozwól osobom z linkiem na dodawania zdjęć do udostępnionego albumu.", "passwordLock": "Blokada hasłem", + "canNotOpenTitle": "Nie można otworzyć tego albumu", + "canNotOpenBody": "Przepraszamy, ten album nie może zostać otwarty w aplikacji.", "disableDownloadWarningTitle": "Uwaga", "disableDownloadWarningBody": "Widzowie mogą nadal robić zrzuty ekranu lub zapisywać kopie zdjęć za pomocą programów trzecich", "allowDownloads": "Zezwól na pobieranie", @@ -351,6 +355,7 @@ "failedToLoadAlbums": "Nie udało się załadować albumów", "hidden": "Ukryte", "authToViewYourHiddenFiles": "Prosimy uwierzytelnić się, aby wyświetlić ukryte pliki", + "authToViewTrashedFiles": "Prosimy uwierzytelnić się, aby wyświetlić swoje pliki w koszu", "trash": "Kosz", "uncategorized": "Bez kategorii", "videoSmallCase": "wideo", @@ -520,7 +525,6 @@ "viewLargeFiles": "Duże pliki", "viewLargeFilesDesc": "Wyświetl pliki zużywające największą ilość pamięci.", "noDuplicates": "✨ Brak duplikatów", - "youveNoDuplicateFilesThatCanBeCleared": "Nie masz duplikatów plików, które mogą być wyczyszczone", "success": "Sukces", "rateUs": "Oceń nas", "remindToEmptyDeviceTrash": "Również opróżnij \"Ostatnio usunięte\" z \"Ustawienia\" -> \"Pamięć\", aby odebrać wolną przestrzeń", @@ -960,6 +964,14 @@ "syncStopped": "Synchronizacja zatrzymana", "syncProgress": "Zachowano {completed}/{total} wspomnień", "uploadingMultipleMemories": "Zachowywanie {count} wspomnień...", + "@uploadingMultipleMemories": { + "description": "Text to tell user how many memories are being preserved", + "placeholders": { + "count": { + "type": "String" + } + } + }, "uploadingSingleMemory": "Zachowywanie 1 wspomnienia...", "@syncProgress": { "description": "Text to tell user how many memories have been preserved", @@ -1377,6 +1389,7 @@ "extraPhotosFound": "Znaleziono dodatkowe zdjęcia", "configuration": "Konfiguracja", "localIndexing": "Indeksowanie lokalne", + "processed": "Przetworzone", "resetPerson": "Usuń", "areYouSureYouWantToResetThisPerson": "Czy na pewno chcesz zresetować tę osobę?", "allPersonGroupingWillReset": "Wszystkie grupy dla tej osoby zostaną zresetowane i stracisz wszystkie sugestie dla tej osoby", @@ -1578,5 +1591,11 @@ "legacyInvite": "{email} zaprosił Cię do zostania zaufanym kontaktem", "authToManageLegacy": "Prosimy uwierzytelnić się, aby zarządzać zaufanymi kontaktami", "useDifferentPlayerInfo": "Masz problem z odtwarzaniem tego wideo? Przytrzymaj tutaj, aby spróbować innego odtwarzacza.", - "orPickFromYourContacts": "or pick from your contacts" + "gallery": "Galeria", + "joinAlbum": "Dołącz do albumu", + "joinAlbumSubtext": "aby wyświetlić i dodać swoje zdjęcia", + "joinAlbumSubtextViewer": "aby dodać to do udostępnionych albumów", + "join": "Dołącz", + "linkEmail": "Połącz adres e-mail", + "noEnteAccountExclamation": "Brak konta Ente!" } \ No newline at end of file diff --git a/mobile/lib/l10n/intl_pt.arb b/mobile/lib/l10n/intl_pt.arb index a27356fbc1..5e527cb705 100644 --- a/mobile/lib/l10n/intl_pt.arb +++ b/mobile/lib/l10n/intl_pt.arb @@ -25,8 +25,8 @@ "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 à account-deletion@ente.io do seu endereço de e-mail registrado.", - "entePhotosPerm": "Ente precisa de sua permissão para preservar suas fotos", + "deleteEmailRequest": "Por favor, envie um e-mail a account-deletion@ente.io do seu endereço de e-mail registrado.", + "entePhotosPerm": "Ente precisa de permissão para preservar suas fotos", "ok": "OK", "createAccount": "Criar conta", "createNewAccount": "Criar nova conta", @@ -55,7 +55,7 @@ "checkInboxAndSpamFolder": "Verifique sua caixa de entrada (e spam) para concluir a verificação", "tapToEnterCode": "Toque para inserir código", "resendEmail": "Reenviar e-mail", - "weHaveSendEmailTo": "Nós enviamos um e-mail à {email}", + "weHaveSendEmailTo": "Enviamos um e-mail à {email}", "@weHaveSendEmailTo": { "description": "Text to indicate that we have sent a mail to the user", "placeholders": { @@ -525,7 +525,7 @@ "viewLargeFiles": "Arquivos grandes", "viewLargeFilesDesc": "Ver arquivos que consumem a maior parte do armazenamento.", "noDuplicates": "✨ Sem duplicatas", - "youveNoDuplicateFilesThatCanBeCleared": "Você não tem arquivos duplicados que possam ser limpos", + "youveNoDuplicateFilesThatCanBeCleared": "Você não possui nenhum arquivo duplicado que possa ser excluído", "success": "Sucesso", "rateUs": "Avaliar", "remindToEmptyDeviceTrash": "Também vazio \"Excluído recentemente\" de \"Opções\" -> \"Armazenamento\" para reivindicar espaço liberado", @@ -965,6 +965,14 @@ "syncStopped": "Sincronização interrompida", "syncProgress": "{completed}/{total} memórias preservadas", "uploadingMultipleMemories": "Preservando {count} memórias...", + "@uploadingMultipleMemories": { + "description": "Text to tell user how many memories are being preserved", + "placeholders": { + "count": { + "type": "String" + } + } + }, "uploadingSingleMemory": "Preservando 1 memória...", "@syncProgress": { "description": "Text to tell user how many memories have been preserved", @@ -1260,7 +1268,7 @@ }, "findPeopleByName": "Busque pessoas facilmente pelo nome", "addViewers": "{count, plural, one {Adicionar visualizador} other {Adicionar visualizadores}}", - "addCollaborators": "{count, plural, one {Adicionar colaborador} other {Adicionar colaboradores}}", + "addCollaborators": "{count, plural, 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", @@ -1529,8 +1537,8 @@ "removeYourselfAsTrustedContact": "Remover si mesmo dos contatos confiáveis", "legacy": "Legado", "legacyPageDesc": "O legado permite que contatos confiáveis acessem sua conta em sua ausência.", - "legacyPageDesc2": "Contatos confiáveis podem iniciar recuperação de conta, e se não for cancelado dentro de 30 dias, redefina sua senha e acesse sua conta.", - "legacyAccounts": "Contas legado", + "legacyPageDesc2": "Contatos confiáveis podem iniciar recuperação de conta. Se não cancelado dentro de 30 dias, redefina sua senha e acesse sua conta.", + "legacyAccounts": "Contas legadas", "trustedContacts": "Contatos confiáveis", "addTrustedContact": "Adicionar contato confiável", "removeInvite": "Remover convite", @@ -1593,5 +1601,78 @@ "linkEmail": "Vincular e-mail", "link": "Vincular", "noEnteAccountExclamation": "Nenhuma conta Ente!", - "orPickFromYourContacts": "or pick from your contacts" + "orPickFromYourContacts": "ou escolher dos seus contatos", + "emailDoesNotHaveEnteAccount": "{email} não possui uma conta Ente.", + "@emailDoesNotHaveEnteAccount": { + "description": "Shown when email doesn't have an Ente account", + "placeholders": { + "email": { + "type": "String" + } + } + }, + "accountOwnerPersonAppbarTitle": "{title} (Eu)", + "@accountOwnerPersonAppbarTitle": { + "description": "Title of appbar for account owner person", + "placeholders": { + "title": { + "type": "String" + } + } + }, + "reassignMe": "Reatribuir \"Eu\"", + "me": "Eu", + "linkEmailToContactBannerCaption": "para compartilhar rápido", + "@linkEmailToContactBannerCaption": { + "description": "Caption for the 'Link email' title. It should be a continuation of the 'Link email' title. Just like how 'Link email' + 'for faster sharing' forms a proper sentence in English, the combination of these two strings should also be a proper sentence in other languages." + }, + "selectPersonToLink": "Selecione a pessoa para vincular", + "linkPersonToEmail": "Vincular pessoa a {email}", + "@linkPersonToEmail": { + "placeholders": { + "email": { + "type": "String" + } + } + }, + "linkPersonToEmailConfirmation": "Isso vinculará {personName} a {email}", + "@linkPersonToEmailConfirmation": { + "description": "Confirmation message when linking a person to an email", + "placeholders": { + "personName": { + "type": "String" + }, + "email": { + "type": "String" + } + } + }, + "selectYourFace": "Selecione seu rosto", + "reassigningLoading": "Reatribuindo...", + "reassignedToName": "Atribuído a {name}", + "@reassignedToName": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "saveChangesBeforeLeavingQuestion": "Salvar mudanças antes de sair?", + "dontSave": "Não salvar", + "thisIsMeExclamation": "Este é você!", + "linkPerson": "Vincular pessoa", + "linkPersonCaption": "para melhor experiência de compartilhamento", + "@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", + "processingVideos": "Processando vídeos", + "streamDetails": "Detalhes da transmissão", + "processing": "Processando", + "queued": "Na fila", + "ineligible": "Inelegível", + "failed": "Falhou", + "playStream": "Reproduzir transmissão", + "playOriginal": "Reproduzir original", + "joinAlbumConfirmationDialogBody": "Unir-se ao álbum fará que seu e-mail seja visível a todos do álbum." } \ No newline at end of file diff --git a/mobile/lib/l10n/intl_ro.arb b/mobile/lib/l10n/intl_ro.arb index 4a2d3f6a1c..b4a6936e7e 100644 --- a/mobile/lib/l10n/intl_ro.arb +++ b/mobile/lib/l10n/intl_ro.arb @@ -22,7 +22,7 @@ "deleteReason1": "Lipsește o funcție cheie de care am nevoie", "deleteReason2": "Aplicația sau o anumită funcție nu se comportă așa cum cred eu că ar trebui", "deleteReason3": "Am găsit un alt serviciu care îmi place mai mult", - "deleteReason4": "Motivul meu nu este listat", + "deleteReason4": "Motivul meu nu apare", "sendEmail": "Trimiteți e-mail", "deleteRequestSLAText": "Solicitarea dvs. va fi procesată în 72 de ore.", "deleteEmailRequest": "Vă rugăm să trimiteți un e-mail la account-deletion@ente.io de pe adresa dvs. de e-mail înregistrată.", @@ -525,7 +525,6 @@ "viewLargeFiles": "Fișiere mari", "viewLargeFilesDesc": "Vizualizați fișierele care consumă cel mai mult spațiu.", "noDuplicates": "✨ Fără dubluri", - "youveNoDuplicateFilesThatCanBeCleared": "Nu aveți dubluri care pot fi șterse", "success": "Succes", "rateUs": "Evaluați-ne", "remindToEmptyDeviceTrash": "De asemenea, goliți dosarul „Șterse recent” din „Setări” -> „Spațiu” pentru a recupera spațiul eliberat", @@ -965,6 +964,14 @@ "syncStopped": "Sincronizare oprită", "syncProgress": "{completed}/{total} amintiri salvate", "uploadingMultipleMemories": "Se salvează {count} amintiri...", + "@uploadingMultipleMemories": { + "description": "Text to tell user how many memories are being preserved", + "placeholders": { + "count": { + "type": "String" + } + } + }, "uploadingSingleMemory": "Se salvează o amintire...", "@syncProgress": { "description": "Text to tell user how many memories have been preserved", @@ -1392,26 +1399,8 @@ "enableMachineLearningBanner": "Activați învățarea automată pentru a folosi căutarea magică și recunoașterea facială", "searchDiscoverEmptySection": "Imaginile vor fi afișate aici odată ce procesarea și sincronizarea este completă", "searchPersonsEmptySection": "Persoanele vor fi afișate aici odată ce procesarea și sincronizarea este completă", - "viewersSuccessfullyAdded": "{count, plural, few {S-au adăugat {count} observatori}=0 {S-au adăugat 0 observatori} =1 {S-a adăugat 1 observator} other {S-au adăugat {count} de observatori}}", - "@viewersSuccessfullyAdded": { - "placeholders": { - "count": { - "type": "int", - "example": "2" - } - }, - "description": "Number of viewers that were successfully added to an album." - }, - "collaboratorsSuccessfullyAdded": "{count, plural, few {S-au adăugat {count} colaboratori}=0 {S-au adăugat 0 colaboratori} =1 {S-a adăugat 1 colaborator} other {S-au adăugat {count} de colaboratori}}", - "@collaboratorsSuccessfullyAdded": { - "placeholders": { - "count": { - "type": "int", - "example": "2" - } - }, - "description": "Number of collaborators that were successfully added to an album." - }, + + "accountIsAlreadyConfigured": "Contul este deja configurat.", "sessionIdMismatch": "Nepotrivire ID sesiune", "@sessionIdMismatch": { @@ -1477,7 +1466,7 @@ }, "currentlyRunning": "rulează în prezent", "ignored": "ignorat", - "photosCount": "{count, plural, few {{count} fotografii}=0 {0 fotografii} =1 {1 fotografie} other {{count} de fotografii}}", + "photosCount": "{count, plural, =0 {0 Fotografii} =1 {O Fotografie} other {{count} Fotografii}}", "@photosCount": { "placeholders": { "count": { @@ -1589,6 +1578,5 @@ "joinAlbum": "Alăturați-vă albumului", "joinAlbumSubtext": "pentru a vedea și a adăuga fotografii", "joinAlbumSubtextViewer": "pentru a adăuga la albumele distribuite", - "join": "Alăturare", - "orPickFromYourContacts": "or pick from your contacts" + "join": "Alăturare" } \ No newline at end of file diff --git a/mobile/lib/l10n/intl_ru.arb b/mobile/lib/l10n/intl_ru.arb index 279a92b952..5695eb350c 100644 --- a/mobile/lib/l10n/intl_ru.arb +++ b/mobile/lib/l10n/intl_ru.arb @@ -504,7 +504,6 @@ "removeDuplicatesDesc": "Просмотрите и удалите точные дубликаты.", "viewLargeFiles": "Большие файлы", "noDuplicates": "✨ Дубликатов нет", - "youveNoDuplicateFilesThatCanBeCleared": "У вас нет дубликатов файлов, которые можно очистить", "success": "Успешно", "rateUs": "Оцените нас", "remindToEmptyDeviceTrash": "Также очистите \"Недавно удалённые\" из \"Настройки\" -> \"Хранилище\", чтобы получить больше свободного места", @@ -1462,6 +1461,5 @@ "removeInvite": "Удалить приглашение", "recoveryWarning": "Доверенный контакт пытается получить доступ к вашей учетной записи", "rejectRecovery": "Отклонить восстановление", - "recoveryInitiated": "Восстановление запущено", - "orPickFromYourContacts": "or pick from your contacts" + "recoveryInitiated": "Восстановление запущено" } \ No newline at end of file diff --git a/mobile/lib/l10n/intl_sl.arb b/mobile/lib/l10n/intl_sl.arb index 1a5f45c37d..c8494661c6 100644 --- a/mobile/lib/l10n/intl_sl.arb +++ b/mobile/lib/l10n/intl_sl.arb @@ -1,4 +1,3 @@ { - "@@locale ": "en", - "orPickFromYourContacts": "or pick from your contacts" + "@@locale ": "en" } \ No newline at end of file diff --git a/mobile/lib/l10n/intl_sv.arb b/mobile/lib/l10n/intl_sv.arb index fdc0021367..5793132069 100644 --- a/mobile/lib/l10n/intl_sv.arb +++ b/mobile/lib/l10n/intl_sv.arb @@ -2,6 +2,8 @@ "@@locale ": "en", "enterYourEmailAddress": "Ange din e-postadress", "accountWelcomeBack": "Välkommen tillbaka!", + "emailAlreadyRegistered": "E-postadress redan registrerad.", + "emailNotRegistered": "E-postadressen är inte registrerad.", "email": "E-post", "cancel": "Avbryt", "verify": "Bekräfta", @@ -12,6 +14,7 @@ "deleteAccountFeedbackPrompt": "Vi är ledsna att se dig lämna oss. Vänligen dela dina synpunkter för att hjälpa oss att förbättra.", "feedback": "Feedback", "kindlyHelpUsWithThisInformation": "Vänligen hjälp oss med denna information", + "confirmDeletePrompt": "Ja, jag vill permanent ta bort detta konto och data i alla appar.", "confirmAccountDeletion": "Bekräfta radering av konto", "deleteAccountPermanentlyButton": "Radera kontot permanent", "yourAccountHasBeenDeleted": "Ditt konto har raderats", @@ -149,6 +152,7 @@ "tryAgain": "Försök igen", "viewRecoveryKey": "Visa återställningsnyckel", "confirmRecoveryKey": "Bekräfta återställningsnyckel", + "recoveryKeyVerifyReason": "Din återställningsnyckel är det enda sättet att återställa dina foton om du glömmer ditt lösenord. Du hittar din återställningsnyckel i Inställningar > Säkerhet.\n\nAnge din återställningsnyckel här för att verifiera att du har sparat den ordentligt.", "confirmYourRecoveryKey": "Bekräfta din återställningsnyckel", "addViewer": "Lägg till bildvy", "addCollaborator": "Lägg till samarbetspartner", @@ -183,6 +187,8 @@ }, "allowAddPhotosDescription": "Tillåt personer med länken att även lägga till foton i det delade albumet.", "passwordLock": "Lösenordskydd", + "canNotOpenTitle": "Kan inte öppna det här albumet", + "canNotOpenBody": "Tyvärr kan detta album inte öppnas i appen.", "disableDownloadWarningTitle": "Vänligen notera:", "disableDownloadWarningBody": "Besökare kan fortfarande ta skärmdumpar eller spara en kopia av dina foton med hjälp av externa verktyg", "allowDownloads": "Tillåt nedladdningar", @@ -271,18 +277,74 @@ "failedToApplyCode": "Det gick inte att använda koden", "enterReferralCode": "Ange hänvisningskod", "codeAppliedPageTitle": "Kod tillämpad", + "changeYourReferralCode": "Ändra din värvningskod", "change": "Ändra", + "unavailableReferralCode": "Tyvärr är denna kod inte tillgänglig.", + "codeChangeLimitReached": "Tyvärr, du har nått gränsen för kodändringar.", "onlyFamilyAdminCanChangeCode": "Kontakta {familyAdminEmail} för att ändra din kod.", "storageInGB": "{storageAmountInGB} GB", "claimed": "Nyttjad", "@claimed": { "description": "Used to indicate storage claimed, like 10GB Claimed" }, + "details": "Uppgifter", + "claimMore": "Begär mer!", + "theyAlsoGetXGb": "De får också {storageAmountInGB} GB", + "freeStorageOnReferralSuccess": "{storageAmountInGB} GB varje gång någon registrerar sig för en betalplan och tillämpar din kod", + "shareTextReferralCode": "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", + "claimFreeStorage": "Hämta kostnadsfri lagring", "inviteYourFriends": "Bjud in dina vänner", + "failedToFetchReferralDetails": "Det gick inte att hämta hänvisningsdetaljer. Försök igen senare.", + "referralStep1": "1. Ge denna kod till dina vänner", + "referralStep2": "2. De registrerar sig för en betalplan", + "referralStep3": "3. Ni får båda {storageInGB} GB* gratis", + "referralsAreCurrentlyPaused": "Hänvisningar är för närvarande pausade", + "youCanAtMaxDoubleYourStorage": "* Du kan max fördubbla ditt lagringsutrymme", + "claimedStorageSoFar": "{isFamilyMember, select, true {Din familj har begärt {storageAmountInGb} GB} false {{storageAmountInGb}} other {Du har begärt {storageAmountInGb} GB!}}", + "@claimedStorageSoFar": { + "placeholders": { + "isFamilyMember": { + "type": "String", + "example": "true" + }, + "storageAmountInGb": { + "type": "int", + "example": "10" + } + } + }, + "faq": "Vanliga frågor och svar", "help": "Hjälp", + "oopsSomethingWentWrong": "Oj, något gick fel", + "peopleUsingYourCode": "Personer som använder din kod", + "eligible": "berättigad", + "total": "totalt", + "codeUsedByYou": "Kod som används av dig", + "freeStorageClaimed": "Gratis lagring begärd", + "freeStorageUsable": "Gratis lagringsutrymme som kan användas", + "usableReferralStorageInfo": "Användbart lagringsutrymme begränsas av din nuvarande plan. Överskrider du lagringsutrymmet kommer automatiskt att kunna använda det när du uppgraderar din plan.", + "removeFromAlbumTitle": "Ta bort från album?", + "removeFromAlbum": "Ta bort från album", + "itemsWillBeRemovedFromAlbum": "Valda objekt kommer att tas bort från detta album", + "removeShareItemsWarning": "Några av de objekt som du tar bort lades av andra personer, och du kommer att förlora tillgång till dem", + "addingToFavorites": "Lägger till bland favoriter...", + "removingFromFavorites": "Tar bort från favoriter...", + "sorryCouldNotAddToFavorites": "Tyvärr, kunde inte lägga till i favoriterna!", + "sorryCouldNotRemoveFromFavorites": "Tyvärr kunde inte ta bort från favoriter!", + "subscribeToEnableSharing": "Du behöver en aktiv betald prenumeration för att möjliggöra delning.", "subscribe": "Prenumerera", + "canOnlyRemoveFilesOwnedByYou": "Kan endast ta bort filer som ägs av dig", + "deleteSharedAlbum": "Radera delat album?", "deleteAlbum": "Radera album", + "deleteAlbumDialog": "Ta också bort foton (och videor) som finns i detta album från alla andra album som de är en del av?", + "deleteSharedAlbumDialogBody": "Albumet kommer att raderas för alla\n\nDu kommer att förlora åtkomst till delade foton i detta album som ägs av andra", + "yesRemove": "Ja, ta bort", + "creatingLink": "Skapar länk...", + "removeWithQuestionMark": "Ta bort?", + "removeParticipantBody": "{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", + "keepPhotos": "Behåll foton", "deletePhotos": "Radera foton", + "inviteToEnte": "Bjud in till Ente", "trash": "Papperskorg", "photoSmallCase": "foto", "yesDelete": "Ja, radera", @@ -295,10 +357,34 @@ "discover_receipts": "Kvitton", "discover_notes": "Anteckningar", "status": "Status", + "clearIndexes": "Rensa index", + "selectFoldersForBackup": "Välj mappar för säkerhetskopiering", + "selectedFoldersWillBeEncryptedAndBackedUp": "Valda mappar kommer att krypteras och säkerhetskopieras", + "unselectAll": "Avmarkera alla", + "selectAll": "Markera allt", "skip": "Hoppa över", + "updatingFolderSelection": "Uppdaterar mappval...", "itemCount": "{count, plural, one{{count} objekt} other{{count} objekt}}", "deleteItemCount": "{count, plural, =1 {Radera {count} objekt} other {Radera {count} objekt}}", + "duplicateItemsGroup": "{count} filer, {formattedSize} vardera", + "@duplicateItemsGroup": { + "description": "Display the number of duplicate files and their size", + "type": "text", + "placeholders": { + "count": { + "example": "12", + "type": "int" + }, + "formattedSize": { + "example": "2.3 MB", + "type": "String" + } + } + }, + "showMemories": "Visa minnen", "yearsAgo": "{count, plural, one{{count} år sedan} other{{count} år sedan}}", + "backupSettings": "Säkerhetskopieringsinställningar", + "backupStatus": "Säkerhetskopieringsstatus", "about": "Om", "terms": "Villkor", "account": "Konto", @@ -368,6 +454,14 @@ "theRecoveryKeyYouEnteredIsIncorrect": "Återställningsnyckeln du angav är felaktig", "twofactorAuthenticationHasBeenDisabled": "Tvåfaktorsautentisering har inaktiverats", "uploadingMultipleMemories": "Bevarar {count} minnen...", + "@uploadingMultipleMemories": { + "description": "Text to tell user how many memories are being preserved", + "placeholders": { + "count": { + "type": "String" + } + } + }, "uploadingSingleMemory": "Bevarar 1 minne...", "viewLogs": "Visa loggar", "copyEmailAddress": "Kopiera e-postadress", @@ -445,6 +539,5 @@ "sort": "Sortera", "newPerson": "Ny person", "addName": "Lägg till namn", - "add": "Lägg till", - "orPickFromYourContacts": "or pick from your contacts" + "add": "Lägg till" } \ No newline at end of file diff --git a/mobile/lib/l10n/intl_ta.arb b/mobile/lib/l10n/intl_ta.arb index 5304ad1d0a..d3d26e203c 100644 --- a/mobile/lib/l10n/intl_ta.arb +++ b/mobile/lib/l10n/intl_ta.arb @@ -15,6 +15,5 @@ "confirmDeletePrompt": "ஆம், எல்லா செயலிகளிலும் இந்தக் கணக்கையும் அதன் தரவையும் நிரந்தரமாக நீக்க விரும்புகிறேன்.", "confirmAccountDeletion": "கணக்கு நீக்குதலை உறுதிப்படுத்தவும்", "deleteAccountPermanentlyButton": "கணக்கை நிரந்தரமாக நீக்கவும்", - "deleteReason1": "எனக்கு தேவையான ஒரு முக்கிய அம்சம் இதில் இல்லை", - "orPickFromYourContacts": "or pick from your contacts" + "deleteReason1": "எனக்கு தேவையான ஒரு முக்கிய அம்சம் இதில் இல்லை" } \ No newline at end of file diff --git a/mobile/lib/l10n/intl_te.arb b/mobile/lib/l10n/intl_te.arb index 1a5f45c37d..c8494661c6 100644 --- a/mobile/lib/l10n/intl_te.arb +++ b/mobile/lib/l10n/intl_te.arb @@ -1,4 +1,3 @@ { - "@@locale ": "en", - "orPickFromYourContacts": "or pick from your contacts" + "@@locale ": "en" } \ No newline at end of file diff --git a/mobile/lib/l10n/intl_th.arb b/mobile/lib/l10n/intl_th.arb index edc625346c..375d1cc22d 100644 --- a/mobile/lib/l10n/intl_th.arb +++ b/mobile/lib/l10n/intl_th.arb @@ -295,6 +295,5 @@ "description": "Label for the map view" }, "maps": "แผนที่", - "enableMaps": "เปิดใช้งานแผนที่", - "orPickFromYourContacts": "or pick from your contacts" + "enableMaps": "เปิดใช้งานแผนที่" } \ No newline at end of file diff --git a/mobile/lib/l10n/intl_ti.arb b/mobile/lib/l10n/intl_ti.arb index 1a5f45c37d..c8494661c6 100644 --- a/mobile/lib/l10n/intl_ti.arb +++ b/mobile/lib/l10n/intl_ti.arb @@ -1,4 +1,3 @@ { - "@@locale ": "en", - "orPickFromYourContacts": "or pick from your contacts" + "@@locale ": "en" } \ No newline at end of file diff --git a/mobile/lib/l10n/intl_tr.arb b/mobile/lib/l10n/intl_tr.arb index c7d7150de7..80bbeef0a8 100644 --- a/mobile/lib/l10n/intl_tr.arb +++ b/mobile/lib/l10n/intl_tr.arb @@ -2,6 +2,8 @@ "@@locale ": "en", "enterYourEmailAddress": "E-posta adresinizi girin", "accountWelcomeBack": "Tekrar hoş geldiniz!", + "emailAlreadyRegistered": "Bu e-posta adresi zaten kayıtlı.", + "emailNotRegistered": "Bu e-posta adresi sistemde kayıtlı değil.", "email": "E-Posta", "cancel": "İptal Et", "verify": "Doğrula", @@ -150,6 +152,7 @@ "tryAgain": "Tekrar deneyiniz", "viewRecoveryKey": "Kurtarma anahtarını görüntüle", "confirmRecoveryKey": "Kurtarma anahtarını doğrula", + "recoveryKeyVerifyReason": "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.", "confirmYourRecoveryKey": "Kurtarma anahtarını doğrulayın", "addViewer": "Görüntüleyici ekle", "addCollaborator": "Düzenleyici ekle", @@ -184,6 +187,8 @@ }, "allowAddPhotosDescription": "Bağlantıya sahip olan kişilere, paylaşılan albüme fotoğraf eklemelerine izin ver.", "passwordLock": "Sifre 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", @@ -225,6 +230,7 @@ }, "description": "Number of participants in an album, including the album owner." }, + "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ı", "shareWithNonenteUsers": "Ente kullanıcısı olmayanlar için paylaş", @@ -234,6 +240,7 @@ "linkHasExpired": "Bağlantının süresi dolmuş", "publicLinkEnabled": "Herkese açık bağlantı aktive edildi", "shareALink": "Linki 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": { "placeholders": { @@ -262,6 +269,7 @@ "shareTextConfirmOthersVerificationID": "Merhaba, bu ente.io doğrulama kimliğinizin doğruluğunu onaylayabilir misiniz: {verificationID}", "somethingWentWrong": "Bazı şeyler yanlış gitti", "sendInvite": "Davet kodu gönder", + "shareTextRecommendUsingEnte": "Orijinal kalitede fotoğraf ve videoları kolayca paylaşabilmemiz için Ente'yi indirin\n\nhttps://ente.io", "done": "Bitti", "applyCodeTitle": "Kodu girin", "enterCodeDescription": "Arkadaşınız tarafından sağlanan kodu girerek hem sizin hem de arkadaşınızın ücretsiz depolamayı talep etmek için girin", @@ -269,6 +277,11 @@ "failedToApplyCode": "Uygulanırken hata oluştu", "enterReferralCode": "Davet kodunuzu girin", "codeAppliedPageTitle": "Kod kabul edildi", + "changeYourReferralCode": "Referans kodunuzu değiştirin", + "change": "Değiştir", + "unavailableReferralCode": "Üzgünüz, bu kod mevcut değil.", + "codeChangeLimitReached": "Üzgünüz, kod değişikliklerinin sınırına ulaştınız.", + "onlyFamilyAdminCanChangeCode": "Kodunuzu değiştirmek için lütfen {familyAdminEmail} ile iletişime geçin.", "storageInGB": "{storageAmountInGB} GB", "claimed": "Alındı", "@claimed": { @@ -278,6 +291,7 @@ "claimMore": "Arttır!", "theyAlsoGetXGb": "Aynı zamanda {storageAmountInGB} GB alıyorlar", "freeStorageOnReferralSuccess": "Birisinin davet kodunuzu uygulayıp ücretli hesap açtığı her seferede {storageAmountInGB} GB", + "shareTextReferralCode": "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", "claimFreeStorage": "Bedava alan talep edin", "inviteYourFriends": "Arkadaşlarını davet et", "failedToFetchReferralDetails": "Davet ayrıntıları çekilemedi. Iütfen daha sonra deneyin.", @@ -317,6 +331,7 @@ "removingFromFavorites": "Favorilerimden kaldır...", "sorryCouldNotAddToFavorites": "Üzgünüm, favorilere ekleyemedim!", "sorryCouldNotRemoveFromFavorites": "Üzgünüm, favorilere ekleyemedim!", + "subscribeToEnableSharing": "Paylaşımı etkinleştirmek için aktif bir ücretli aboneliğe ihtiyacınız var.", "subscribe": "Abone ol", "canOnlyRemoveFilesOwnedByYou": "Yalnızca size ait dosyaları kaldırabilir", "deleteSharedAlbum": "Paylaşılan albüm silinsin mi?", @@ -329,6 +344,7 @@ "removeParticipantBody": "{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", "keepPhotos": "Fotoğrafları sakla", "deletePhotos": "Fotoğrafları sil", + "inviteToEnte": "Ente'ye davet edin", "removePublicLink": "Herkese açık link oluştur", "disableLinkMessage": "Bu, \"{albumName}\"e erişim için olan genel bağlantıyı kaldıracaktır.", "sharing": "Paylaşılıyor...", @@ -339,12 +355,16 @@ "failedToLoadAlbums": "Albüm yüklenirken hata oluştu", "hidden": "Gizle", "authToViewYourHiddenFiles": "Gizli dosyalarınızı görüntülemek için kimlik doğrulama yapınız", + "authToViewTrashedFiles": "Çöp dosyalarınızı görüntülemek için lütfen kimlik doğrulaması yapın", "trash": "Cöp kutusu", "uncategorized": "Kategorisiz", "videoSmallCase": "video", "photoSmallCase": "fotoğraf", "singleFileDeleteHighlight": "Tüm albümlerden silinecek.", + "singleFileInBothLocalAndRemote": "{fileType} Ente ve cihazınızdan silinecektir.", + "singleFileInRemoteOnly": "{fileType} Ente'den silinecektir.", "singleFileDeleteFromDevice": "Bu {fileType}, cihazınızdan silinecek.", + "deleteFromEnte": "Ente'den Sil", "yesDelete": "Evet, sil", "movedToTrash": "Cöp kutusuna taşı", "deleteFromDevice": "Cihazınızdan silin", @@ -397,8 +417,35 @@ "description": "The text to display in the advanced settings section" }, "photoGridSize": "Fotoğraf ızgara boyutu", + "manageDeviceStorage": "Cihaz önbelliğini yönet", + "manageDeviceStorageDesc": "Yerel önbellek depolama alanını gözden geçirin ve temizleyin.", "machineLearning": "Makine öğrenimi", + "mlConsent": "Makine öğrenimini etkinleştir", + "mlConsentTitle": "Makine öğrenimi etkinleştirilsin mi?", + "mlConsentDescription": "Makine öğrenimini etkinleştirirseniz, Ente sizinle paylaşılanlar da dahil olmak üzere dosyalardan yüz geometrisi gibi bilgileri çıkarır.\n\nBu, cihazınızda gerçekleşecek ve oluşturulan tüm biyometrik bilgiler uçtan uca şifrelenecektir.", + "mlConsentPrivacy": "Gizlilik politikamızdaki bu özellik hakkında daha fazla ayrıntı için lütfen buraya tıklayın", + "mlConsentConfirmation": "Anladım, ve makine öğrenimini etkinleştirmek istiyorum", "magicSearch": "Sihirli arama", + "discover": "Keşfet", + "@discover": { + "description": "The text to display for the discover section under which we show receipts, screenshots, sunsets, greenery, etc." + }, + "discover_identity": "Kimlik", + "discover_screenshots": "Ekran Görüntüleri", + "discover_receipts": "Makbuzlar", + "discover_notes": "Notlar", + "discover_memes": "Mimler", + "discover_visiting_cards": "Ziyaret Kartları", + "discover_babies": "Bebekler", + "discover_pets": "Evcil Hayvanlar", + "discover_selfies": "Özçekimler", + "discover_wallpapers": "Duvar Kağıtları", + "discover_food": "Yiyecek", + "discover_celebrations": "Kutlamalar ", + "discover_sunset": "Gün batımı", + "discover_hills": "Tepeler", + "discover_greenery": "Yeşillik", + "mlIndexingDescription": "Tüm öğeler dizine eklenene kadar makine öğreniminin daha yüksek bant genişliği ve pil kullanımı ile sonuçlanacağını lütfen unutmayın. Daha hızlı indeksleme için masaüstü uygulamasını kullanmayı düşünün, tüm sonuçlar otomatik olarak senkronize edilecektir.", "loadingModel": "Modeller indiriliyor...", "waitingForWifi": "WiFi bekleniyor...", "status": "Durum", @@ -431,14 +478,18 @@ "showMemories": "Anıları göster", "yearsAgo": "{count, plural, one{{count} yıl önce} other{{count} yıl önce}}", "backupSettings": "Yedekleme seçenekleri", + "backupStatus": "Yedekleme durumu", + "backupStatusDescription": "Eklenen öğeler burada görünecek", "backupOverMobileData": "Mobil veri ile yedekle", "backupVideos": "Videolari yedekle", "disableAutoLock": "Otomatik kilidi devre dışı bırak", + "deviceLockExplanation": "Ente uygulaması önplanda calıştığında ve bir yedekleme işlemi devam ettiğinde, cihaz ekran kilidini devre dışı bırakın. Bu genellikle gerekli olmasa da, büyük dosyaların yüklenmesi ve büyük kütüphanelerin başlangıçta içe aktarılması sürecini hızlandırabilir.", "about": "Hakkında", "weAreOpenSource": "Biz açık kaynağız!", "privacy": "Gizlilik", "terms": "Şartlar", "checkForUpdates": "Güncellemeleri kontol et", + "checkStatus": "Durumu kontrol et", "checking": "Kontrol ediliyor...", "youAreOnTheLatestVersion": "En son sürüme sahipsiniz", "account": "Hesap", @@ -453,6 +504,7 @@ "authToInitiateAccountDeletion": "Hesap silme işlemini başlatmak için lütfen kimlik doğrulaması yapın", "areYouSureYouWantToLogout": "Çıkış yapmak istediğinize emin misiniz?", "yesLogout": "Evet, oturumu kapat", + "aNewVersionOfEnteIsAvailable": "Ente için yeni bir sürüm mevcut.", "update": "Güncelle", "installManually": "Manuel kurulum", "criticalUpdateAvailable": "Kritik güncelleme mevcut", @@ -465,11 +517,14 @@ "backedUpFolders": "Yedeklenmiş klasörler", "backup": "Yedekle", "freeUpDeviceSpace": "Cihaz alanını boşaltın", + "freeUpDeviceSpaceDesc": "Zaten yedeklenmiş dosyaları temizleyerek cihazınızda yer kazanın.", "allClear": "✨ Tamamen temizle", "noDeviceThatCanBeDeleted": "Bu cihazda silinebilecek hiçbir dosyanız yok", "removeDuplicates": "Yinelenenleri kaldır", + "removeDuplicatesDesc": "Tam olarak yinelenen dosyaları gözden geçirin ve kaldırın.", + "viewLargeFiles": "Büyük dosyalar", + "viewLargeFilesDesc": "En fazla depolama alanı tüketen dosyaları görüntüleyin.", "noDuplicates": "Yinelenenleri kaldır", - "youveNoDuplicateFilesThatCanBeCleared": "Temizlenebilecek yinelenen dosyalarınız yok", "success": "Başarılı", "rateUs": "Bizi değerlendirin", "remindToEmptyDeviceTrash": "Ayrıca boşalan alanı talep etmek için \"Ayarlar\" -> \"Depolama\" bölümünden \"Son Silinenler \"i boşaltın", @@ -540,6 +595,7 @@ "systemTheme": "Sistem", "freeTrial": "Ücretsiz deneme", "selectYourPlan": "Planınızı seçin", + "enteSubscriptionPitch": "Ente anılarınızı korur, böylece cihazınızı kaybetseniz bile anılarınıza her zaman ulaşabilirsiniz.", "enteSubscriptionShareWithFamily": "Aileniz de planınıza eklenebilir.", "currentUsageIs": "Güncel kullanımınız ", "@currentUsageIs": { @@ -554,6 +610,7 @@ "freeTrialValidTill": "Ücretsiz deneme {endDate} sona erir", "validTill": "{endDate} tarihine kadar geçerli", "addOnValidTill": "{storageAmount} eklentiniz {endDate} tarihine kadar geçerlidir", + "playStoreFreeTrialValidTill": "Ücretsiz deneme süresi {endDate} tarihine kadar geçerlidir.\nDaha sonra ücretli bir plan seçebilirsiniz.", "subWillBeCancelledOn": "Aboneliğiniz {endDate} tarihinde iptal edilecektir", "subscription": "Abonelik", "paymentDetails": "Ödeme detayları", @@ -604,6 +661,7 @@ "appleId": "Apple kimliği", "playstoreSubscription": "PlayStore aboneliği", "appstoreSubscription": "PlayStore aboneliği", + "subAlreadyLinkedErrMessage": "{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''", "visitWebToManage": "Aboneliğinizi yönetmek için lütfen web.ente.io adresini ziyaret edin", "couldNotUpdateSubscription": "Abonelikler kaydedilemedi", "pleaseContactSupportAndWeWillBeHappyToHelp": "Lütfen support@ente.io ile iletişime geçin; size yardımcı olmaktan memnuniyet duyarız!", @@ -635,6 +693,8 @@ "noPhotosAreBeingBackedUpRightNow": "Şu anda hiçbir fotoğraf yedeklenmiyor", "preserveMore": "Daha fazlasını koruyun", "grantFullAccessPrompt": "Lütfen Ayarlar uygulamasında tüm fotoğraflara erişime izin verin", + "allowPermTitle": "Fotoğraflara erişime izin verin", + "allowPermBody": "Ente'nin kitaplığınızı görüntüleyebilmesi ve yedekleyebilmesi için lütfen Ayarlar'dan fotoğraflarınıza erişime izin verin.", "openSettings": "Ayarları Açın", "selectMorePhotos": "Daha Fazla Fotoğraf Seç", "existingUser": "Mevcut kullanıcı", @@ -648,7 +708,9 @@ "everywhere": "her yerde", "androidIosWebDesktop": "Android, iOS, Web, Masaüstü", "mobileWebDesktop": "Mobil, Web, Masaüstü", + "newToEnte": "Ente'de yeniyim", "pleaseLoginAgain": "Lütfen tekrar giriş yapın", + "autoLogoutMessage": "Teknik aksaklık nedeniyle oturumunuz kapatıldı. Verdiğimiz rahatsızlıktan dolayı özür dileriz.", "yourSubscriptionHasExpired": "Aboneliğinizin süresi doldu", "storageLimitExceeded": "Depolama sınırı aşıldı", "upgrade": "Yükselt", @@ -659,10 +721,12 @@ }, "backupFailed": "Yedekleme başarısız oldu", "couldNotBackUpTryLater": "Verilerinizi yedekleyemedik.\nDaha sonra tekrar deneyeceğiz.", + "enteCanEncryptAndPreserveFilesOnlyIfYouGrant": "Ente dosyaları yalnızca erişim izni verdiğiniz takdirde şifreleyebilir ve koruyabilir", "pleaseGrantPermissions": "Lütfen izin ver", "grantPermission": "İzinleri değiştir", "privateSharing": "Özel paylaşım", "shareOnlyWithThePeopleYouWant": "Yalnızca istediğiniz kişilerle paylaşın", + "usePublicLinksForPeopleNotOnEnte": "Ente'de olmayan kişiler için genel bağlantıları kullanın", "allowPeopleToAddPhotos": "Kullanıcıların fotoğraf eklemesine izin ver", "shareAnAlbumNow": "Şimdi bir albüm paylaşın", "collectEventPhotos": "Etkinlik fotoğraflarını topla", @@ -714,11 +778,13 @@ "unhide": "Gizleme", "unarchive": "Arşivden cıkar", "favorite": "Favori", + "removeFromFavorite": "Favorilerden Kaldır", "shareLink": "Linki paylaş", "createCollage": "Kolaj oluştur", "saveCollage": "Kolajı kaydet", "collageSaved": "Kolajınız galeriye kaydedildi", "collageLayout": "Düzen", + "addToEnte": "Ente'ye ekle", "addToAlbum": "Albüme ekle", "delete": "Sil", "hide": "Gizle", @@ -757,6 +823,16 @@ "referFriendsAnd2xYourPlan": "Arkadaşlarınıza önerin ve planınızı 2 katına çıkarın", "shareAlbumHint": "Bir albüm açın ve paylaşmak için sağ üstteki paylaş düğmesine dokunun.", "itemsShowTheNumberOfDaysRemainingBeforePermanentDeletion": "Öğeler, kalıcı olarak silinmeden önce kalan gün sayısını gösterir", + "trashDaysLeft": "{count, plural, =0 {yakında} =1{1 gün} other {{count} gün}}", + "@trashDaysLeft": { + "description": "Text to indicate number of days remaining before permanent deletion", + "placeholders": { + "count": { + "example": "1|2|3", + "type": "int" + } + } + }, "deleteAll": "Hepsini Sil", "renameAlbum": "Albümü yeniden adlandır", "convertToAlbum": "Albüme taşı", @@ -773,7 +849,10 @@ "photosAddedByYouWillBeRemovedFromTheAlbum": "Eklediğiniz fotoğraflar albümden kaldırılacak", "youveNoFilesInThisAlbumThatCanBeDeleted": "Bu cihazda silinebilecek hiçbir dosyanız yok", "youDontHaveAnyArchivedItems": "Arşivlenmiş öğeniz yok.", + "ignoredFolderUploadReason": "Bu albümdeki bazı dosyalar daha önce ente'den silindiğinden yükleme işleminde göz ardı edildi.", "resetIgnoredFiles": "Yok sayılan dosyaları sıfırla", + "deviceFilesAutoUploading": "Bu cihazın albümüne eklenen dosyalar otomatik olarak ente'ye yüklenecektir.", + "turnOnBackupForAutoUpload": "Bu cihaz klasörüne eklenen dosyaları otomatik olarak ente'ye yüklemek için yedeklemeyi açın.", "noHiddenPhotosOrVideos": "Gizli fotoğraf veya video yok", "toHideAPhotoOrVideo": "Bir fotoğrafı veya videoyu gizlemek için", "openTheItem": "• Öğeyi açın", @@ -799,6 +878,7 @@ "close": "Kapat", "setAs": "Şu şekilde ayarla", "fileSavedToGallery": "Video galeriye kaydedildi", + "filesSavedToGallery": "Dosyalar galeriye kaydedildi", "fileFailedToSaveToGallery": "Dosya galeriye kaydedilemedi", "download": "İndir", "pressAndHoldToPlayVideo": "Videoları yönetmek için basılı tutun", @@ -850,6 +930,15 @@ "@freeUpSpaceSaving": { "description": "Text to tell user how much space they can free up by deleting items from the device" }, + "freeUpAccessPostDelete": "Aktif bir aboneliğiniz olduğu sürece ente'de {count, plural, one {it} other {them}} erişimine devam edebilirsiniz", + "@freeUpAccessPostDelete": { + "placeholders": { + "count": { + "example": "1", + "type": "int" + } + } + }, "freeUpAmount": "{sizeInMBorGB} yer açın", "thisEmailIsAlreadyInUse": "Bu e-posta zaten kullanılıyor", "incorrectCode": "Yanlış kod", @@ -874,6 +963,16 @@ "encryptingBackup": "Yedekleme şifreleniyor...", "syncStopped": "Senkronizasyon durduruldu", "syncProgress": "{completed}/{total} anı korundu", + "uploadingMultipleMemories": "{count} anı korunuyor...", + "@uploadingMultipleMemories": { + "description": "Text to tell user how many memories are being preserved", + "placeholders": { + "count": { + "type": "String" + } + } + }, + "uploadingSingleMemory": "1 anı korunuyor...", "@syncProgress": { "description": "Text to tell user how many memories have been preserved", "placeholders": { @@ -892,6 +991,7 @@ "renameFile": "Dosyayı yeniden adlandır", "enterFileName": "Dosya adını girin", "filesDeleted": "Dosyalar silinmiş", + "selectedFilesAreNotOnEnte": "Seçilen dosyalar Ente'de değil", "thisActionCannotBeUndone": "Bu eylem geri alınamaz", "emptyTrash": "Çöp kutusu boşaltılsın mı?", "permDeleteWarning": "Çöp kutusundaki tüm öğeler kalıcı olarak silinecek\n\nBu işlem geri alınamaz", @@ -900,6 +1000,7 @@ "permanentlyDeleteFromDevice": "Cihazdan kalıcı olarak silinsin mi?", "someOfTheFilesYouAreTryingToDeleteAre": "Silmeye çalıştığınız dosyalardan bazıları yalnızca cihazınızda mevcuttur ve silindiği takdirde kurtarılamaz", "theyWillBeDeletedFromAllAlbums": "Tüm albümlerden silinecek.", + "someItemsAreInBothEnteAndYourDevice": "Bazı öğeler hem Ente'de hem de cihazınızda bulunur.", "selectedItemsWillBeDeletedFromAllAlbumsAndMoved": "Seçilen öğeler tüm albümlerden silinecek ve çöp kutusuna taşınacak.", "theseItemsWillBeDeletedFromYourDevice": "Bu öğeler cihazınızdan silinecektir.", "itLooksLikeSomethingWentWrongPleaseRetryAfterSome": "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.", @@ -939,6 +1040,7 @@ "fileTypesAndNames": "Dosya türleri ve adları", "location": "Konum", "moments": "Anlar", + "searchFaceEmptySection": "İndeksleme yapıldıktan sonra insanlar burada gösterilecek", "searchDatesEmptySection": "Tarihe, aya veya yıla göre arama yapın", "searchLocationEmptySection": "Bir fotoğrafın belli bir yarıçapında çekilen fotoğrafları gruplandırın", "searchPeopleEmptySection": "İnsanları davet ettiğinizde onların paylaştığı tüm fotoğrafları burada göreceksiniz", @@ -993,6 +1095,7 @@ "@storageUsageInfo": { "description": "Example: 1.2 GB of 2 GB used or 100 GB or 2TB used" }, + "availableStorageSpace": "{freeAmount} {storageUnit} ücretsiz", "appVersion": "Sürüm: {versionValue}", "verifyIDLabel": "Doğrula", "fileInfoAddDescHint": "Bir açıklama ekle...", @@ -1003,6 +1106,7 @@ }, "setRadius": "Yarıçapı ayarla", "familyPlanPortalTitle": "Aile", + "familyPlanOverview": "Ekstra ödeme yapmadan mevcut planınıza 5 aile üyesi ekleyin.\n\nHer üyenin kendine ait özel alanı vardır ve paylaşılmadıkça birbirlerinin dosyalarını göremezler.\n\nAile planları ücretli ente aboneliğine sahip müşteriler tarafından kullanılabilir.\n\nBaşlamak için şimdi abone olun!", "androidBiometricHint": "Kimliği doğrula", "@androidBiometricHint": { "description": "Hint message advising the user how to authenticate with biometrics. It is used on Android side. Maximum 60 characters." @@ -1080,15 +1184,18 @@ "noAlbumsSharedByYouYet": "Henüz paylaştığınız albüm yok", "sharedWithYou": "Sizinle paylaşıldı", "sharedByYou": "Paylaştıklarınız", + "inviteYourFriendsToEnte": "Katılmaları için arkadaşlarınızı davet edin", "failedToDownloadVideo": "Video indirilemedi", "hiding": "Gizleniyor...", "unhiding": "Gösteriliyor...", "successfullyHid": "Başarıyla saklandı", "successfullyUnhid": "Başarıyla arşivden çıkarıldı", "crashReporting": "Çökme raporlaması", + "resumableUploads": "Devam edilebilir yüklemeler", "addToHiddenAlbum": "Gizli albüme ekle", "moveToHiddenAlbum": "Gizli albüme ekle", "fileTypes": "Dosya türü", + "deleteConfirmDialogBody": "Kullandığınız Ente uygulamaları varsa bu hesap diğer Ente uygulamalarıyla bağlantılıdır. Tüm Ente uygulamalarına yüklediğiniz veriler ve hesabınız kalıcı olarak silinecektir.", "hearUsWhereTitle": "Ente'yi nereden duydunuz? (opsiyonel)", "hearUsExplanation": "Biz uygulama kurulumlarını takip etmiyoruz. Bizi nereden duyduğunuzdan bahsetmeniz bize çok yardımcı olacak!", "viewAddOnButton": "Eklentileri görüntüle", @@ -1118,6 +1225,7 @@ } }, "faces": "Yüzler", + "people": "Kişiler", "contents": "İçerikler", "addNew": "Yeni ekle", "@addNew": { @@ -1140,6 +1248,10 @@ "waitingForVerification": "Doğrulama bekleniyor...", "passkey": "Parola Anahtarı", "passkeyAuthTitle": "Geçiş anahtarı doğrulaması", + "loginWithTOTP": "TOTP ile giriş yap", + "passKeyPendingVerification": "Doğrulama hala bekliyor", + "loginSessionExpired": "Oturum süresi doldu", + "loginSessionExpiredDetails": "Oturum süreniz doldu. Tekrar giriş yapın.", "verifyPasskey": "Şifrenizi doğrulayın", "playOnTv": "Albümü TV'de oynat", "pair": "Eşleştir", @@ -1148,6 +1260,12 @@ "deviceCodeHint": "Kodu girin", "joinDiscord": "Discord'a Katıl", "locations": "Konum", + "addAName": "Bir Ad Ekle", + "findThemQuickly": "Onları çabucak bulun", + "@findThemQuickly": { + "description": "Subtitle to indicate that the user can find people quickly by name" + }, + "findPeopleByName": "Kişileri isimlere göre çabucak bulun", "addViewers": "{count, plural, zero {Görüntüleyen ekle} one {Görüntüleyen ekle} other {Görüntüleyen ekle}}", "addCollaborators": "{count, plural, zero {Ortak çalışan ekle} one {Ortak çalışan ekle} other {Ortak çalışan ekle}}", "longPressAnEmailToVerifyEndToEndEncryption": "Uçtan uca şifrelemeyi doğrulamak için bir e-postaya uzun basın.", @@ -1158,5 +1276,402 @@ "invalidEndpointMessage": "Üzgünüz, girdiğiniz uç nokta geçersiz. Lütfen geçerli bir uç nokta girin ve tekrar deneyin.", "endpointUpdatedMessage": "Fatura başarıyla güncellendi", "customEndpoint": "{endpoint}'e bağlanıldı", - "orPickFromYourContacts": "or pick from your contacts" + "createCollaborativeLink": "Ortak bağlantı oluşturun", + "search": "Ara", + "enterPersonName": "Kişi ismini giriniz", + "enterName": "İsim girin", + "savePerson": "Kişiyi Kaydet", + "editPerson": "Kişiyi Düzenle", + "mergedPhotos": "Birleştirilmiş fotoğraflar", + "orMergeWithExistingPerson": "Ya da mevcut olan ile birleştirin", + "enterDateOfBirth": "Doğum Günü (isteğe bağlı)", + "birthday": "Doğum Günü", + "removePersonLabel": "Kişi etiketini kaldırın", + "autoPairDesc": "Otomatik eşleştirme yalnızca Chromecast destekleyen cihazlarla çalışır.", + "manualPairDesc": "PIN ile eşleştirme, albümünüzü görüntülemek istediğiniz herhangi bir ekranla çalışır.", + "connectToDevice": "Cihaza bağlanın", + "autoCastDialogBody": "Mevcut Cast cihazlarını burada görebilirsiniz.", + "autoCastiOSPermission": "Ayarlar'da Ente Photos uygulaması için Yerel Ağ izinlerinin açık olduğundan emin olun.", + "noDeviceFound": "Aygıt bulunamadı", + "stopCastingTitle": "Yayını durdur", + "stopCastingBody": "Yansıtmayı durdurmak istiyor musunuz?", + "castIPMismatchTitle": "Albüm yüklenirken hata oluştu", + "castIPMismatchBody": "Lütfen TV ile aynı ağda olduğunuzdan emin olun.", + "pairingComplete": "Eşleştirme tamamlandı", + "savingEdits": "Düzenlemeler kaydediliyor...", + "autoPair": "Otomatik eşle", + "pairWithPin": "PIN ile eşleştirin", + "faceRecognition": "Yüz Tanıma", + "foundFaces": "Yüzler bulundu", + "clusteringProgress": "Kümeleme ilerlemesi", + "indexingIsPaused": "İndeksleme duraklatılmıştır. Cihaz hazır olduğunda otomatik olarak devam edecektir.", + "trim": "Kes", + "crop": "Kırp", + "rotate": "Döndür", + "left": "Sol", + "right": "Sağ", + "whatsNew": "Yenilikler", + "reviewSuggestions": "Önerileri inceleyin", + "review": "Gözden Geçir", + "useAsCover": "Kapak olarak kullanın", + "notPersonLabel": "{name} değil mi?", + "@notPersonLabel": { + "description": "Label to indicate that the person in the photo is not the person whose name is mentioned", + "placeholders": { + "name": { + "content": "{name}", + "type": "String" + } + } + }, + "enable": "Etkinleştir", + "enabled": "Etkin", + "moreDetails": "Daha fazla detay", + "enableMLIndexingDesc": "Ente, yüz tanıma, sihirli arama ve diğer gelişmiş arama özellikleri için cihaz üzerinde makine öğrenimini destekler", + "magicSearchHint": "Sihirli arama, fotoğrafları içeriklerine göre aramanıza olanak tanır, örneğin 'çiçek', 'kırmızı araba', 'kimlik belgeleri'", + "panorama": "Panorama", + "reenterPassword": "Şifrenizi tekrar girin", + "reenterPin": "PIN'inizi tekrar girin", + "deviceLock": "Cihaz kilidi", + "pinLock": "Pin kilidi", + "next": "Sonraki", + "setNewPassword": "Yeni şifre belirle", + "enterPin": "PIN Girin", + "setNewPin": "Yeni PIN belirleyin", + "appLock": "Uygulama kilidi", + "noSystemLockFound": "Sistem kilidi bulunamadı", + "tapToUnlock": "Açmak için dokun", + "tooManyIncorrectAttempts": "Çok fazla hatalı deneme", + "videoInfo": "Video Bilgileri", + "autoLock": "Otomatik Kilit", + "immediately": "Hemen", + "autoLockFeatureDescription": "Uygulamayı arka plana attıktan sonra kilitlendiği süre", + "hideContent": "İçeriği gizle", + "hideContentDescriptionAndroid": "Uygulama değiştiricide bulunan uygulama içeriğini gizler ve ekran görüntülerini devre dışı bırakır", + "hideContentDescriptionIos": "Uygulama değiştiricideki uygulama içeriğini gizler", + "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", + "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.", + "nameTheAlbum": "Albüm İsmi", + "collectPhotosDescription": "Arkadaşlarınızın orijinal kalitede fotoğraf yükleyebileceği bir bağlantı oluşturun.", + "collect": "Topla", + "appLockDescriptions": "Cihazınızın varsayılan kilit ekranı ile PIN veya parola içeren özel bir kilit ekranı arasında seçim yapın.", + "toEnableAppLockPleaseSetupDevicePasscodeOrScreen": "Uygulama kilidini etkinleştirmek için lütfen sistem ayarlarınızda cihaz şifresi veya ekran kilidi ayarlayın.", + "authToViewPasskey": "Geçiş anahtarınızı görüntülemek için lütfen kimlik doğrulaması yapın", + "loopVideoOn": "Video Döngüsü Açık", + "loopVideoOff": "Video Döngüsü Kapalı", + "localSyncErrorMessage": "Yerel fotoğraf senkronizasyonu beklenenden daha uzun sürdüğü için bir şeyler ters gitmiş gibi görünüyor. Lütfen destek ekibimize ulaşın", + "showPerson": "Kişiyi Göster", + "sort": "Sırala", + "mostRecent": "En son", + "mostRelevant": "En alakalı", + "loadingYourPhotos": "Fotoğraflarınız yükleniyor...", + "processingImport": "İşleniyor {folderName}...", + "personName": "Kişi Adı", + "addNewPerson": "Yeni kişi ekle", + "addNameOrMerge": "İsim ekleyin veya birleştirin", + "mergeWithExisting": "Var olan ile birleştir.", + "newPerson": "Yeni Kişi", + "addName": "İsim Ekle", + "add": "Ekle", + "extraPhotosFoundFor": "{text} için ekstra fotoğraflar bulundu", + "@extraPhotosFoundFor": { + "placeholders": { + "text": { + "type": "String" + } + } + }, + "extraPhotosFound": "Ekstra fotoğraflar bulundu", + "configuration": "Yapılandırma", + "localIndexing": "Yerel indeksleme", + "processed": "İşlenen", + "resetPerson": "Kaldır", + "areYouSureYouWantToResetThisPerson": "Bu kişiyi sıfırlamak istediğinden emin misin?", + "allPersonGroupingWillReset": "Bu kişi için tüm gruplamalar sıfırlanacak ve bu kişi için yaptığınız tüm önerileri kaybedeceksiniz", + "yesResetPerson": "Evet, kişiyi sıfırla", + "onlyThem": "Sadece onlar", + "checkingModels": "Modelleri kontrol ediyorum...", + "enableMachineLearningBanner": "Sihirli arama ve yüz tanıma için makine öğrenimini etkinleştirin", + "searchDiscoverEmptySection": "İşleme ve senkronizasyon tamamlandığında görüntüler burada gösterilecektir", + "searchPersonsEmptySection": "İşleme ve senkronizasyon tamamlandığında kişiler burada gösterilecektir", + "viewersSuccessfullyAdded": "{count, plural, =0 {0 Görüntüleyen eklendi} =1 {1 Görüntüleyen eklendi} other {{count} Görüntüleyen eklendi}}", + "@viewersSuccessfullyAdded": { + "placeholders": { + "count": { + "type": "int", + "example": "2" + } + }, + "description": "Number of viewers that were successfully added to an album." + }, + "collaboratorsSuccessfullyAdded": "{count, plural, =0 {0 işbirlikçi eklendi} =1 {1 işbirlikçi eklendi} other {{count} işbirlikçi eklendi}}", + "@collaboratorsSuccessfullyAdded": { + "placeholders": { + "count": { + "type": "int", + "example": "2" + } + }, + "description": "Number of collaborators that were successfully added to an album." + }, + "accountIsAlreadyConfigured": "Hesap zaten yapılandırılmıştır.", + "sessionIdMismatch": "Oturum kimliği uyuşmazlığı", + "@sessionIdMismatch": { + "description": "In passkey page, deeplink is ignored because of session ID mismatch." + }, + "failedToFetchActiveSessions": "Etkin oturumlar getirilemedi", + "@failedToFetchActiveSessions": { + "description": "In session page, warn user (in toast) that active sessions could not be fetched." + }, + "failedToRefreshStripeSubscription": "Abonelik yenilenemedi", + "failedToPlayVideo": "Video oynatılamadı", + "uploadIsIgnoredDueToIgnorereason": "Yükleme {ignoreReason} nedeniyle yok sayıldı", + "@uploadIsIgnoredDueToIgnorereason": { + "placeholders": { + "ignoreReason": { + "type": "String", + "example": "no network" + } + } + }, + "typeOfGallerGallerytypeIsNotSupportedForRename": "Galeri türü {galleryType} yeniden adlandırma için desteklenmiyor", + "@typeOfGallerGallerytypeIsNotSupportedForRename": { + "placeholders": { + "galleryType": { + "type": "String", + "example": "no network" + } + } + }, + "tapToUploadIsIgnoredDue": "Yüklemek için dokunun, yükleme şu anda {ignoreReason} nedeniyle yok sayılıyor", + "@tapToUploadIsIgnoredDue": { + "description": "Shown in upload icon widet, inside a tooltip.", + "placeholders": { + "ignoreReason": { + "type": "String", + "example": "no network" + } + } + }, + "tapToUpload": "Yüklemek için tıklayın", + "@tapToUpload": { + "description": "Shown in upload icon widet, inside a tooltip." + }, + "info": "Bilgi", + "addFiles": "Dosyaları Ekle", + "castAlbum": "Yayın albümü", + "imageNotAnalyzed": "Görüntü analiz edilmedi", + "noFacesFound": "Yüz bulunamadı", + "fileNotUploadedYet": "Dosya henüz yüklenmedi", + "noSuggestionsForPerson": "{personName} için öneri yok", + "@noSuggestionsForPerson": { + "placeholders": { + "personName": { + "type": "String", + "example": "Alice" + } + } + }, + "month": "ay", + "yearShort": "yıl", + "@yearShort": { + "description": "Appears in pricing page (/yr)" + }, + "currentlyRunning": "şu anda çalışıyor", + "ignored": "yoksayıldı", + "photosCount": "{count, plural, =0 {0 fotoğraf} =1 {1 fotoğraf} other {{count} fotoğraflar}}", + "@photosCount": { + "placeholders": { + "count": { + "type": "int", + "example": "2" + } + } + }, + "file": "Dosya", + "searchSectionsLengthMismatch": "Bölüm uzunluğu uyuşmazlığı: {snapshotLength} != {searchLength}", + "@searchSectionsLengthMismatch": { + "description": "Appears in search tab page", + "placeholders": { + "snapshotLength": { + "type": "int", + "example": "1" + }, + "searchLength": { + "type": "int", + "example": "2" + } + } + }, + "selectMailApp": "Mail Uygulamasını Seç", + "selectAllShort": "Tümü", + "@selectAllShort": { + "description": "Text that appears in bottom right when you start to select multiple photos. When clicked, it selects all photos." + }, + "changeLogMagicSearchImprovementTitle": "Sihirli Arama İyileştirme", + "changeLogMagicSearchImprovementContent": "Sihirli aramayı çok daha hızlı olacak şekilde geliştirdik, böylece aradığınızı bulmak için beklemek zorunda kalmazsınız.", + "changeLogBackupStatusTitle": "Yedekleme Durumu", + "changeLogBackupStatusContent": "Ente'ye yüklenen tüm dosyaların, hatalar ve sıraya alınanlar da dahil olmak üzere bir günlüğünü ekledik.", + "changeLogDiscoverTitle": "Keşfet", + "changeLogDiscoverContent": "Kimlik kartlarınızın, notlarınızın ve hatta memlerinizin fotoğraflarını mı arıyorsunuz? Arama sekmesine gidin ve Keşfet'e göz atın. Anlamsal aramamıza dayanarak, sizin için önemli olabilecek fotoğrafları bulabileceğiniz bir yerdir.\\n\\n Sadece Makine Öğrenimini etkinleştirdiyseniz kullanılabilir.", + "selectCoverPhoto": "Kapak fotoğrafı seçin", + "newLocation": "Yeni konum", + "faceNotClusteredYet": "Yüz henüz kümelenmedi, lütfen daha sonra tekrar gelin", + "theLinkYouAreTryingToAccessHasExpired": "Erişmeye çalıştığınız bağlantının süresi dolmuştur.", + "openFile": "Dosyayı aç", + "backupFile": "Yedek Dosyası", + "openAlbumInBrowser": "Albümü tarayıcıda aç", + "openAlbumInBrowserTitle": "Bu albüme fotoğraf eklemek için lütfen web uygulamasını kullanın", + "allow": "İzin ver", + "allowAppToOpenSharedAlbumLinks": "Uygulamanın paylaşılan albüm bağlantılarını açmasına izin ver", + "seePublicAlbumLinksInApp": "Uygulamadaki herkese açık albüm bağlantılarını görün", + "emergencyContacts": "Acil Durum İletişim Bilgileri", + "acceptTrustInvite": "Daveti Kabul Et", + "declineTrustInvite": "Daveti Reddet", + "removeYourselfAsTrustedContact": "Kendinizi güvenilir kişi olarak kaldırın", + "legacy": "Geleneksel", + "legacyPageDesc": "Geleneksel yol, güvendiğiniz kişilerin yokluğunuzda hesabınıza erişmesine olanak tanır.", + "legacyPageDesc2": "Güvenilir kişiler hesap kurtarma işlemini başlatabilir ve 30 gün içinde engellenmezse şifrenizi sıfırlayabilir ve hesabınıza erişebilir.", + "legacyAccounts": "Geleneksel hesaplar", + "trustedContacts": "Güvenilir kişiler", + "addTrustedContact": "Güvenilir kişi ekle", + "removeInvite": "Davetiyeyi kaldır", + "recoveryWarning": "Güvenilir bir kişi hesabınıza erişmeye çalışıyor", + "rejectRecovery": "Kurtarmayı reddet", + "recoveryInitiated": "Kurtarma başlatıldı", + "recoveryInitiatedDesc": "Hesabınıza {days} gün sonra erişebilirsiniz. {email} adresine bir bildirim gönderilecektir.", + "@recoveryInitiatedDesc": { + "placeholders": { + "days": { + "type": "int", + "example": "30" + }, + "email": { + "type": "String", + "example": "me@example.com" + } + } + }, + "cancelAccountRecovery": "Kurtarma işlemini iptal et", + "recoveryAccount": "Hesabı kurtar", + "cancelAccountRecoveryBody": "Kurtarmayı iptal etmek istediğinize emin misiniz?", + "startAccountRecoveryTitle": "Kurtarmayı başlat", + "whyAddTrustContact": ".", + "recoveryReady": "Artık yeni bir parola belirleyerek {email} hesabını kurtarabilirsiniz.", + "@recoveryReady": { + "placeholders": { + "email": { + "type": "String", + "example": "me@example.com" + } + } + }, + "recoveryWarningBody": "{email} hesabınızı kurtarmaya çalışıyor.", + "trustedInviteBody": "{email} ile eski bir irtibat kişisi olmaya davet edildiniz.", + "warning": "Uyarı", + "proceed": "Devam edin", + "confirmAddingTrustedContact": "Güvenilir bir kişi olarak {email} eklemek üzeresiniz. Eğer {numOfDays} gün boyunca yoksanız hesabınızı kurtarabilecekler.", + "@confirmAddingTrustedContact": { + "placeholders": { + "email": { + "type": "String", + "example": "me@example.com" + }, + "numOfDays": { + "type": "int", + "example": "30" + } + } + }, + "legacyInvite": "{email} sizi güvenilir bir kişi olmaya davet etti", + "authToManageLegacy": "Güvenilir kişilerinizi yönetmek için lütfen kimlik doğrulaması yapın", + "useDifferentPlayerInfo": "Bu videoyu oynatmakta sorun mu yaşıyorsunuz? Farklı bir oynatıcı denemek için buraya uzun basın.", + "hideSharedItemsFromHomeGallery": "Paylaşılan öğeleri ana galeriden gizle", + "gallery": "Galeri", + "joinAlbum": "Albüme Katılın", + "joinAlbumSubtext": "fotoğraflarınızı görüntülemek ve eklemek için", + "joinAlbumSubtextViewer": "bunu paylaşılan albümlere eklemek için", + "join": "Katıl", + "linkEmail": "E-posta bağlantısı", + "link": "Bağlantı", + "noEnteAccountExclamation": "Ente hesabı yok!", + "orPickFromYourContacts": "veya kişilerinizden birini seçin", + "emailDoesNotHaveEnteAccount": "{email} bir Ente hesabına sahip değil", + "@emailDoesNotHaveEnteAccount": { + "description": "Shown when email doesn't have an Ente account", + "placeholders": { + "email": { + "type": "String" + } + } + }, + "accountOwnerPersonAppbarTitle": "{title} (Ben)", + "@accountOwnerPersonAppbarTitle": { + "description": "Title of appbar for account owner person", + "placeholders": { + "title": { + "type": "String" + } + } + }, + "reassignMe": "\"Ben \"i yeniden atayın", + "me": "Ben", + "linkEmailToContactBannerCaption": "d", + "@linkEmailToContactBannerCaption": { + "description": "Caption for the 'Link email' title. It should be a continuation of the 'Link email' title. Just like how 'Link email' + 'for faster sharing' forms a proper sentence in English, the combination of these two strings should also be a proper sentence in other languages." + }, + "selectPersonToLink": "Bağlantı kurulacak kişiyi seçin", + "linkPersonToEmail": "Kişiyi {email} adresine bağlayın", + "@linkPersonToEmail": { + "placeholders": { + "email": { + "type": "String" + } + } + }, + "linkPersonToEmailConfirmation": "Bu, {personName} ile {email} arasında bağlantı kuracaktır.", + "@linkPersonToEmailConfirmation": { + "description": "Confirmation message when linking a person to an email", + "placeholders": { + "personName": { + "type": "String" + }, + "email": { + "type": "String" + } + } + }, + "selectYourFace": "Yüzünüzü seçin", + "reassigningLoading": "Yeniden atama...", + "reassignedToName": "Sizi {name}'e yeniden atadım", + "@reassignedToName": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "saveChangesBeforeLeavingQuestion": "Ayrılmadan önce değişiklikleri kaydedin mi?", + "dontSave": "Kaydetme", + "thisIsMeExclamation": "Bu benim!", + "linkPerson": "Bağlantı kişisi", + "linkPersonCaption": "daha iyi paylaşım deneyimi için", + "@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ışı", + "processingVideos": "Videolar işleniyor", + "streamDetails": "Yayın detayları", + "processing": "İşleniyor", + "queued": "Kuyrukta", + "ineligible": "Uygun Değil", + "failed": "Başarısız oldu", + "playStream": "Akışı oynat", + "playOriginal": "Orijinali oynat", + "joinAlbumConfirmationDialogBody": "Bir albüme katılmak, e-postanızın katılımcılar tarafından görülebilmesini sağlayacaktır." } \ No newline at end of file diff --git a/mobile/lib/l10n/intl_uk.arb b/mobile/lib/l10n/intl_uk.arb index 32733441e1..1f0e2c0d10 100644 --- a/mobile/lib/l10n/intl_uk.arb +++ b/mobile/lib/l10n/intl_uk.arb @@ -520,7 +520,6 @@ "viewLargeFiles": "Великі файли", "viewLargeFilesDesc": "Перегляньте файли, які займають найбільше місця у сховищі.", "noDuplicates": "✨ Немає дублікатів", - "youveNoDuplicateFilesThatCanBeCleared": "У вас немає дублікатів файлів, які можна очистити", "success": "Успішно", "rateUs": "Оцініть нас", "remindToEmptyDeviceTrash": "Також очистьте «Нещодавно видалено» в «Налаштування» -> «Сховище», щоб отримати вільне місце", @@ -960,6 +959,14 @@ "syncStopped": "Синхронізацію зупинено", "syncProgress": "{completed} / {total} спогадів збережено", "uploadingMultipleMemories": "Збереження {count} спогадів...", + "@uploadingMultipleMemories": { + "description": "Text to tell user how many memories are being preserved", + "placeholders": { + "count": { + "type": "String" + } + } + }, "uploadingSingleMemory": "Зберігаємо 1 спогад...", "@syncProgress": { "description": "Text to tell user how many memories have been preserved", @@ -1386,7 +1393,7 @@ "enableMachineLearningBanner": "Увімкніть машинне навчання для магічного пошуку та розпізнавання облич", "searchDiscoverEmptySection": "Зображення будуть показані тут після завершення оброблення та синхронізації", "searchPersonsEmptySection": "Люди будуть показані тут після завершення оброблення та синхронізації", - "viewersSuccessfullyAdded": "{count, plural, few {Додано {count} користувача} many {Додано {count} користувачів}=0 {Додано 0 користувачів} =1 {Додано 1 користувач} other {Додано {count} користувачів}}", + "viewersSuccessfullyAdded": "{count, plural, one {} few {Додано {count} користувача} many {Додано {count} користувачів}=0 {Додано 0 користувачів} =1 {Додано 1 користувач} other {Додано {count} користувачів}}", "@viewersSuccessfullyAdded": { "placeholders": { "count": { @@ -1396,7 +1403,7 @@ }, "description": "Number of viewers that were successfully added to an album." }, - "collaboratorsSuccessfullyAdded": "{count, plural, few {Додано {count} співаторів} many {Додано {count} співаторів}=0 {Додано 0 співавторів} =1 {Додано 1 співавтор} other {Додано {count} співавторів}}", + "collaboratorsSuccessfullyAdded": "{count, plural, one {} few {Додано {count} співаторів} many {Додано {count} співаторів}=0 {Додано 0 співавторів} =1 {Додано 1 співавтор} other {Додано {count} співавторів}}", "@collaboratorsSuccessfullyAdded": { "placeholders": { "count": { @@ -1471,7 +1478,7 @@ }, "currentlyRunning": "зараз працює", "ignored": "ігнорується", - "photosCount": "{count, plural, few {{count} фото} many {{count} фото}=0 {0 фото} =1 {1 фото} other {{count} фото}}", + "photosCount": "{count, plural, one {} few {{count} фото} many {{count} фото}=0 {0 фото} =1 {1 фото} other {{count} фото}}", "@photosCount": { "placeholders": { "count": { @@ -1577,6 +1584,5 @@ }, "legacyInvite": "{email} запросив вас стати довіреною особою", "authToManageLegacy": "Авторизуйтесь, щоби керувати довіреними контактами", - "useDifferentPlayerInfo": "Виникли проблеми з відтворенням цього відео? Натисніть і утримуйте тут, щоб спробувати інший плеєр.", - "orPickFromYourContacts": "or pick from your contacts" + "useDifferentPlayerInfo": "Виникли проблеми з відтворенням цього відео? Натисніть і утримуйте тут, щоб спробувати інший плеєр." } \ No newline at end of file diff --git a/mobile/lib/l10n/intl_vi.arb b/mobile/lib/l10n/intl_vi.arb index 658d5a857b..8ad79e7b48 100644 --- a/mobile/lib/l10n/intl_vi.arb +++ b/mobile/lib/l10n/intl_vi.arb @@ -525,7 +525,6 @@ "viewLargeFiles": "Tệp lớn", "viewLargeFilesDesc": "Xem các tệp đang tiêu tốn nhiều dung lượng lưu trữ nhất.", "noDuplicates": "✨ Không có trùng lặp", - "youveNoDuplicateFilesThatCanBeCleared": "Bạn không có tệp trùng lặp nào có thể được xóa", "success": "Thành công", "rateUs": "Đánh giá chúng tôi", "remindToEmptyDeviceTrash": "Cũng hãy xóa \"Đã xóa gần đây\" từ \"Cài đặt\" -> \"Lưu trữ\" để chiếm không gian đã giải phóng", @@ -965,6 +964,14 @@ "syncStopped": "Đồng bộ hóa đã dừng", "syncProgress": "{completed}/{total} kỷ niệm đã được lưu giữ", "uploadingMultipleMemories": "Đang lưu giữ {count} kỷ niệm...", + "@uploadingMultipleMemories": { + "description": "Text to tell user how many memories are being preserved", + "placeholders": { + "count": { + "type": "String" + } + } + }, "uploadingSingleMemory": "Đang lưu giữ 1 kỷ niệm...", "@syncProgress": { "description": "Text to tell user how many memories have been preserved", @@ -1589,6 +1596,5 @@ "joinAlbum": "Tham gia album", "joinAlbumSubtext": "để xem và thêm ảnh của bạn", "joinAlbumSubtextViewer": "thêm vào album được chia sẻ", - "join": "Tham gia", - "orPickFromYourContacts": "or pick from your contacts" + "join": "Tham gia" } \ No newline at end of file diff --git a/mobile/lib/l10n/intl_zh.arb b/mobile/lib/l10n/intl_zh.arb index a4185b23ab..fd08378794 100644 --- a/mobile/lib/l10n/intl_zh.arb +++ b/mobile/lib/l10n/intl_zh.arb @@ -525,7 +525,6 @@ "viewLargeFiles": "大文件", "viewLargeFilesDesc": "查看占用存储空间最多的文件。", "noDuplicates": "✨ 没有重复内容", - "youveNoDuplicateFilesThatCanBeCleared": "您没有可以被清除的重复文件", "success": "成功", "rateUs": "给我们评分", "remindToEmptyDeviceTrash": "同时从“设置”->“存储”中清空“最近删除”以领取释放的空间", @@ -965,6 +964,14 @@ "syncStopped": "同步已停止", "syncProgress": "已保存的回忆 {completed}/共 {total}", "uploadingMultipleMemories": "正在保存 {count} 个回忆...", + "@uploadingMultipleMemories": { + "description": "Text to tell user how many memories are being preserved", + "placeholders": { + "count": { + "type": "String" + } + } + }, "uploadingSingleMemory": "正在保存 1 个回忆...", "@syncProgress": { "description": "Text to tell user how many memories have been preserved", @@ -1590,5 +1597,81 @@ "joinAlbumSubtext": "来查看和添加您的照片", "joinAlbumSubtextViewer": "来将其添加到共享相册", "join": "加入", - "orPickFromYourContacts": "or pick from your contacts" + "linkEmail": "链接邮箱", + "link": "链接", + "noEnteAccountExclamation": "没有 Ente 账户!", + "orPickFromYourContacts": "或从您的联系人中选择", + "emailDoesNotHaveEnteAccount": "{email} 没有 Ente 账户。", + "@emailDoesNotHaveEnteAccount": { + "description": "Shown when email doesn't have an Ente account", + "placeholders": { + "email": { + "type": "String" + } + } + }, + "accountOwnerPersonAppbarTitle": "{title} (我)", + "@accountOwnerPersonAppbarTitle": { + "description": "Title of appbar for account owner person", + "placeholders": { + "title": { + "type": "String" + } + } + }, + "reassignMe": "重新分配“我”", + "me": "我", + "linkEmailToContactBannerCaption": "来实现更快的共享", + "@linkEmailToContactBannerCaption": { + "description": "Caption for the 'Link email' title. It should be a continuation of the 'Link email' title. Just like how 'Link email' + 'for faster sharing' forms a proper sentence in English, the combination of these two strings should also be a proper sentence in other languages." + }, + "selectPersonToLink": "选择要链接的人", + "linkPersonToEmail": "将人员链接到 {email}", + "@linkPersonToEmail": { + "placeholders": { + "email": { + "type": "String" + } + } + }, + "linkPersonToEmailConfirmation": "这将会将 {personName} 链接到 {email}", + "@linkPersonToEmailConfirmation": { + "description": "Confirmation message when linking a person to an email", + "placeholders": { + "personName": { + "type": "String" + }, + "email": { + "type": "String" + } + } + }, + "selectYourFace": "选择你的脸", + "reassigningLoading": "正在重新分配...", + "reassignedToName": "已将您重新分配给 {name}", + "@reassignedToName": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "saveChangesBeforeLeavingQuestion": "离开之前要保存更改吗?", + "dontSave": "不保存", + "thisIsMeExclamation": "这就是我!", + "linkPerson": "链接人员", + "linkPersonCaption": "来感受更好的共享体验", + "@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": "正在处理", + "queued": "已入列", + "ineligible": "不合格", + "failed": "失败", + "playStream": "播放流", + "playOriginal": "播放原内容", + "joinAlbumConfirmationDialogBody": "加入相册将使相册的参与者可以看到您的电子邮件地址。" } \ No newline at end of file diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart index 91741eb54a..50edd83e97 100644 --- a/mobile/lib/main.dart +++ b/mobile/lib/main.dart @@ -4,6 +4,7 @@ import 'dart:io'; import "package:adaptive_theme/adaptive_theme.dart"; import 'package:background_fetch/background_fetch.dart'; import "package:computer/computer.dart"; +import 'package:ente_crypto/ente_crypto.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import "package:flutter/rendering.dart"; @@ -26,13 +27,13 @@ import 'package:photos/ente_theme_data.dart'; import "package:photos/extensions/stop_watch.dart"; import "package:photos/l10n/l10n.dart"; import "package:photos/service_locator.dart"; +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/local_sync_service.dart'; import "package:photos/services/machine_learning/face_ml/person/person_service.dart"; import 'package:photos/services/machine_learning/ml_service.dart'; import 'package:photos/services/machine_learning/semantic_search/semantic_search_service.dart'; @@ -40,18 +41,16 @@ import 'package:photos/services/memories_service.dart'; import "package:photos/services/notification_service.dart"; // import 'package:photos/services/push_service.dart'; import "package:photos/services/preview_video_store.dart"; -import 'package:photos/services/remote_sync_service.dart'; import 'package:photos/services/search_service.dart'; -import "package:photos/services/sync_service.dart"; -import "package:photos/services/user_service.dart"; +import 'package:photos/services/sync/local_sync_service.dart'; +import 'package:photos/services/sync/remote_sync_service.dart'; +import "package:photos/services/sync/sync_service.dart"; import 'package:photos/ui/tools/app_lock.dart'; import 'package:photos/ui/tools/lock_screen.dart'; -import 'package:photos/utils/crypto_util.dart'; import "package:photos/utils/email_util.dart"; import 'package:photos/utils/file_uploader.dart'; import "package:photos/utils/lock_screen_settings.dart"; import 'package:shared_preferences/shared_preferences.dart'; -import "package:video_player_media_kit/video_player_media_kit.dart"; final _logger = Logger("main"); @@ -237,10 +236,6 @@ Future _init(bool isBackground, {String via = ''}) async { ServiceLocator.instance .init(preferences, NetworkClient.instance.enteDio, packageInfo); - if (!isBackground && flagService.internalUser) { - VideoPlayerMediaKit.ensureInitialized(iOS: true); - } - _logger.info("UserService init $tlog"); await UserService.instance.init(); _logger.info("UserService init done $tlog"); diff --git a/mobile/lib/models/billing_plan.dart b/mobile/lib/models/api/billing/billing_plan.dart similarity index 100% rename from mobile/lib/models/billing_plan.dart rename to mobile/lib/models/api/billing/billing_plan.dart diff --git a/mobile/lib/models/subscription.dart b/mobile/lib/models/api/billing/subscription.dart similarity index 100% rename from mobile/lib/models/subscription.dart rename to mobile/lib/models/api/billing/subscription.dart diff --git a/mobile/lib/models/collection/collection_file_item.dart b/mobile/lib/models/api/collection/collection_file_item.dart similarity index 100% rename from mobile/lib/models/collection/collection_file_item.dart rename to mobile/lib/models/api/collection/collection_file_item.dart diff --git a/mobile/lib/models/api/collection/create_request.dart b/mobile/lib/models/api/collection/create_request.dart index 24bf0a9b0f..2cc3954bae 100644 --- a/mobile/lib/models/api/collection/create_request.dart +++ b/mobile/lib/models/api/collection/create_request.dart @@ -1,5 +1,5 @@ +import "package:photos/models/api/metadata.dart"; import 'package:photos/models/collection/collection.dart'; -import 'package:photos/services/file_magic_service.dart'; class CreateRequest { String encryptedKey; @@ -45,7 +45,7 @@ class CreateRequest { map['keyDecryptionNonce'] = keyDecryptionNonce; map['encryptedName'] = encryptedName; map['nameDecryptionNonce'] = nameDecryptionNonce; - map['type'] = Collection.typeToString(type); + map['type'] = typeToString(type); if (attributes != null) { map['attributes'] = attributes!.toMap(); } diff --git a/mobile/lib/models/trash_item_request.dart b/mobile/lib/models/api/collection/trash_item_request.dart similarity index 100% rename from mobile/lib/models/trash_item_request.dart rename to mobile/lib/models/api/collection/trash_item_request.dart diff --git a/mobile/lib/models/api/entity/data.dart b/mobile/lib/models/api/entity/data.dart index b46b3bdb75..691ab0edc9 100644 --- a/mobile/lib/models/api/entity/data.dart +++ b/mobile/lib/models/api/entity/data.dart @@ -1,6 +1,6 @@ import 'dart:convert'; -import 'package:flutter/material.dart'; +import "package:freezed_annotation/freezed_annotation.dart"; @immutable class EntityData { diff --git a/mobile/lib/models/api/metadata.dart b/mobile/lib/models/api/metadata.dart new file mode 100644 index 0000000000..dbb3a479b0 --- /dev/null +++ b/mobile/lib/models/api/metadata.dart @@ -0,0 +1,54 @@ +class UpdateMagicMetadataRequest { + final int id; + final MetadataRequest? magicMetadata; + + UpdateMagicMetadataRequest({required this.id, required this.magicMetadata}); + + factory UpdateMagicMetadataRequest.fromJson(dynamic json) { + return UpdateMagicMetadataRequest( + id: json['id'], + magicMetadata: json['magicMetadata'] != null + ? MetadataRequest.fromJson(json['magicMetadata']) + : null, + ); + } + + Map toJson() { + final map = {}; + map['id'] = id; + if (magicMetadata != null) { + map['magicMetadata'] = magicMetadata!.toJson(); + } + return map; + } +} + +class MetadataRequest { + int? version; + int? count; + String? data; + String? header; + + MetadataRequest({ + required this.version, + required this.count, + required this.data, + required this.header, + }); + + MetadataRequest.fromJson(dynamic json) { + version = json['version']; + count = json['count']; + data = json['data']; + header = json['header']; + } + + Map toJson() { + final map = {}; + map['version'] = version; + map['count'] = count; + map['data'] = data; + map['header'] = header; + return map; + } +} diff --git a/mobile/lib/models/delete_account.dart b/mobile/lib/models/api/user/delete_account.dart similarity index 100% rename from mobile/lib/models/delete_account.dart rename to mobile/lib/models/api/user/delete_account.dart diff --git a/mobile/lib/models/key_attributes.dart b/mobile/lib/models/api/user/key_attributes.dart similarity index 100% rename from mobile/lib/models/key_attributes.dart rename to mobile/lib/models/api/user/key_attributes.dart diff --git a/mobile/lib/models/key_gen_result.dart b/mobile/lib/models/api/user/key_gen_result.dart similarity index 65% rename from mobile/lib/models/key_gen_result.dart rename to mobile/lib/models/api/user/key_gen_result.dart index 9ea188286b..6d999ea110 100644 --- a/mobile/lib/models/key_gen_result.dart +++ b/mobile/lib/models/api/user/key_gen_result.dart @@ -1,7 +1,7 @@ import "dart:typed_data"; -import 'package:photos/models/key_attributes.dart'; -import 'package:photos/models/private_key_attributes.dart'; +import 'package:photos/models/api/user/key_attributes.dart'; +import 'package:photos/models/api/user/private_key_attributes.dart'; class KeyGenResult { final KeyAttributes keyAttributes; diff --git a/mobile/lib/models/private_key_attributes.dart b/mobile/lib/models/api/user/private_key_attributes.dart similarity index 100% rename from mobile/lib/models/private_key_attributes.dart rename to mobile/lib/models/api/user/private_key_attributes.dart diff --git a/mobile/lib/models/sessions.dart b/mobile/lib/models/api/user/sessions.dart similarity index 100% rename from mobile/lib/models/sessions.dart rename to mobile/lib/models/api/user/sessions.dart diff --git a/mobile/lib/models/set_keys_request.dart b/mobile/lib/models/api/user/set_keys_request.dart similarity index 100% rename from mobile/lib/models/set_keys_request.dart rename to mobile/lib/models/api/user/set_keys_request.dart diff --git a/mobile/lib/models/set_recovery_key_request.dart b/mobile/lib/models/api/user/set_recovery_key_request.dart similarity index 100% rename from mobile/lib/models/set_recovery_key_request.dart rename to mobile/lib/models/api/user/set_recovery_key_request.dart diff --git a/mobile/lib/models/base_location.dart b/mobile/lib/models/base_location.dart new file mode 100644 index 0000000000..38b7847c64 --- /dev/null +++ b/mobile/lib/models/base_location.dart @@ -0,0 +1,51 @@ +import "package:photos/models/file/file.dart"; +import "package:photos/models/location/location.dart"; + +class BaseLocation { + final List files; + int? firstCreationTime; + int? lastCreationTime; + final Location location; + final bool isCurrentBase; + + BaseLocation( + this.files, + this.location, + this.isCurrentBase, { + this.firstCreationTime, + this.lastCreationTime, + }); + + int averageCreationTime() { + if (firstCreationTime != null && lastCreationTime != null) { + return (firstCreationTime! + lastCreationTime!) ~/ 2; + } + final List creationTimes = files + .where((file) => file.creationTime != null) + .map((file) => file.creationTime!) + .toList(); + if (creationTimes.length < 2) { + return creationTimes.isEmpty ? 0 : creationTimes.first; + } + creationTimes.sort(); + firstCreationTime ??= creationTimes.first; + lastCreationTime ??= creationTimes.last; + return (firstCreationTime! + lastCreationTime!) ~/ 2; + } + + BaseLocation copyWith({ + List? files, + int? firstCreationTime, + int? lastCreationTime, + Location? location, + bool? isCurrentBase, + }) { + return BaseLocation( + files ?? this.files, + location ?? this.location, + isCurrentBase ?? this.isCurrentBase, + firstCreationTime: firstCreationTime ?? this.firstCreationTime, + lastCreationTime: lastCreationTime ?? this.lastCreationTime, + ); + } +} diff --git a/mobile/lib/models/collection/collection.dart b/mobile/lib/models/collection/collection.dart index df15f47840..c62c9432d5 100644 --- a/mobile/lib/models/collection/collection.dart +++ b/mobile/lib/models/collection/collection.dart @@ -8,7 +8,7 @@ import "package:photos/models/metadata/common_keys.dart"; class Collection { final int id; - final User? owner; + final User owner; final String encryptedKey; final String? keyDecryptionNonce; @Deprecated("Use collectionName instead") @@ -20,8 +20,8 @@ class Collection { final String? nameDecryptionNonce; final CollectionType type; final CollectionAttributes attributes; - final List? sharees; - final List? publicURLs; + final List sharees; + final List publicURLs; final int updationTime; final bool isDeleted; @@ -95,12 +95,12 @@ class Collection { // hasLink returns true if there's any link attached to the collection // including expired links - bool get hasLink => publicURLs != null && publicURLs!.isNotEmpty; + bool get hasLink => publicURLs.isNotEmpty; bool get hasCover => (pubMagicMetadata.coverID ?? 0) > 0; // hasSharees returns true if the collection is shared with other ente users - bool get hasSharees => sharees != null && sharees!.isNotEmpty; + bool get hasSharees => sharees.isNotEmpty; bool get isPinned => (magicMetadata.order ?? 0) != 0; @@ -121,52 +121,43 @@ class Collection { } List getSharees() { - final List result = []; - if (sharees == null) { - return result; - } - for (final User? u in sharees!) { - if (u != null) { - result.add(u); - } - } - return result; + return sharees; } bool isOwner(int userID) { - return (owner?.id ?? 0) == userID; + return (owner.id ?? -100) == userID; } bool isDownloadEnabledForPublicLink() { - if (publicURLs == null || publicURLs!.isEmpty) { + if (publicURLs.isEmpty) { return false; } - return publicURLs?.first?.enableDownload ?? true; + return publicURLs.first.enableDownload; } bool isCollectEnabledForPublicLink() { - if (publicURLs == null || publicURLs!.isEmpty) { + if (publicURLs.isEmpty) { return false; } - return publicURLs?.first?.enableCollect ?? false; + return publicURLs.first.enableCollect; } bool get isJoinEnabled { - if (publicURLs == null || publicURLs!.isEmpty) { + if (publicURLs.isEmpty) { return false; } - return publicURLs?.first?.enableJoin ?? false; + return publicURLs.first.enableJoin; } CollectionParticipantRole getRole(int userID) { if (isOwner(userID)) { return CollectionParticipantRole.owner; } - if (sharees == null) { + if (sharees.isEmpty) { return CollectionParticipantRole.unknown; } - for (final User? u in sharees!) { - if (u != null && u.id == userID) { + for (final User u in sharees) { + if (u.id == userID) { if (u.isViewer) { return CollectionParticipantRole.viewer; } else if (u.isCollaborator) { @@ -185,40 +176,8 @@ class Collection { } void updateSharees(List newSharees) { - sharees?.clear(); - sharees?.addAll(newSharees); - } - - static CollectionType typeFromString(String type) { - switch (type) { - case "folder": - return CollectionType.folder; - case "favorites": - return CollectionType.favorites; - case "uncategorized": - return CollectionType.uncategorized; - case "album": - return CollectionType.album; - case "unknown": - return CollectionType.unknown; - } - debugPrint("unexpected collection type $type"); - return CollectionType.unknown; - } - - static String typeToString(CollectionType type) { - switch (type) { - case CollectionType.folder: - return "folder"; - case CollectionType.favorites: - return "favorites"; - case CollectionType.album: - return "album"; - case CollectionType.uncategorized: - return "uncategorized"; - case CollectionType.unknown: - return "unknown"; - } + sharees.clear(); + sharees.addAll(newSharees); } Collection copyWith({ @@ -303,6 +262,38 @@ enum CollectionType { unknown, } +CollectionType typeFromString(String type) { + switch (type) { + case "folder": + return CollectionType.folder; + case "favorites": + return CollectionType.favorites; + case "uncategorized": + return CollectionType.uncategorized; + case "album": + return CollectionType.album; + case "unknown": + return CollectionType.unknown; + } + debugPrint("unexpected collection type $type"); + return CollectionType.unknown; +} + +String typeToString(CollectionType type) { + switch (type) { + case CollectionType.folder: + return "folder"; + case CollectionType.favorites: + return "favorites"; + case CollectionType.album: + return "album"; + case CollectionType.uncategorized: + return "uncategorized"; + case CollectionType.unknown: + return "unknown"; + } +} + extension CollectionTypeExtn on CollectionType { bool get canDelete => this != CollectionType.favorites && this != CollectionType.uncategorized; diff --git a/mobile/lib/models/file/file.dart b/mobile/lib/models/file/file.dart index 937bee0cb1..eea913a1bc 100644 --- a/mobile/lib/models/file/file.dart +++ b/mobile/lib/models/file/file.dart @@ -10,10 +10,10 @@ 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/utils/date_time_util.dart'; import 'package:photos/utils/exif_util.dart'; import 'package:photos/utils/file_uploader_util.dart'; import "package:photos/utils/panorama_util.dart"; +import 'package:photos/utils/standalone/date_time.dart'; //Todo: files with no location data have lat and long set to 0.0. This should ideally be null. class EnteFile { @@ -160,6 +160,7 @@ class EnteFile { Future> getMetadataForUpload( MediaUploadData mediaUploadData, + ParsedExifDateTime? exifTime, ) async { final asset = await getAsset; // asset can be null for files shared to app @@ -170,36 +171,24 @@ class EnteFile { } } bool hasExifTime = false; - if ((fileType == FileType.image || fileType == FileType.video) && - mediaUploadData.sourceFile != null) { - final exifData = await getExifFromSourceFile(mediaUploadData.sourceFile!); - if (exifData != null) { - if (fileType == FileType.image) { - final exifTime = await getCreationTimeFromEXIF(null, exifData); - if (exifTime != null) { - hasExifTime = true; - creationTime = exifTime.microsecondsSinceEpoch; - } - mediaUploadData.isPanorama = checkPanoramaFromEXIF(null, exifData); - - if (mediaUploadData.isPanorama != true) { - try { - final xmpData = await getXmp(mediaUploadData.sourceFile!); - mediaUploadData.isPanorama = checkPanoramaFromXMP(xmpData); - } catch (_) {} - - mediaUploadData.isPanorama ??= false; - } - } - if (Platform.isAndroid) { - //Fix for missing location data in lower android versions. - final Location? exifLocation = locationFromExif(exifData); - if (Location.isValidLocation(exifLocation)) { - location = exifLocation; - } - } - } + if (exifTime != null && exifTime.time != null) { + hasExifTime = true; + creationTime = exifTime.time!.microsecondsSinceEpoch; } + if (mediaUploadData.exifData != null) { + mediaUploadData.isPanorama = + checkPanoramaFromEXIF(null, mediaUploadData.exifData); + } + if (mediaUploadData.isPanorama != true && + fileType == FileType.image && + mediaUploadData.sourceFile != null) { + try { + final xmpData = await getXmp(mediaUploadData.sourceFile!); + mediaUploadData.isPanorama = checkPanoramaFromXMP(xmpData); + } catch (_) {} + mediaUploadData.isPanorama ??= false; + } + // Try to get the timestamp from fileName. In case of iOS, file names are // generic IMG_XXXX, so only parse it on Android devices if (!hasExifTime && Platform.isAndroid && title != null) { diff --git a/mobile/lib/models/memories/filler_memory.dart b/mobile/lib/models/memories/filler_memory.dart new file mode 100644 index 0000000000..3e4e02b2c2 --- /dev/null +++ b/mobile/lib/models/memories/filler_memory.dart @@ -0,0 +1,19 @@ +import "package:photos/models/memories/memory.dart"; +import "package:photos/models/memories/smart_memory.dart"; + +class FillerMemory extends SmartMemory { + FillerMemory( + List memories, + String title, + int firstDateToShow, + int lastDateToShow, { + int? firstCreationTime, + int? lastCreationTime, + }) : super( + memories, + MemoryType.filler, + title, + firstDateToShow, + lastDateToShow, + ); +} diff --git a/mobile/lib/models/memories/memories_cache.dart b/mobile/lib/models/memories/memories_cache.dart new file mode 100644 index 0000000000..5298c7f81a --- /dev/null +++ b/mobile/lib/models/memories/memories_cache.dart @@ -0,0 +1,273 @@ +import "dart:convert"; + +import "package:photos/models/location/location.dart"; +import "package:photos/models/memories/people_memory.dart"; +import "package:photos/models/memories/smart_memory.dart"; +import "package:photos/models/memories/trip_memory.dart"; + +const kPersonShowTimeout = Duration(days: 7 * 10); +const kPersonAndTypeShowTimeout = Duration(days: 7 * 26); +const kTripShowTimeout = Duration(days: 7 * 25); + +final maxShowTimeout = [ + kPersonShowTimeout, + kPersonAndTypeShowTimeout, + kTripShowTimeout, + ].reduce((value, element) => value > element ? value : element) * + 3; + +class MemoriesCache { + final List toShowMemories; + final List peopleShownLogs; + final List tripsShownLogs; + + MemoriesCache({ + required this.toShowMemories, + required this.peopleShownLogs, + required this.tripsShownLogs, + }); + + factory MemoriesCache.fromJson(Map json) { + return MemoriesCache( + toShowMemories: ToShowMemory.decodeJsonToList(json['toShowMemories']), + peopleShownLogs: PeopleShownLog.decodeJsonToList(json['peopleShownLogs']), + tripsShownLogs: TripsShownLog.decodeJsonToList(json['tripsShownLogs']), + ); + } + + Map toJson() { + return { + 'toShowMemories': ToShowMemory.encodeListToJson(toShowMemories), + 'peopleShownLogs': PeopleShownLog.encodeListToJson(peopleShownLogs), + 'tripsShownLogs': TripsShownLog.encodeListToJson(tripsShownLogs), + }; + } + + static String encodeToJsonString(MemoriesCache cache) { + return jsonEncode(cache.toJson()); + } + + static MemoriesCache decodeFromJsonString(String jsonString) { + return MemoriesCache.fromJson(jsonDecode(jsonString)); + } +} + +class ToShowMemory { + final String title; + final List fileUploadedIDs; + final MemoryType type; + final int firstTimeToShow; + final int lastTimeToShow; + final int calculationTime; + + final String? personID; + final PeopleMemoryType? peopleMemoryType; + final Location? location; + + bool get isOld { + final now = DateTime.now().microsecondsSinceEpoch; + return now > lastTimeToShow; + } + + bool shouldShowNow() { + final now = DateTime.now().microsecondsSinceEpoch; + final relevantForNow = now >= firstTimeToShow && now < lastTimeToShow; + final calculatedForNow = (now >= calculationTime) && + (now < calculationTime + kMemoriesUpdateFrequency.inMicroseconds); + return relevantForNow && calculatedForNow; + } + + ToShowMemory( + this.title, + this.fileUploadedIDs, + this.type, + this.firstTimeToShow, + this.lastTimeToShow, + this.calculationTime, { + this.personID, + this.peopleMemoryType, + this.location, + }) : assert( + (type == MemoryType.people && + personID != null && + peopleMemoryType != null) || + (type == MemoryType.trips && location != null) || + (type != MemoryType.people && type != MemoryType.trips), + "PersonID and peopleMemoryType must be provided for people memory type, and location must be provided for trips memory type", + ); + + factory ToShowMemory.fromSmartMemory(SmartMemory memory, DateTime calcTime) { + String? personID; + PeopleMemoryType? peopleMemoryType; + Location? location; + if (memory is PeopleMemory) { + personID = memory.personID; + peopleMemoryType = memory.peopleMemoryType; + } else if (memory is TripMemory) { + location = memory.location; + } + return ToShowMemory( + memory.title, + memory.memories + .where((m) => m.file.uploadedFileID != null) + .map((m) => m.file.uploadedFileID!) + .toList(), + memory.type, + memory.firstDateToShow, + memory.lastDateToShow, + calcTime.microsecondsSinceEpoch, + personID: personID, + peopleMemoryType: peopleMemoryType, + location: location, + ); + } + + factory ToShowMemory.fromJson(Map json) { + return ToShowMemory( + json['title'], + List.from(json['fileUploadedIDs']), + memoryTypeFromString(json['type']), + json['firstTimeToShow'], + json['lastTimeToShow'], + json['calculationTime'], + personID: json['personID'], + peopleMemoryType: json['peopleMemoryType'] != null + ? peopleMemoryTypeFromString(json['peopleMemoryType']) + : null, + location: json['location'] != null + ? Location( + latitude: json['location']['latitude'], + longitude: json['location']['longitude'], + ) + : null, + ); + } + + Map toJson() { + return { + 'title': title, + 'fileUploadedIDs': fileUploadedIDs.toList(), + 'type': type.toString().split('.').last, + 'firstTimeToShow': firstTimeToShow, + 'lastTimeToShow': lastTimeToShow, + 'calculationTime': calculationTime, + 'personID': personID, + 'peopleMemoryType': peopleMemoryType?.toString().split('.').last, + 'location': location != null + ? { + 'latitude': location!.latitude!, + 'longitude': location!.longitude!, + } + : null, + }; + } + + static String encodeListToJson(List toShowMemories) { + final jsonList = toShowMemories.map((memory) => memory.toJson()).toList(); + return jsonEncode(jsonList); + } + + static List decodeJsonToList(String jsonString) { + final jsonList = jsonDecode(jsonString) as List; + return jsonList.map((json) => ToShowMemory.fromJson(json)).toList(); + } +} + +class PeopleShownLog { + final String personID; + final PeopleMemoryType peopleMemoryType; + final int lastTimeShown; + + PeopleShownLog( + this.personID, + this.peopleMemoryType, + this.lastTimeShown, + ); + + factory PeopleShownLog.fromOldCacheMemory(ToShowMemory memory) { + assert( + memory.type == MemoryType.people && + memory.personID != null && + memory.peopleMemoryType != null, + ); + return PeopleShownLog( + memory.personID!, + memory.peopleMemoryType!, + memory.lastTimeToShow, + ); + } + + factory PeopleShownLog.fromJson(Map json) { + return PeopleShownLog( + json['personID'], + peopleMemoryTypeFromString(json['peopleMemoryType']), + json['lastTimeShown'], + ); + } + + Map toJson() { + return { + 'personID': personID, + 'peopleMemoryType': peopleMemoryType.toString().split('.').last, + 'lastTimeShown': lastTimeShown, + }; + } + + static String encodeListToJson(List shownLogs) { + final jsonList = shownLogs.map((log) => log.toJson()).toList(); + return jsonEncode(jsonList); + } + + static List decodeJsonToList(String jsonString) { + final jsonList = jsonDecode(jsonString) as List; + return jsonList.map((json) => PeopleShownLog.fromJson(json)).toList(); + } +} + +class TripsShownLog { + final Location location; + final int lastTimeShown; + + TripsShownLog( + this.location, + this.lastTimeShown, + ); + + factory TripsShownLog.fromOldCacheMemory(ToShowMemory memory) { + assert(memory.type == MemoryType.trips && memory.location != null); + return TripsShownLog( + memory.location!, + memory.lastTimeToShow, + ); + } + + factory TripsShownLog.fromJson(Map json) { + return TripsShownLog( + Location( + latitude: json['location']['latitude'], + longitude: json['location']['longitude'], + ), + json['lastTimeShown'], + ); + } + + Map toJson() { + return { + 'location': { + 'latitude': location.latitude!, + 'longitude': location.longitude!, + }, + 'lastTimeShown': lastTimeShown, + }; + } + + static String encodeListToJson(List shownLogs) { + final jsonList = shownLogs.map((log) => log.toJson()).toList(); + return jsonEncode(jsonList); + } + + static List decodeJsonToList(String jsonString) { + final jsonList = jsonDecode(jsonString) as List; + return jsonList.map((json) => TripsShownLog.fromJson(json)).toList(); + } +} diff --git a/mobile/lib/models/memories/memory.dart b/mobile/lib/models/memories/memory.dart new file mode 100644 index 0000000000..d0f2db37ef --- /dev/null +++ b/mobile/lib/models/memories/memory.dart @@ -0,0 +1,42 @@ +import 'package:photos/models/file/file.dart'; + +class Memory { + final EnteFile file; + int _seenTime; + + Memory(this.file, this._seenTime); + + bool isSeen() { + return _seenTime != -1; + } + + int seenTime() { + return _seenTime; + } + + void markSeen() { + _seenTime = DateTime.now().microsecondsSinceEpoch; + } + + Memory.fromFile(this.file, Map? seenTimes) + : _seenTime = seenTimes?[file.generatedID] ?? -1; + + static List fromFiles( + List files, + Map? seenTimes, + ) { + final memories = []; + for (final file in files) { + memories.add(Memory.fromFile(file, seenTimes)); + } + return memories; + } + + static List filesFromMemories(List memories) { + final List files = []; + for (final memory in memories) { + files.add(memory.file); + } + return files; + } +} diff --git a/mobile/lib/models/memories/people_memory.dart b/mobile/lib/models/memories/people_memory.dart new file mode 100644 index 0000000000..0ce3ac6453 --- /dev/null +++ b/mobile/lib/models/memories/people_memory.dart @@ -0,0 +1,106 @@ +import "package:photos/models/memories/memory.dart"; +import "package:photos/models/memories/smart_memory.dart"; + +enum PeopleMemoryType { + youAndThem, + doingSomethingTogether, + spotlight, + lastTimeYouSawThem, +} + +const peopleRotationTypes = [ + PeopleMemoryType.youAndThem, + PeopleMemoryType.doingSomethingTogether, + PeopleMemoryType.spotlight, +]; + +PeopleMemoryType peopleMemoryTypeFromString(String type) { + switch (type) { + case "youAndThem": + return PeopleMemoryType.youAndThem; + case "doingSomethingTogether": + return PeopleMemoryType.doingSomethingTogether; + case "spotlight": + return PeopleMemoryType.spotlight; + case "lastTimeYouSawThem": + return PeopleMemoryType.lastTimeYouSawThem; + default: + throw ArgumentError("Invalid people memory type: $type"); + } +} + +enum PeopleActivity { party, hiking, feast, selfies, sports } + +String activityQuery(PeopleActivity activity) { + switch (activity) { + case PeopleActivity.party: + return "Photo of people celebrating together"; + case PeopleActivity.hiking: + return "Photo of people hiking together in nature"; + case PeopleActivity.feast: + return "Photo of people having a big feast together"; + case PeopleActivity.selfies: + return "Happy and nostalgic selfie with people"; + case PeopleActivity.sports: + return "Photo of people joyfully playing sports together"; + } +} + +String activityTitle(PeopleActivity activity, String personName) { + switch (activity) { + case PeopleActivity.party: + return "Party with $personName"; + case PeopleActivity.hiking: + return "Hiking with $personName"; + case PeopleActivity.feast: + return "Feasting with $personName"; + case PeopleActivity.selfies: + return "Selfies with $personName"; + case PeopleActivity.sports: + return "Sports with $personName"; + } +} + +class PeopleMemory extends SmartMemory { + final String personID; + final PeopleMemoryType peopleMemoryType; + + PeopleMemory( + List memories, + String title, + int firstDateToShow, + int lastDateToShow, + this.peopleMemoryType, + this.personID, { + super.firstCreationTime, + super.lastCreationTime, + }) : super( + memories, + MemoryType.people, + title, + firstDateToShow, + lastDateToShow, + ); + + PeopleMemory copyWith({ + List? memories, + String? title, + int? firstDateToShow, + int? lastDateToShow, + PeopleMemoryType? peopleMemoryType, + String? personID, + int? firstCreationTime, + int? lastCreationTime, + }) { + return PeopleMemory( + memories ?? this.memories, + title ?? this.title, + firstDateToShow ?? this.firstDateToShow, + lastDateToShow ?? this.lastDateToShow, + peopleMemoryType ?? this.peopleMemoryType, + personID ?? this.personID, + firstCreationTime: firstCreationTime ?? this.firstCreationTime, + lastCreationTime: lastCreationTime ?? this.lastCreationTime, + ); + } +} diff --git a/mobile/lib/models/memories/smart_memory.dart b/mobile/lib/models/memories/smart_memory.dart new file mode 100644 index 0000000000..d3527b2895 --- /dev/null +++ b/mobile/lib/models/memories/smart_memory.dart @@ -0,0 +1,82 @@ +import "package:photos/models/memories/memory.dart"; + +const kMemoriesUpdateFrequency = Duration(days: 7); +const kMemoriesMargin = Duration(days: 2); +const kDayItself = Duration(days: 1); + +enum MemoryType { + people, + trips, + time, + filler, +} + +MemoryType memoryTypeFromString(String type) { + switch (type) { + case "people": + return MemoryType.people; + case "trips": + return MemoryType.trips; + case "time": + return MemoryType.time; + case "filler": + return MemoryType.filler; + default: + throw ArgumentError("Invalid memory type: $type"); + } +} + +class SmartMemory { + final List memories; + final MemoryType type; + String title; + int firstDateToShow; + int lastDateToShow; + + int? firstCreationTime; + int? lastCreationTime; + // TODO: lau: actually use this in calculated filters + + SmartMemory( + this.memories, + this.type, + this.title, + this.firstDateToShow, + this.lastDateToShow, { + this.firstCreationTime, + this.lastCreationTime, + }); + + bool get notForShow => firstDateToShow == 0 && lastDateToShow == 0; + + bool isOld() { + return lastDateToShow < DateTime.now().microsecondsSinceEpoch; + } + + bool shouldShowNow() { + final int now = DateTime.now().microsecondsSinceEpoch; + return now >= firstDateToShow && now <= lastDateToShow; + } + + int averageCreationTime() { + if (firstCreationTime != null && lastCreationTime != null) { + return (firstCreationTime! + lastCreationTime!) ~/ 2; + } + final List creationTimes = memories + .where((memory) => memory.file.creationTime != null) + .map((memory) => memory.file.creationTime!) + .toList(); + if (creationTimes.length < 2) { + if (creationTimes.isEmpty) { + firstCreationTime = 0; + lastCreationTime = 0; + return 0; + } + return creationTimes.isEmpty ? 0 : creationTimes.first; + } + creationTimes.sort(); + firstCreationTime ??= creationTimes.first; + lastCreationTime ??= creationTimes.last; + return (firstCreationTime! + lastCreationTime!) ~/ 2; + } +} diff --git a/mobile/lib/models/memories/time_memory.dart b/mobile/lib/models/memories/time_memory.dart new file mode 100644 index 0000000000..7dafef90ec --- /dev/null +++ b/mobile/lib/models/memories/time_memory.dart @@ -0,0 +1,19 @@ +import "package:photos/models/memories/memory.dart"; +import "package:photos/models/memories/smart_memory.dart"; + +class TimeMemory extends SmartMemory { + TimeMemory( + List memories, + String title, + int firstDateToShow, + int lastDateToShow, { + int? firstCreationTime, + int? lastCreationTime, + }) : super( + memories, + MemoryType.time, + title, + firstDateToShow, + lastDateToShow, + ); +} diff --git a/mobile/lib/models/memories/trip_memory.dart b/mobile/lib/models/memories/trip_memory.dart new file mode 100644 index 0000000000..8763d20c1e --- /dev/null +++ b/mobile/lib/models/memories/trip_memory.dart @@ -0,0 +1,43 @@ +import "package:photos/models/location/location.dart"; +import "package:photos/models/memories/memory.dart"; +import "package:photos/models/memories/smart_memory.dart"; + +class TripMemory extends SmartMemory { + final Location location; + + TripMemory( + List memories, + String title, + int firstDateToShow, + int lastDateToShow, + this.location, { + super.firstCreationTime, + super.lastCreationTime, + }) : super( + memories, + MemoryType.trips, + title, + firstDateToShow, + lastDateToShow, + ); + + TripMemory copyWith({ + List? memories, + String? title, + int? firstDateToShow, + int? lastDateToShow, + Location? location, + int? firstCreationTime, + int? lastCreationTime, + }) { + return TripMemory( + memories ?? this.memories, + title ?? this.title, + firstDateToShow ?? this.firstDateToShow, + lastDateToShow ?? this.lastDateToShow, + location ?? this.location, + firstCreationTime: firstCreationTime ?? this.firstCreationTime, + lastCreationTime: lastCreationTime ?? this.lastCreationTime, + ); + } +} diff --git a/mobile/lib/models/memory.dart b/mobile/lib/models/memory.dart deleted file mode 100644 index 32bc40e13c..0000000000 --- a/mobile/lib/models/memory.dart +++ /dev/null @@ -1,20 +0,0 @@ -import 'package:photos/models/file/file.dart'; - -class Memory { - final EnteFile file; - int _seenTime; - - Memory(this.file, this._seenTime); - - bool isSeen() { - return _seenTime != -1; - } - - int seenTime() { - return _seenTime; - } - - void markSeen() { - _seenTime = DateTime.now().microsecondsSinceEpoch; - } -} diff --git a/mobile/lib/models/metadata/file_magic.dart b/mobile/lib/models/metadata/file_magic.dart index 7599b7c82f..02f6188a9d 100644 --- a/mobile/lib/models/metadata/file_magic.dart +++ b/mobile/lib/models/metadata/file_magic.dart @@ -14,6 +14,8 @@ const latKey = "lat"; const longKey = "long"; const motionVideoIndexKey = "mvi"; const noThumbKey = "noThumb"; +const dateTimeKey = 'dateTime'; +const offsetTimeKey = 'offsetTime'; class MagicMetadata { // 0 -> visible @@ -46,6 +48,11 @@ class PubMagicMetadata { double? lat; double? long; + // 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; + String? offsetTime; + // Motion Video Index. Positive value (>0) indicates that the file is a motion // photo int? mvi; @@ -74,6 +81,8 @@ class PubMagicMetadata { this.mvi, this.noThumb, this.mediaType, + this.dateTime, + this.offsetTime, }); factory PubMagicMetadata.fromEncodedJson(String encodedJson) => @@ -96,6 +105,8 @@ class PubMagicMetadata { mvi: map[motionVideoIndexKey], noThumb: map[noThumbKey], mediaType: map[mediaTypeKey], + dateTime: map[dateTimeKey], + offsetTime: map[offsetTimeKey], ); } diff --git a/mobile/lib/models/ml/face/box.dart b/mobile/lib/models/ml/face/box.dart index d5d8672fb9..8d4adfd112 100644 --- a/mobile/lib/models/ml/face/box.dart +++ b/mobile/lib/models/ml/face/box.dart @@ -1,4 +1,4 @@ -import "package:photos/utils/parse.dart"; +import "package:photos/utils/standalone/parse.dart"; /// Bounding box of a face. /// diff --git a/mobile/lib/models/ml/face/face.dart b/mobile/lib/models/ml/face/face.dart index 1001d4988e..5e4c554e3b 100644 --- a/mobile/lib/models/ml/face/face.dart +++ b/mobile/lib/models/ml/face/face.dart @@ -4,7 +4,7 @@ import "package:photos/models/ml/face/dimension.dart"; import "package:photos/models/ml/face/landmark.dart"; import 'package:photos/services/machine_learning/face_ml/face_filtering/face_filtering_constants.dart'; import "package:photos/services/machine_learning/ml_result.dart"; -import "package:photos/utils/parse.dart"; +import "package:photos/utils/standalone/parse.dart"; // FileInfo contains the image width and height of the image the face was detected in. class FileInfo { diff --git a/mobile/lib/models/ml/face/person.dart b/mobile/lib/models/ml/face/person.dart index b758285461..f373c89303 100644 --- a/mobile/lib/models/ml/face/person.dart +++ b/mobile/lib/models/ml/face/person.dart @@ -52,6 +52,8 @@ class PersonData { String? avatarFaceID; List assigned = List.empty(); List rejectedFaceIDs = List.empty(); + + /// string formatted in `yyyy-MM-dd` final String? birthDate; /// email should be always looked via userID as user might have changed diff --git a/mobile/lib/models/ml/vector.dart b/mobile/lib/models/ml/vector.dart index 1b0fd21491..925a02f6b3 100644 --- a/mobile/lib/models/ml/vector.dart +++ b/mobile/lib/models/ml/vector.dart @@ -1,5 +1,4 @@ import "dart:convert"; - import "package:ml_linalg/vector.dart"; class EmbeddingVector { @@ -13,11 +12,25 @@ class EmbeddingVector { required List embedding, }) : vector = Vector.fromList(embedding); - static Vector decodeEmbedding(String embedding) { - return Vector.fromList(List.from(jsonDecode(embedding) as List)); + static EmbeddingVector fromJsonString(String jsonString) { + return _fromJson(jsonDecode(jsonString) as Map); } - static String encodeEmbedding(Vector embedding) { - return jsonEncode(embedding.toList()); + String toJsonString() { + return jsonEncode(_toJson()); + } + + Map _toJson() { + return { + "fileID": fileID, + "embedding": vector.toList(), + }; + } + + static EmbeddingVector _fromJson(Map json) { + return EmbeddingVector( + fileID: json["fileID"] as int, + embedding: List.from(json["embedding"] as List), + ); } } diff --git a/mobile/lib/models/search/search_types.dart b/mobile/lib/models/search/search_types.dart index 90d1a320f5..63d1543042 100644 --- a/mobile/lib/models/search/search_types.dart +++ b/mobile/lib/models/search/search_types.dart @@ -242,7 +242,7 @@ extension SectionTypeExtensions on SectionType { case SectionType.moment: if (flagService.internalUser) { - return SearchService.instance.onThisDayOrWeekResults(context, limit); + return SearchService.instance.smartMemories(context, limit); } return SearchService.instance.getRandomMomentsSearchResults(context); diff --git a/mobile/lib/models/user_details.dart b/mobile/lib/models/user_details.dart index f107a2e6b0..d7318a9796 100644 --- a/mobile/lib/models/user_details.dart +++ b/mobile/lib/models/user_details.dart @@ -2,9 +2,8 @@ import 'dart:convert'; import 'dart:math'; import 'package:collection/collection.dart'; +import "package:photos/models/api/billing/subscription.dart"; import "package:photos/models/api/storage_bonus/bonus.dart"; -import 'package:photos/models/file/file_type.dart'; -import 'package:photos/models/subscription.dart'; class UserDetails { final String email; @@ -52,6 +51,10 @@ class UserDetails { } int getFreeStorage() { + final int? memberLimit = familyMemberStorageLimit(); + if (memberLimit != null) { + return max(memberLimit - usage, 0); + } return max(getTotalStorage() - getFamilyOrPersonalUsage(), 0); } @@ -62,6 +65,17 @@ class UserDetails { storageBonus; } + // return the member storage limit if user is part of family and the admin + // has set the storage limit for the user. + int? familyMemberStorageLimit() { + if (isPartOfFamily()) { + final FamilyMember? currentUserMember = familyData!.members! + .firstWhereOrNull((element) => element.email.trim() == email.trim()); + return currentUserMember?.storageLimit; + } + return null; + } + // This is the total storage for which user has paid for. int getPlanPlusAddonStorage() { return (isPartOfFamily() ? familyData!.storage : subscription.storage) + @@ -107,12 +121,14 @@ class FamilyMember { final int usage; final String id; final bool isAdmin; + final int? storageLimit; FamilyMember( this.email, this.usage, this.id, this.isAdmin, + this.storageLimit, ); factory FamilyMember.fromMap(Map map) { @@ -121,6 +137,7 @@ class FamilyMember { map['usage'] as int, map['id'] as String, map['isAdmin'] as bool, + map['storageLimit'] as int?, ); } @@ -130,6 +147,7 @@ class FamilyMember { 'usage': usage, 'id': id, 'isAdmin': isAdmin, + 'storageLimit': storageLimit, }; } @@ -189,6 +207,10 @@ class FamilyData { return members!.map((e) => e.usage).toList().sum; } + FamilyMember? getMemberByID(String id) { + return members!.firstWhereOrNull((element) => element.id == id); + } + static fromMap(Map? map) { if (map == null) return null; assert(map['members'] != null && map['members'].length >= 0); @@ -215,19 +237,3 @@ class FamilyData { factory FamilyData.fromJson(String source) => FamilyData.fromMap(json.decode(source)); } - -class FilesCount { - final Map filesCount; - FilesCount(this.filesCount); - - int get total => - images + videos + livePhotos + (filesCount[getInt(FileType.other)] ?? 0); - - int get photos => images + livePhotos; - - int get images => filesCount[FileType.image] ?? 0; - - int get videos => filesCount[FileType.video] ?? 0; - - int get livePhotos => filesCount[FileType.livePhoto] ?? 0; -} diff --git a/mobile/lib/models/upload_url.dart b/mobile/lib/module/upload/model/upload_url.dart similarity index 100% rename from mobile/lib/models/upload_url.dart rename to mobile/lib/module/upload/model/upload_url.dart diff --git a/mobile/lib/module/upload/service/multipart.dart b/mobile/lib/module/upload/service/multipart.dart index 163f055845..f4ecf3b3c4 100644 --- a/mobile/lib/module/upload/service/multipart.dart +++ b/mobile/lib/module/upload/service/multipart.dart @@ -1,17 +1,16 @@ import "dart:io"; import "package:dio/dio.dart"; +import "package:ente_crypto/ente_crypto.dart"; import "package:ente_feature_flag/ente_feature_flag.dart"; import "package:flutter/foundation.dart"; import "package:logging/logging.dart"; import "package:photos/core/constants.dart"; import "package:photos/db/upload_locks_db.dart"; -import "package:photos/models/encryption_result.dart"; import "package:photos/module/upload/model/multipart.dart"; import "package:photos/module/upload/model/xml.dart"; import "package:photos/service_locator.dart"; import "package:photos/services/collections_service.dart"; -import "package:photos/utils/crypto_util.dart"; class MultiPartUploader { final Dio _enteDio; @@ -66,17 +65,12 @@ class MultiPartUploader { Future getMultipartUploadURLs(int count) async { try { - assert( - _featureFlagService.internalUser, - "Multipart upload should not be enabled for external users.", - ); final response = await _enteDio.get( "/files/multipart-upload-urls", queryParameters: { "count": count, }, ); - return MultipartUploadURLs.fromMap(response.data); } on Exception catch (e) { _logger.severe('failed to get multipart url', e); @@ -132,7 +126,7 @@ class MultiPartUploader { // upload individual parts and get their etags try { etags = await _uploadParts(multipartInfo, encryptedFile); - } on DioError catch (e) { + } on DioException catch (e) { if (e.response?.statusCode == 404) { _logger.severe( "Multipart upload not found for key ${multipartInfo.urls.objectKey}", @@ -157,7 +151,7 @@ class MultiPartUploader { etags, multipartInfo.urls.completeURL, ); - } on DioError catch (e) { + } on DioException catch (e) { if (e.response?.statusCode == 404) { _logger.severe( "Multipart upload not found for key ${multipartInfo.urls.objectKey}", diff --git a/mobile/lib/service_locator.dart b/mobile/lib/service_locator.dart index d33bef747e..2cfce9c076 100644 --- a/mobile/lib/service_locator.dart +++ b/mobile/lib/service_locator.dart @@ -4,16 +4,17 @@ 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/services/billing_service.dart"; +import "package:photos/services/account/billing_service.dart"; import "package:photos/services/entity_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"; import "package:photos/services/magic_cache_service.dart"; +import "package:photos/services/memories_cache_service.dart"; +import "package:photos/services/smart_memories_service.dart"; import "package:photos/services/storage_bonus_service.dart"; -import "package:photos/services/trash_sync_service.dart"; +import "package:photos/services/sync/trash_sync_service.dart"; import "package:photos/services/update_service.dart"; -import "package:photos/services/user_remote_flag_service.dart"; import "package:photos/utils/local_settings.dart"; import "package:shared_preferences/shared_preferences.dart"; @@ -101,15 +102,6 @@ LocationService get locationService { return _locationService!; } -UserRemoteFlagService? _userRemoteFlagService; -UserRemoteFlagService get userRemoteFlagService { - _userRemoteFlagService ??= UserRemoteFlagService( - ServiceLocator.instance.enteDio, - ServiceLocator.instance.prefs, - ); - return _userRemoteFlagService!; -} - MagicCacheService? _magicCacheService; MagicCacheService get magicCacheService { _magicCacheService ??= MagicCacheService( @@ -118,6 +110,20 @@ MagicCacheService get magicCacheService { return _magicCacheService!; } +MemoriesCacheService? _memoriesCacheService; +MemoriesCacheService get memoriesCacheService { + _memoriesCacheService ??= MemoriesCacheService( + ServiceLocator.instance.prefs, + ); + return _memoriesCacheService!; +} + +SmartMemoriesService? _smartMemoriesService; +SmartMemoriesService get smartMemoriesService { + _smartMemoriesService ??= SmartMemoriesService(); + return _smartMemoriesService!; +} + BillingService? _billingService; BillingService get billingService { _billingService ??= BillingService( diff --git a/mobile/lib/services/billing_service.dart b/mobile/lib/services/account/billing_service.dart similarity index 92% rename from mobile/lib/services/billing_service.dart rename to mobile/lib/services/account/billing_service.dart index 9c5f5c3507..2d3d7317c7 100644 --- a/mobile/lib/services/billing_service.dart +++ b/mobile/lib/services/account/billing_service.dart @@ -7,10 +7,10 @@ import 'package:flutter/material.dart'; import 'package:logging/logging.dart'; import 'package:photos/core/errors.dart'; import "package:photos/generated/l10n.dart"; -import 'package:photos/models/billing_plan.dart'; -import 'package:photos/models/subscription.dart'; +import 'package:photos/models/api/billing/billing_plan.dart'; +import 'package:photos/models/api/billing/subscription.dart'; import 'package:photos/models/user_details.dart'; -import 'package:photos/services/user_service.dart'; +import 'package:photos/services/account/user_service.dart'; import 'package:photos/ui/common/web_page.dart'; import 'package:photos/utils/dialog_util.dart'; @@ -75,7 +75,7 @@ class BillingService { }, ); return Subscription.fromMap(response.data["subscription"]); - } on DioError catch (e) { + } on DioException catch (e) { if (e.response != null && e.response!.statusCode == 409) { throw SubscriptionAlreadyClaimedError(); } else { @@ -92,7 +92,7 @@ class BillingService { final response = await _enteDio.get("/billing/subscription"); final subscription = Subscription.fromMap(response.data["subscription"]); return subscription; - } on DioError catch (e, s) { + } on DioException catch (e, s) { _logger.severe(e, s); rethrow; } @@ -104,7 +104,7 @@ class BillingService { await _enteDio.post("/billing/stripe/cancel-subscription"); final subscription = Subscription.fromMap(response.data["subscription"]); return subscription; - } on DioError catch (e, s) { + } on DioException catch (e, s) { _logger.severe(e, s); rethrow; } @@ -116,7 +116,7 @@ class BillingService { await _enteDio.post("/billing/stripe/activate-subscription"); final subscription = Subscription.fromMap(response.data["subscription"]); return subscription; - } on DioError catch (e, s) { + } on DioException catch (e, s) { _logger.severe(e, s); rethrow; } @@ -133,7 +133,7 @@ class BillingService { }, ); return response.data["url"]; - } on DioError catch (e, s) { + } on DioException catch (e, s) { _logger.severe(e, s); rethrow; } diff --git a/mobile/lib/services/passkey_service.dart b/mobile/lib/services/account/passkey_service.dart similarity index 100% rename from mobile/lib/services/passkey_service.dart rename to mobile/lib/services/account/passkey_service.dart diff --git a/mobile/lib/services/user_service.dart b/mobile/lib/services/account/user_service.dart similarity index 95% rename from mobile/lib/services/user_service.dart rename to mobile/lib/services/account/user_service.dart index 60b2ad04f0..82341c5e6f 100644 --- a/mobile/lib/services/user_service.dart +++ b/mobile/lib/services/account/user_service.dart @@ -4,6 +4,7 @@ import "dart:math"; import 'package:bip39/bip39.dart' as bip39; import 'package:dio/dio.dart'; +import 'package:ente_crypto/ente_crypto.dart'; import "package:flutter/foundation.dart"; import 'package:flutter/material.dart'; import 'package:logging/logging.dart'; @@ -19,13 +20,13 @@ import "package:photos/generated/l10n.dart"; import "package:photos/l10n/l10n.dart"; import "package:photos/models/account/two_factor.dart"; import "package:photos/models/api/collection/user.dart"; +import 'package:photos/models/api/user/delete_account.dart'; +import 'package:photos/models/api/user/key_attributes.dart'; +import 'package:photos/models/api/user/key_gen_result.dart'; +import 'package:photos/models/api/user/sessions.dart'; +import 'package:photos/models/api/user/set_keys_request.dart'; +import 'package:photos/models/api/user/set_recovery_key_request.dart'; import "package:photos/models/api/user/srp.dart"; -import 'package:photos/models/delete_account.dart'; -import 'package:photos/models/key_attributes.dart'; -import 'package:photos/models/key_gen_result.dart'; -import 'package:photos/models/sessions.dart'; -import 'package:photos/models/set_keys_request.dart'; -import 'package:photos/models/set_recovery_key_request.dart'; import 'package:photos/models/user_details.dart'; import "package:photos/services/collections_service.dart"; import "package:photos/services/machine_learning/face_ml/person/person_service.dart"; @@ -39,11 +40,10 @@ import 'package:photos/ui/account/two_factor_authentication_page.dart'; import 'package:photos/ui/account/two_factor_recovery_page.dart'; import 'package:photos/ui/account/two_factor_setup_page.dart'; import "package:photos/ui/common/progress_dialog.dart"; +import 'package:photos/ui/notification/toast.dart'; import "package:photos/ui/tabs/home_widget.dart"; -import 'package:photos/utils/crypto_util.dart'; import 'package:photos/utils/dialog_util.dart'; import 'package:photos/utils/navigation_util.dart'; -import 'package:photos/utils/toast_util.dart'; import "package:pointycastle/export.dart"; import "package:pointycastle/srp/srp6_client.dart"; import "package:pointycastle/srp/srp6_standard_groups.dart"; @@ -124,7 +124,7 @@ class UserService { } else { throw Exception("send-ott action failed, non-200"); } - } on DioError catch (e) { + } on DioException catch (e) { await dialog.hide(); _logger.info(e); final String? enteErrCode = e.response?.data["code"]; @@ -185,7 +185,7 @@ class UserService { ); final publicKey = response.data["publicKey"]; return publicKey; - } on DioError catch (e) { + } on DioException catch (e) { if (e.response != null && e.response?.statusCode == 404) { return null; } @@ -221,7 +221,7 @@ class UserService { } } return userDetails; - } on DioError catch (e) { + } on DioException catch (e) { _logger.info(e); rethrow; } @@ -231,7 +231,7 @@ class UserService { try { final response = await _enteDio.get("/users/sessions"); return Sessions.fromMap(response.data); - } on DioError catch (e) { + } on DioException catch (e) { _logger.info(e); rethrow; } @@ -245,7 +245,7 @@ class UserService { "token": token, }, ); - } on DioError catch (e) { + } on DioException catch (e) { _logger.info(e); rethrow; } @@ -254,7 +254,7 @@ class UserService { Future leaveFamilyPlan() async { try { await _enteDio.delete("/family/leave"); - } on DioError catch (e) { + } on DioException catch (e) { _logger.warning('failed to leave family plan', e); rethrow; } @@ -271,7 +271,7 @@ class UserService { } } catch (e) { // check if token is already invalid - if (e is DioError && e.response?.statusCode == 401) { + if (e is DioException && e.response?.statusCode == 401) { await Configuration.instance.logout(); Navigator.of(context).popUntil((route) => route.isFirst); return; @@ -342,7 +342,7 @@ class UserService { }, ); return response.data; - } on DioError catch (e) { + } on DioException catch (e) { if (e.response != null) { if (e.response!.statusCode == 404 || e.response!.statusCode == 410) { throw PassKeySessionExpiredError(); @@ -460,7 +460,7 @@ class UserService { // should never reach here throw Exception("unexpected response during email verification"); } - } on DioError catch (e) { + } on DioException catch (e) { _logger.info(e); await dialog.hide(); if (e.response != null && e.response!.statusCode == 410) { @@ -532,7 +532,7 @@ class UserService { S.of(context).oops, S.of(context).verificationFailedPleaseTryAgain, ); - } on DioError catch (e) { + } on DioException catch (e) { await dialog.hide(); if (e.response != null && e.response!.statusCode == 403) { // ignore: unawaited_futures @@ -592,7 +592,7 @@ class UserService { } else { throw Exception("get-srp-attributes action failed"); } - } on DioError catch (e) { + } on DioException catch (e) { if (e.response != null && e.response!.statusCode == 404) { throw SrpSetupNotCompleteError(); } @@ -695,7 +695,7 @@ class UserService { late Uint8List keyEncryptionKey; _logger.finest('Start deriving key'); keyEncryptionKey = await CryptoUtil.deriveKey( - utf8.encode(userPassword) as Uint8List, + utf8.encode(userPassword), CryptoUtil.base642bin(srpAttributes.kekSalt), srpAttributes.memLimit, srpAttributes.opsLimit, @@ -865,7 +865,7 @@ class UserService { (route) => route.isFirst, ); } - } on DioError catch (e) { + } on DioException catch (e) { await dialog.hide(); _logger.severe(e); if (e.response != null && e.response!.statusCode == 404) { @@ -932,7 +932,7 @@ class UserService { (route) => route.isFirst, ); } - } on DioError catch (e) { + } on DioException catch (e) { await dialog.hide(); _logger.severe('error while recovery 2fa', e); if (e.response != null && e.response!.statusCode == 404) { @@ -1031,7 +1031,7 @@ class UserService { (route) => route.isFirst, ); } - } on DioError catch (e) { + } on DioException catch (e) { await dialog.hide(); _logger.severe("error during recovery", e); if (e.response != null && e.response!.statusCode == 404) { @@ -1126,7 +1126,7 @@ class UserService { } catch (e, s) { await dialog.hide(); _logger.severe(e, s); - if (e is DioError) { + if (e is DioException) { if (e.response != null && e.response!.statusCode == 401) { // ignore: unawaited_futures showErrorDialog( @@ -1311,34 +1311,30 @@ class UserService { for (final c in CollectionsService.instance.getActiveCollections()) { // Add collaborators and viewers of collections owned by user - if (c.owner?.id == ownerID) { - for (final User? u in c.sharees ?? []) { - if (u != null && u.id != null && u.email.isNotEmpty) { + if (c.owner.id == ownerID) { + for (final User u in c.sharees) { + if (u.id != null && u.email.isNotEmpty) { if (!existingEmails.contains(u.email)) { relevantUsers.add(u); existingEmails.add(u.email); } } } - } else if (c.owner?.id != null && c.owner!.email.isNotEmpty) { + } else if (c.owner.id != null && c.owner.email.isNotEmpty) { // Add owners of collections shared with user - if (!existingEmails.contains(c.owner!.email)) { - relevantUsers.add(c.owner!); - existingEmails.add(c.owner!.email); + if (!existingEmails.contains(c.owner.email)) { + relevantUsers.add(c.owner); + existingEmails.add(c.owner.email); } // Add collaborators of collections shared with user where user is a // viewer or a collaborator - for (final User? u in c.sharees ?? []) { - if (u != null && - u.id != null && + for (final User u in c.sharees) { + if (u.id != null && u.email.isNotEmpty && u.email == ownerEmail && (u.isCollaborator || u.isViewer)) { - for (final User? u in c.sharees ?? []) { - if (u != null && - u.id != null && - u.email.isNotEmpty && - u.isCollaborator) { + for (final User u in c.sharees) { + if (u.id != null && u.email.isNotEmpty && u.isCollaborator) { if (!existingEmails.contains(u.email)) { relevantUsers.add(u); existingEmails.add(u.email); @@ -1392,32 +1388,28 @@ class UserService { for (final c in CollectionsService.instance.getActiveCollections()) { // Add collaborators and viewers of collections owned by user - if (c.owner?.id == ownerID) { - for (final User? u in c.sharees ?? []) { - if (u != null && u.id != null && u.email.isNotEmpty) { + if (c.owner.id == ownerID) { + for (final User u in c.sharees) { + if (u.id != null && u.email.isNotEmpty) { if (!emailIDs.contains(u.email)) { emailIDs.add(u.email); } } } - } else if (c.owner?.id != null && c.owner!.email.isNotEmpty) { + } else if (c.owner.id != null && c.owner.email.isNotEmpty) { // Add owners of collections shared with user - if (!emailIDs.contains(c.owner!.email)) { - emailIDs.add(c.owner!.email); + if (!emailIDs.contains(c.owner.email)) { + emailIDs.add(c.owner.email); } // Add collaborators of collections shared with user where user is a // viewer or a collaborator - for (final User? u in c.sharees ?? []) { - if (u != null && - u.id != null && + for (final User u in c.sharees) { + if (u.id != null && u.email.isNotEmpty && u.email == ownerEmail && (u.isCollaborator || u.isViewer)) { - for (final User? u in c.sharees ?? []) { - if (u != null && - u.id != null && - u.email.isNotEmpty && - u.isCollaborator) { + for (final User u in c.sharees) { + if (u.id != null && u.email.isNotEmpty && u.isCollaborator) { if (!emailIDs.contains(u.email)) { emailIDs.add(u.email); } diff --git a/mobile/lib/services/collections_service.dart b/mobile/lib/services/collections_service.dart index 7c5de3985b..7ee743f5ee 100644 --- a/mobile/lib/services/collections_service.dart +++ b/mobile/lib/services/collections_service.dart @@ -4,6 +4,7 @@ import 'dart:math'; import 'package:collection/collection.dart'; import 'package:dio/dio.dart'; +import 'package:ente_crypto/ente_crypto.dart'; import "package:fast_base58/fast_base58.dart"; import 'package:flutter/foundation.dart'; import "package:flutter/material.dart"; @@ -24,11 +25,12 @@ import 'package:photos/events/local_photos_updated_event.dart'; import 'package:photos/extensions/list.dart'; import 'package:photos/extensions/stop_watch.dart'; import "package:photos/generated/l10n.dart"; +import 'package:photos/models/api/collection/collection_file_item.dart'; import 'package:photos/models/api/collection/create_request.dart'; import "package:photos/models/api/collection/public_url.dart"; import "package:photos/models/api/collection/user.dart"; +import "package:photos/models/api/metadata.dart"; import 'package:photos/models/collection/collection.dart'; -import 'package:photos/models/collection/collection_file_item.dart'; import 'package:photos/models/collection/collection_items.dart'; import 'package:photos/models/file/file.dart'; import "package:photos/models/files_split.dart"; @@ -36,10 +38,8 @@ import "package:photos/models/metadata/collection_magic.dart"; import "package:photos/service_locator.dart"; import 'package:photos/services/app_lifecycle_service.dart'; import "package:photos/services/favorites_service.dart"; -import 'package:photos/services/file_magic_service.dart'; -import 'package:photos/services/local_sync_service.dart'; -import 'package:photos/services/remote_sync_service.dart'; -import 'package:photos/utils/crypto_util.dart'; +import 'package:photos/services/sync/local_sync_service.dart'; +import 'package:photos/services/sync/remote_sync_service.dart'; import "package:photos/utils/dialog_util.dart"; import "package:photos/utils/file_key.dart"; import "package:photos/utils/local_settings.dart"; @@ -139,7 +139,7 @@ class CollectionsService { } } // remove reference for incoming collections when unshared/deleted - if (collection.isDeleted && ownerID != collection.owner?.id) { + if (collection.isDeleted && ownerID != collection.owner.id) { await _db.deleteCollection(collection.id); } else { // keep entry for deletedCollection as collectionKey may be used during @@ -394,7 +394,7 @@ class CollectionsService { final List collections = getCollectionsForUI(includedShared: true); for (final c in collections) { - if (c.owner!.id == Configuration.instance.getUserID()) { + if (c.owner.id == Configuration.instance.getUserID()) { if (c.hasSharees || c.hasLink && !c.isQuickLinkCollection()) { outgoing.add(c); } else if (c.isQuickLinkCollection()) { @@ -472,8 +472,8 @@ class CollectionsService { if (collectionID != null) { final Collection? collection = getCollectionByID(collectionID); if (collection != null) { - if (collection.owner?.id == userID) { - _cachedUserIdToUser[userID] = collection.owner!; + if (collection.owner.id == userID) { + _cachedUserIdToUser[userID] = collection.owner; } else { final matchingUser = collection.getSharees().firstWhereOrNull( (u) => u.id == userID, @@ -553,7 +553,7 @@ class CollectionsService { unawaited(_db.insert([_collectionIDToCollections[collectionID]!])); RemoteSyncService.instance.sync(silently: true).ignore(); return sharees; - } on DioError catch (e) { + } on DioException catch (e) { if (e.response?.statusCode == 402) { throw SharingNotPermittedForFreeAccountsError(); } @@ -641,7 +641,7 @@ class CollectionsService { } else { await _handleCollectionDeletion(collection); } - } on DioError catch (e) { + } on DioException catch (e) { if (e.response != null) { debugPrint("Error " + e.response!.toString()); } @@ -666,6 +666,7 @@ class CollectionsService { ), ); sync().ignore(); + // not required once remote & local world are separate LocalSyncService.instance.syncAll().ignore(); } @@ -698,7 +699,7 @@ class CollectionsService { ); final encryptedKey = CryptoUtil.base642bin(collection.encryptedKey); Uint8List? collectionKey; - if (collection.owner?.id == _config.getUserID()) { + if (collection.owner.id == _config.getUserID()) { // If the collection is owned by the user, decrypt with the master key if (_config.getKey() == null) { // Possible during AppStore account migration, where SecureStorage @@ -730,7 +731,7 @@ class CollectionsService { await updateMagicMetadata(collection, {"subType": 0}); } final encryptedName = CryptoUtil.encryptSync( - utf8.encode(newName) as Uint8List, + utf8.encode(newName), getCollectionKey(collection.id), ); await _enteDio.post( @@ -767,7 +768,7 @@ class CollectionsService { ) async { final int ownerID = Configuration.instance.getUserID()!; try { - if (collection.owner?.id != ownerID) { + if (collection.owner.id != ownerID) { throw AssertionError("cannot modify albums not owned by you"); } // read the existing magic metadata and apply new updates to existing data @@ -781,7 +782,7 @@ class CollectionsService { final key = getCollectionKey(collection.id); final encryptedMMd = await CryptoUtil.encryptChaCha( - utf8.encode(jsonEncode(jsonToUpdate)) as Uint8List, + utf8.encode(jsonEncode(jsonToUpdate)), key, ); // for required field, the json validator on golang doesn't treat 0 as valid @@ -798,7 +799,7 @@ class CollectionsService { ); await _enteDio.put( "/collections/magic-metadata", - data: params, + data: params.toJson(), ); // update the local information so that it's reflected on UI collection.mMdEncodedJson = jsonEncode(jsonToUpdate); @@ -808,7 +809,7 @@ class CollectionsService { // trigger sync to fetch the latest collection state from server sync().ignore(); - } on DioError catch (e) { + } on DioException catch (e) { if (e.response != null && e.response?.statusCode == 409) { _logger.severe('collection magic data out of sync'); sync().ignore(); @@ -826,7 +827,7 @@ class CollectionsService { ) async { final int ownerID = Configuration.instance.getUserID()!; try { - if (collection.owner?.id != ownerID) { + if (collection.owner.id != ownerID) { throw AssertionError("cannot modify albums not owned by you"); } // read the existing magic metadata and apply new updates to existing data @@ -840,7 +841,7 @@ class CollectionsService { final key = getCollectionKey(collection.id); final encryptedMMd = await CryptoUtil.encryptChaCha( - utf8.encode(jsonEncode(jsonToUpdate)) as Uint8List, + utf8.encode(jsonEncode(jsonToUpdate)), key, ); // for required field, the json validator on golang doesn't treat 0 as valid @@ -857,7 +858,7 @@ class CollectionsService { ); await _enteDio.put( "/collections/public-magic-metadata", - data: params, + data: params.toJson(), ); // update the local information so that it's reflected on UI collection.mMdPubEncodedJson = jsonEncode(jsonToUpdate); @@ -867,7 +868,7 @@ class CollectionsService { _cacheLocalPathAndCollection(collection); // trigger sync to fetch the latest collection state from server sync().ignore(); - } on DioError catch (e) { + } on DioException catch (e) { if (e.response != null && e.response?.statusCode == 409) { _logger.severe('collection magic data out of sync'); sync().ignore(); @@ -885,7 +886,7 @@ class CollectionsService { ) async { final int ownerID = Configuration.instance.getUserID()!; try { - if (collection.owner?.id == ownerID) { + if (collection.owner.id == ownerID) { throw AssertionError("cannot modify sharee settings for albums owned " "by you"); } @@ -900,7 +901,7 @@ class CollectionsService { final key = getCollectionKey(collection.id); final encryptedMMd = await CryptoUtil.encryptChaCha( - utf8.encode(jsonEncode(jsonToUpdate)) as Uint8List, + utf8.encode(jsonEncode(jsonToUpdate)), key, ); // for required field, the json validator on golang doesn't treat 0 as valid @@ -917,7 +918,7 @@ class CollectionsService { ); await _enteDio.put( "/collections/sharee-magic-metadata", - data: params, + data: params.toJson(), ); // update the local information so that it's reflected on UI collection.sharedMmdJson = jsonEncode(jsonToUpdate); @@ -927,7 +928,7 @@ class CollectionsService { _cacheLocalPathAndCollection(collection); // trigger sync to fetch the latest collection state from server sync().ignore(); - } on DioError catch (e) { + } on DioException catch (e) { if (e.response != null && e.response?.statusCode == 409) { _logger.severe('collection magic data out of sync'); sync().ignore(); @@ -952,13 +953,13 @@ class CollectionsService { "enableJoin": true, }, ); - collection.publicURLs?.add(PublicURL.fromMap(response.data["result"])); + collection.publicURLs.add(PublicURL.fromMap(response.data["result"])); await _db.insert(List.from([collection])); _collectionIDToCollections[collection.id] = collection; Bus.instance.fire( CollectionUpdatedEvent(collection.id, [], "shareUrL"), ); - } on DioError catch (e) { + } on DioException catch (e) { if (e.response?.statusCode == 402) { throw SharingNotPermittedForFreeAccountsError(); } @@ -980,14 +981,14 @@ class CollectionsService { data: json.encode(prop), ); // remove existing url information - collection.publicURLs?.clear(); - collection.publicURLs?.add(PublicURL.fromMap(response.data["result"])); + collection.publicURLs.clear(); + collection.publicURLs.add(PublicURL.fromMap(response.data["result"])); await _db.insert(List.from([collection])); _collectionIDToCollections[collection.id] = collection; Bus.instance.fire( CollectionUpdatedEvent(collection.id, [], "updateUrl"), ); - } on DioError catch (e) { + } on DioException catch (e) { if (e.response?.statusCode == 402) { throw SharingNotPermittedForFreeAccountsError(); } @@ -1003,7 +1004,7 @@ class CollectionsService { await _enteDio.delete( "/collections/share-url/" + collection.id.toString(), ); - collection.publicURLs?.clear(); + collection.publicURLs.clear(); await _db.insert(List.from([collection])); _collectionIDToCollections[collection.id] = collection; Bus.instance.fire( @@ -1013,7 +1014,7 @@ class CollectionsService { "disableShareUrl", ), ); - } on DioError catch (e) { + } on DioException catch (e) { _logger.info(e); rethrow; } @@ -1038,7 +1039,7 @@ class CollectionsService { return collections; } catch (e, s) { _logger.warning(e, s); - if (e is DioError && e.response?.statusCode == 401) { + if (e is DioException && e.response?.statusCode == 401) { throw UnauthorizedError(); } rethrow; @@ -1091,7 +1092,7 @@ class CollectionsService { } catch (e, s) { _logger.warning(e, s); _logger.severe("Failed to fetch public collection"); - if (e is DioError && e.response?.statusCode == 410) { + if (e is DioException && e.response?.statusCode == 410) { await showInfoDialog( context, title: S.of(context).linkExpired, @@ -1100,7 +1101,7 @@ class CollectionsService { throw UnauthorizedError(); } await showGenericErrorDialog(context: context, error: e); - if (e is DioError && e.response?.statusCode == 401) { + if (e is DioException && e.response?.statusCode == 401) { throw UnauthorizedError(); } rethrow; @@ -1271,7 +1272,7 @@ class CollectionsService { final encryptedKeyData = CryptoUtil.encryptSync(collectionKey, _config.getKey()!); final encryptedName = CryptoUtil.encryptSync( - utf8.encode(albumName) as Uint8List, + utf8.encode(albumName), collectionKey, ); final collection = await createAndCacheCollection( @@ -1300,7 +1301,7 @@ class CollectionsService { _cacheLocalPathAndCollection(collection); return collection; } catch (e) { - if (e is DioError && e.response?.statusCode == 401) { + if (e is DioException && e.response?.statusCode == 401) { throw UnauthorizedError(); } _logger.severe('failed to fetch collection: $collectionID', e); @@ -1321,7 +1322,7 @@ class CollectionsService { final encryptedKeyData = CryptoUtil.encryptSync(collectionKey, _config.getKey()!); final encryptedPath = - CryptoUtil.encryptSync(utf8.encode(path) as Uint8List, collectionKey); + CryptoUtil.encryptSync(utf8.encode(path), collectionKey); final collection = await createAndCacheCollection( CreateRequest( encryptedKey: CryptoUtil.bin2base64(encryptedKeyData.encryptedData!), diff --git a/mobile/lib/services/entity_service.dart b/mobile/lib/services/entity_service.dart index d3df225ca3..2986a33ef5 100644 --- a/mobile/lib/services/entity_service.dart +++ b/mobile/lib/services/entity_service.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:convert'; import 'dart:math'; +import "package:ente_crypto/ente_crypto.dart"; import 'package:flutter/foundation.dart'; import 'package:logging/logging.dart'; import "package:photos/core/configuration.dart"; @@ -12,7 +13,6 @@ import "package:photos/models/api/entity/data.dart"; import "package:photos/models/api/entity/key.dart"; import "package:photos/models/api/entity/type.dart"; import "package:photos/models/local_entity_data.dart"; -import "package:photos/utils/crypto_util.dart"; import "package:photos/utils/gzip.dart"; import 'package:shared_preferences/shared_preferences.dart'; diff --git a/mobile/lib/services/favorites_service.dart b/mobile/lib/services/favorites_service.dart index 42a1252cf6..68cdc59944 100644 --- a/mobile/lib/services/favorites_service.dart +++ b/mobile/lib/services/favorites_service.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'dart:convert'; -import 'dart:typed_data'; +import 'package:ente_crypto/ente_crypto.dart'; import 'package:flutter/material.dart'; import 'package:photos/core/configuration.dart'; import 'package:photos/core/event_bus.dart'; @@ -13,9 +13,8 @@ import 'package:photos/models/api/collection/create_request.dart'; import 'package:photos/models/collection/collection.dart'; import 'package:photos/models/file/file.dart'; import 'package:photos/services/collections_service.dart'; -import 'package:photos/services/remote_sync_service.dart'; +import 'package:photos/services/sync/remote_sync_service.dart'; import 'package:photos/ui/actions/collection/collection_sharing_actions.dart'; -import 'package:photos/utils/crypto_util.dart'; class FavoritesService { late Configuration _config; @@ -230,7 +229,7 @@ class FavoritesService { if (_cachedFavoritesCollectionID == null) { final collections = _collectionsService.getActiveCollections(); for (final collection in collections) { - if (collection.owner!.id == _config.getUserID() && + if (collection.owner.id == _config.getUserID() && collection.type == CollectionType.favorites) { _cachedFavoritesCollectionID = collection.id; return collection; @@ -254,7 +253,7 @@ class FavoritesService { final encryptedKeyResult = CryptoUtil.encryptSync(favoriteCollectionKey, _config.getKey()!); final encName = CryptoUtil.encryptSync( - utf8.encode("Favorites") as Uint8List, + utf8.encode("Favorites"), favoriteCollectionKey, ); final collection = await _collectionsService.createAndCacheCollection( diff --git a/mobile/lib/services/file_magic_service.dart b/mobile/lib/services/file_magic_service.dart index 9fbbfb9760..5c970f5867 100644 --- a/mobile/lib/services/file_magic_service.dart +++ b/mobile/lib/services/file_magic_service.dart @@ -1,7 +1,7 @@ import 'dart:convert'; -import 'dart:typed_data'; import 'package:dio/dio.dart'; +import 'package:ente_crypto/ente_crypto.dart'; import 'package:logging/logging.dart'; import 'package:photos/core/configuration.dart'; import 'package:photos/core/constants.dart'; @@ -12,11 +12,11 @@ import 'package:photos/events/files_updated_event.dart'; import 'package:photos/events/force_reload_home_gallery_event.dart'; import 'package:photos/events/local_photos_updated_event.dart'; import 'package:photos/extensions/list.dart'; +import "package:photos/models/api/metadata.dart"; import 'package:photos/models/file/file.dart'; import "package:photos/models/metadata/common_keys.dart"; import "package:photos/models/metadata/file_magic.dart"; -import 'package:photos/services/remote_sync_service.dart'; -import 'package:photos/utils/crypto_util.dart'; +import 'package:photos/services/sync/remote_sync_service.dart'; import "package:photos/utils/file_key.dart"; class FileMagicService { @@ -95,7 +95,7 @@ class FileMagicService { final fileKey = getFileKey(file); final encryptedMMd = await CryptoUtil.encryptChaCha( - utf8.encode(jsonEncode(jsonToUpdate)) as Uint8List, + utf8.encode(jsonEncode(jsonToUpdate)), fileKey, ); params['metadataList'].add( @@ -117,7 +117,7 @@ class FileMagicService { // should be eventually synced after remote sync has completed await _filesDB.insertMultiple(files); RemoteSyncService.instance.sync(silently: true).ignore(); - } on DioError catch (e) { + } on DioException catch (e) { if (e.response != null && e.response!.statusCode == 409) { RemoteSyncService.instance.sync(silently: true).ignore(); } @@ -161,7 +161,7 @@ class FileMagicService { final fileKey = getFileKey(file); final encryptedMMd = await CryptoUtil.encryptChaCha( - utf8.encode(jsonEncode(jsonToUpdate)) as Uint8List, + utf8.encode(jsonEncode(jsonToUpdate)), fileKey, ); params['metadataList'].add( @@ -185,7 +185,7 @@ class FileMagicService { // update the state of the selected file. Same file in other collection // should be eventually synced after remote sync has completed RemoteSyncService.instance.sync(silently: true).ignore(); - } on DioError catch (e) { + } on DioException catch (e) { if (e.response != null && e.response!.statusCode == 409) { RemoteSyncService.instance.sync(silently: true).ignore(); } @@ -196,58 +196,3 @@ class FileMagicService { } } } - -class UpdateMagicMetadataRequest { - final int id; - final MetadataRequest? magicMetadata; - - UpdateMagicMetadataRequest({required this.id, required this.magicMetadata}); - - factory UpdateMagicMetadataRequest.fromJson(dynamic json) { - return UpdateMagicMetadataRequest( - id: json['id'], - magicMetadata: json['magicMetadata'] != null - ? MetadataRequest.fromJson(json['magicMetadata']) - : null, - ); - } - - Map toJson() { - final map = {}; - map['id'] = id; - if (magicMetadata != null) { - map['magicMetadata'] = magicMetadata!.toJson(); - } - return map; - } -} - -class MetadataRequest { - int? version; - int? count; - String? data; - String? header; - - MetadataRequest({ - required this.version, - required this.count, - required this.data, - required this.header, - }); - - MetadataRequest.fromJson(dynamic json) { - version = json['version']; - count = json['count']; - data = json['data']; - header = json['header']; - } - - Map toJson() { - final map = {}; - map['version'] = version; - map['count'] = count; - map['data'] = data; - map['header'] = header; - return map; - } -} diff --git a/mobile/lib/services/filedata/filedata_service.dart b/mobile/lib/services/filedata/filedata_service.dart index 424bf0210b..dd01fa36e4 100644 --- a/mobile/lib/services/filedata/filedata_service.dart +++ b/mobile/lib/services/filedata/filedata_service.dart @@ -29,6 +29,18 @@ class FileDataService { _prefs = prefs; } + /// 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( + objectId: objectId, + objectSize: objectSize, + ); + } + Future putFileData(EnteFile file, FileDataEntity data) async { data.validate(); final ChaChaEncryptionResult encryptionResult = await gzipAndEncryptJson( diff --git a/mobile/lib/services/filedata/model/file_data.dart b/mobile/lib/services/filedata/model/file_data.dart index 4aedf96128..7de75dbb06 100644 --- a/mobile/lib/services/filedata/model/file_data.dart +++ b/mobile/lib/services/filedata/model/file_data.dart @@ -1,5 +1,5 @@ import "package:photos/models/ml/face/face.dart"; -import "package:photos/utils/parse.dart"; +import "package:photos/utils/standalone/parse.dart"; const _faceKey = 'face'; const _clipKey = 'clip'; diff --git a/mobile/lib/services/files_service.dart b/mobile/lib/services/files_service.dart index a72db91a9e..d6f0994f30 100644 --- a/mobile/lib/services/files_service.dart +++ b/mobile/lib/services/files_service.dart @@ -5,9 +5,11 @@ import 'package:logging/logging.dart'; import 'package:path/path.dart'; import 'package:photos/core/configuration.dart'; import 'package:photos/core/network/network.dart'; +import "package:photos/db/device_files_db.dart"; import 'package:photos/db/files_db.dart'; import 'package:photos/extensions/list.dart'; import "package:photos/generated/l10n.dart"; +import "package:photos/models/backup_status.dart"; import 'package:photos/models/file/file.dart'; import "package:photos/models/file_load_result.dart"; import "package:photos/models/metadata/file_magic.dart"; @@ -16,7 +18,7 @@ import "package:photos/services/ignored_files_service.dart"; import "package:photos/ui/components/action_sheet_widget.dart"; import "package:photos/ui/components/buttons/button_widget.dart"; import "package:photos/ui/components/models/button_type.dart"; -import 'package:photos/utils/date_time_util.dart'; +import 'package:photos/utils/standalone/date_time.dart'; class FilesService { late Dio _enteDio; @@ -71,6 +73,39 @@ class FilesService { } } + Future getBackupStatus({String? pathID}) async { + BackedUpFileIDs ids; + final bool hasMigratedSize = await FilesService.instance.hasMigratedSizes(); + if (pathID == null) { + ids = await FilesDB.instance.getBackedUpIDs(); + } else { + ids = await FilesDB.instance.getBackedUpForDeviceCollection( + pathID, + Configuration.instance.getUserID()!, + ); + } + late int size; + if (hasMigratedSize) { + size = ids.localSize; + } else { + size = await _getFileSize(ids.uploadedIDs); + } + return BackupStatus(ids.localIDs, size); + } + + Future _getFileSize(List fileIDs) async { + try { + final response = await _enteDio.post( + "/files/size", + data: {"fileIDs": fileIDs}, + ); + return response.data["size"]; + } catch (e) { + _logger.severe(e); + rethrow; + } + } + Future> getFilesSizeFromInfo(List uploadedFileID) async { try { final response = await _enteDio.post( diff --git a/mobile/lib/services/hidden_service.dart b/mobile/lib/services/hidden_service.dart index 53aeb24621..b97695a62d 100644 --- a/mobile/lib/services/hidden_service.dart +++ b/mobile/lib/services/hidden_service.dart @@ -1,8 +1,8 @@ import "dart:async"; import 'dart:convert'; -import 'dart:typed_data'; import 'package:collection/collection.dart'; +import 'package:ente_crypto/ente_crypto.dart'; import 'package:flutter/material.dart'; import 'package:logging/logging.dart'; import "package:photos/core/constants.dart"; @@ -12,13 +12,12 @@ 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/models/api/collection/create_request.dart'; +import "package:photos/models/api/metadata.dart"; import 'package:photos/models/collection/collection.dart'; import 'package:photos/models/file/file.dart'; import "package:photos/models/metadata/collection_magic.dart"; import "package:photos/models/metadata/common_keys.dart"; import 'package:photos/services/collections_service.dart'; -import 'package:photos/services/file_magic_service.dart'; -import 'package:photos/utils/crypto_util.dart'; import 'package:photos/utils/dialog_util.dart'; extension HiddenService on CollectionsService { @@ -32,7 +31,7 @@ extension HiddenService on CollectionsService { final int userID = config.getUserID()!; final allDefaultHidden = collectionIDToCollections.values .where( - (element) => element.isDefaultHidden() && element.owner!.id == userID, + (element) => element.isDefaultHidden() && element.owner.id == userID, ) .toList(); @@ -101,7 +100,7 @@ extension HiddenService on CollectionsService { collectionIDToCollections.values.firstWhereOrNull( (element) => element.type == CollectionType.uncategorized && - element.owner!.id == userID, + element.owner.id == userID, ); if (matchedCollection != null) { cachedUncategorizedCollection = matchedCollection; @@ -166,7 +165,9 @@ extension HiddenService on CollectionsService { await dialog.hide(); } on AssertionError catch (e) { await dialog.hide(); - unawaited(showErrorDialog(context, S.of(context).oops, e.message as String)); + unawaited( + showErrorDialog(context, S.of(context).oops, e.message as String), + ); return false; } catch (e, s) { _logger.severe("Could not hide", e, s); @@ -214,7 +215,7 @@ extension HiddenService on CollectionsService { final encKey = CryptoUtil.encryptSync(uncategorizedCollectionKey, config.getKey()!); final encName = CryptoUtil.encryptSync( - utf8.encode("Uncategorized") as Uint8List, + utf8.encode("Uncategorized"), uncategorizedCollectionKey, ); final collection = await createAndCacheCollection( @@ -240,7 +241,7 @@ extension HiddenService on CollectionsService { final encryptedKeyData = CryptoUtil.encryptSync(collectionKey, config.getKey()!); final encryptedName = CryptoUtil.encryptSync( - utf8.encode(name) as Uint8List, + utf8.encode(name), collectionKey, ); final jsonToUpdate = CollectionMagicMetadata( @@ -249,7 +250,7 @@ extension HiddenService on CollectionsService { ).toJson(); assert(jsonToUpdate.length == 2, "metadata should have two keys"); final encryptedMMd = await CryptoUtil.encryptChaCha( - utf8.encode(jsonEncode(jsonToUpdate)) as Uint8List, + utf8.encode(jsonEncode(jsonToUpdate)), collectionKey, ); final MetadataRequest metadataRequest = MetadataRequest( diff --git a/mobile/lib/services/isolate_functions.dart b/mobile/lib/services/isolate_functions.dart index 08624ef206..c123facf79 100644 --- a/mobile/lib/services/isolate_functions.dart +++ b/mobile/lib/services/isolate_functions.dart @@ -1,7 +1,9 @@ import "dart:io" show File; import 'dart:typed_data' show Uint8List; +import "package:ml_linalg/linalg.dart"; import "package:photos/models/ml/face/box.dart"; +import "package:photos/models/ml/vector.dart"; import "package:photos/services/machine_learning/face_ml/face_clustering/face_clustering_service.dart"; import "package:photos/services/machine_learning/ml_model.dart"; import "package:photos/services/machine_learning/ml_result.dart"; @@ -32,8 +34,11 @@ enum IsolateOperation { /// [MLComputer] runClipText, + /// [MLComputer] + compareEmbeddings, + /// [FaceClusteringService] - linearIncrementalClustering + linearIncrementalClustering, } /// WARNING: Only return primitives unless you know the method is only going @@ -115,6 +120,21 @@ Future isolateFunction( final textEmbedding = await ClipTextEncoder.predict(args); return List.from(textEmbedding, growable: false); + /// MLComputer + case IsolateOperation.compareEmbeddings: + final List embeddings = + (args['embeddings'] as List) + .map((jsonString) => EmbeddingVector.fromJsonString(jsonString)) + .toList(); + final otherEmbedding = + Vector.fromList(args['otherEmbedding'] as List); + final Map result = {}; + for (final embedding in embeddings) { + final double similarity = embedding.vector.dot(otherEmbedding); + result[embedding.fileID] = similarity; + } + return Map.from(result); + /// Cases for MLComputer end here /// Cases for FaceClusteringService start here diff --git a/mobile/lib/services/local/local_sync_util.dart b/mobile/lib/services/local/local_sync_util.dart index 65b5bf2a80..2be4707a22 100644 --- a/mobile/lib/services/local/local_sync_util.dart +++ b/mobile/lib/services/local/local_sync_util.dart @@ -34,17 +34,26 @@ Future, List>> getLocalPathAssetsAndFiles( if (assetsInPath.isEmpty) { result = const Tuple2({}, []); } else { - result = await Computer.shared().compute( - _getLocalIDsAndFilesFromAssets, - param: { - "pathEntity": pathEntity, - "fromTime": fromTime, - "alreadySeenLocalIDs": alreadySeenLocalIDs, - "assetList": assetsInPath, - }, - taskName: - "getLocalPathAssetsAndFiles-${pathEntity.name}-count-${assetsInPath.length}", - ); + try { + result = await Computer.shared().compute( + _getLocalIDsAndFilesFromAssets, + param: { + "pathEntity": pathEntity, + "fromTime": fromTime, + "alreadySeenLocalIDs": alreadySeenLocalIDs, + "assetList": assetsInPath, + }, + taskName: + "getLocalPathAssetsAndFiles-${pathEntity.name}-count-${assetsInPath.length}", + ); + } catch (e) { + _logger.severe("_getLocalIDsAndFilesFromAssets failed", e); + _logger.info( + "Failed for pathEntity: ${pathEntity.name}", + ); + rethrow; + } + alreadySeenLocalIDs.addAll(result.item1); uniqueFiles.addAll(result.item2); } @@ -123,12 +132,10 @@ Future getDiffWithLocal( // current set of assets available on device Set existingIDs, // localIDs of files already imported in app Map> pathToLocalIDs, - Set invalidIDs, ) async { final Map args = {}; args['assets'] = assets; args['existingIDs'] = existingIDs; - args['invalidIDs'] = invalidIDs; args['pathToLocalIDs'] = pathToLocalIDs; final LocalDiffResult diffResult = await Computer.shared().compute( _getLocalAssetsDiff, @@ -147,7 +154,6 @@ Future getDiffWithLocal( LocalDiffResult _getLocalAssetsDiff(Map args) { final List onDeviceLocalPathAsset = args['assets']; final Set existingIDs = args['existingIDs']; - final Set invalidIDs = args['invalidIDs']; final Map> pathToLocalIDs = args['pathToLocalIDs']; final Map> newPathToLocalIDs = >{}; final Map> removedPathToLocalIDs = @@ -179,7 +185,6 @@ LocalDiffResult _getLocalAssetsDiff(Map args) { // End localPathAsset.localIDs.removeAll(existingIDs); - localPathAsset.localIDs.removeAll(invalidIDs); if (localPathAsset.localIDs.isNotEmpty) { unsyncedAssets.add(localPathAsset); } @@ -303,12 +308,8 @@ Future, List>> _getLocalIDsAndFilesFromAssets( (fromTime / ~1000); if (!alreadySeenLocalIDs.contains(entity.id) && assetCreatedOrUpdatedAfterGivenTime) { - try { - final file = await EnteFile.fromAsset(pathEntity.name, entity); - files.add(file); - } catch (e) { - _logger.severe(e); - } + final file = await EnteFile.fromAsset(pathEntity.name, entity); + files.add(file); } } return Tuple2(localIDs, files); diff --git a/mobile/lib/services/local_authentication_service.dart b/mobile/lib/services/local_authentication_service.dart index fecca7ac70..3fb2aaf275 100644 --- a/mobile/lib/services/local_authentication_service.dart +++ b/mobile/lib/services/local_authentication_service.dart @@ -3,12 +3,12 @@ import "dart:async"; import 'package:flutter/material.dart'; import 'package:local_auth/local_auth.dart'; import 'package:photos/core/configuration.dart'; +import 'package:photos/ui/notification/toast.dart'; import "package:photos/ui/settings/lock_screen/lock_screen_password.dart"; import "package:photos/ui/settings/lock_screen/lock_screen_pin.dart"; import 'package:photos/ui/tools/app_lock.dart'; import 'package:photos/utils/auth_util.dart'; import 'package:photos/utils/dialog_util.dart'; -import 'package:photos/utils/toast_util.dart'; class LocalAuthenticationService { LocalAuthenticationService._privateConstructor(); diff --git a/mobile/lib/services/local_file_update_service.dart b/mobile/lib/services/local_file_update_service.dart index ce5a9080af..0f9c48dcc0 100644 --- a/mobile/lib/services/local_file_update_service.dart +++ b/mobile/lib/services/local_file_update_service.dart @@ -9,11 +9,8 @@ import 'package:photos/core/errors.dart'; import 'package:photos/db/file_updation_db.dart'; import 'package:photos/db/files_db.dart'; import "package:photos/extensions/list.dart"; -import "package:photos/extensions/stop_watch.dart"; -import "package:photos/models/file/extensions/file_props.dart"; import 'package:photos/models/file/file.dart'; import 'package:photos/models/file/file_type.dart'; -import "package:photos/services/files_service.dart"; import 'package:photos/utils/file_uploader_util.dart'; import 'package:photos/utils/file_util.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -24,13 +21,11 @@ class LocalFileUpdateService { late FileUpdationDB _fileUpdationDB; late SharedPreferences _prefs; late Logger _logger; - final String _iosLivePhotoSizeMigrationDone = 'fm_ios_live_photo_check'; - final String _doneLivePhotoImport = 'fm_import_ios_live_photo_check'; final String _androidMissingGPSImportDone = 'fm_android_missing_gps_import_done'; final String _androidMissingGPSCheckDone = 'fm_android_missing_gps_check_done'; - static int twoHundredKb = 200 * 1024; + final List _oldMigrationKeys = [ 'fm_badCreationTime', 'fm_badCreationTimeCompleted', @@ -40,6 +35,8 @@ class LocalFileUpdateService { 'fm_badLocationMigrationDone', 'fm_ios_live_photo_size', 'fm_import_ios_live_photo_size', + 'fm_ios_live_photo_check', + 'fm_import_ios_live_photo_check', ]; Completer? _existingMigration; @@ -65,9 +62,6 @@ class LocalFileUpdateService { try { await _markFilesWhichAreActuallyUpdated(); _cleanUpOlderMigration().ignore(); - if (!Platform.isAndroid) { - await _handleLivePhotosSizedCheck(); - } if (Platform.isAndroid) { await _androidMissingGPSCheck(); } @@ -95,6 +89,7 @@ class LocalFileUpdateService { 'missingLocationV2', 'badLocationCord', 'livePhotoSize', + 'livePhotoCheck', ]); for (var element in _oldMigrationKeys) { await _prefs.remove(element); @@ -217,181 +212,6 @@ class LocalFileUpdateService { ); } - Future checkLivePhoto(EnteFile file) async { - if (file.localID == null || - file.localID!.isEmpty || - !file.isUploaded || - file.fileType != FileType.livePhoto || - !file.isOwner) { - return; - } - if (_prefs.containsKey(_iosLivePhotoSizeMigrationDone)) { - return; - } - final hasEntry = await _fileUpdationDB.isExisting( - file.localID!, - FileUpdationDB.livePhotoCheck, - ); - if (hasEntry) { - _logger.info('eager checkLivePhoto ${file.tag}'); - await _checkLivePhotoWithLowOrUnknownSize([file.localID!]); - } - } - - Future _handleLivePhotosSizedCheck() async { - try { - if (_prefs.containsKey(_iosLivePhotoSizeMigrationDone)) { - return; - } - await _importLivePhotoReUploadCandidates(); - - // singleRunLimit indicates number of files to check during single - // invocation of this method. The limit act as a crude way to limit the - // resource consumed by the method - const int singleRunLimit = 500; - final localIDsToProcess = - await _fileUpdationDB.getLocalIDsForPotentialReUpload( - singleRunLimit, - FileUpdationDB.livePhotoCheck, - ); - if (localIDsToProcess.isNotEmpty) { - final chunksOf50 = localIDsToProcess.chunks(50); - for (final chunk in chunksOf50) { - final sTime = DateTime.now().microsecondsSinceEpoch; - final List futures = []; - final chunkOf10 = chunk.chunks(10); - for (final smallChunk in chunkOf10) { - futures.add(_checkLivePhotoWithLowOrUnknownSize(smallChunk)); - } - await Future.wait(futures); - final eTime = DateTime.now().microsecondsSinceEpoch; - final d = Duration(microseconds: eTime - sTime); - _logger.info( - 'Performed hashCheck for ${chunk.length} livePhoto files ' - 'completed in ${d.inSeconds.toString()} secs', - ); - } - } else { - await _prefs.setBool(_iosLivePhotoSizeMigrationDone, true); - } - } catch (e, s) { - _logger.severe('error while checking livePhotoSize check', e, s); - } - } - - Future _checkLivePhotoWithLowOrUnknownSize( - List localIDsToProcess, - ) async { - final int userID = Configuration.instance.getUserID()!; - final List result = - await FilesDB.instance.getLocalFiles(localIDsToProcess); - final List localFilesForUser = []; - final Set localIDsWithFile = {}; - final Set missingSizeIDs = {}; - final Set processedIDs = {}; - for (EnteFile file in result) { - if (file.ownerID == null || file.ownerID == userID) { - localFilesForUser.add(file); - localIDsWithFile.add(file.localID!); - if (file.isUploaded && file.fileSize == null) { - missingSizeIDs.add(file.uploadedFileID!); - } - if (file.isUploaded && file.updationTime == null) { - // file already queued for re-upload - processedIDs.add(file.localID!); - } - } - } - if (missingSizeIDs.isNotEmpty) { - await FilesService.instance.backFillSizes(missingSizeIDs.toList()); - _logger.info('sizes back fill for ${missingSizeIDs.length} files'); - // return early, let the check run in the next batch - return; - } - - // if a file for localID doesn't exist, then mark it as processed - // otherwise the app will be stuck in retrying same set of ids - - for (String localID in localIDsToProcess) { - if (!localIDsWithFile.contains(localID)) { - processedIDs.add(localID); - } - } - _logger.info(" check ${localIDsToProcess.length} files for livePhotoSize, " - "missing file cnt ${processedIDs.length}"); - - for (EnteFile file in localFilesForUser) { - if (file.fileSize == null) { - _logger.info('fileSize still null, skip this file'); - continue; - } else if (file.fileType != FileType.livePhoto) { - _logger.severe('fileType is not livePhoto, skip this file'); - processedIDs.add(file.localID!); - continue; - } - if (processedIDs.contains(file.localID)) { - continue; - } - try { - late MediaUploadData uploadData; - late int mediaUploadSize; - (uploadData, mediaUploadSize) = await getUploadDataWithSizeSize(file); - if ((file.fileSize! - mediaUploadSize).abs() > twoHundredKb) { - _logger.info( - 'Re-upload livePhoto localHash ${uploadData.hashData?.fileHash ?? "null"} & localSize: $mediaUploadSize' - ' and remoteHash ${file.hash ?? "null"} & removeSize: ${file.fileSize!}', - ); - await FilesDB.instance.markFilesForReUpload( - userID, - file.localID!, - file.title, - file.location, - file.creationTime!, - file.modificationTime!, - file.fileType, - ); - } - processedIDs.add(file.localID!); - } on InvalidFileError catch (e) { - if (e.reason == InvalidReason.livePhotoToImageTypeChanged || - e.reason == InvalidReason.imageToLivePhotoTypeChanged) { - // let existing file update check handle this case - _fileUpdationDB.insertMultiple( - [file.localID!], - FileUpdationDB.modificationTimeUpdated, - ).ignore(); - } else { - _logger.severe("livePhoto check failed: invalid file ${file.tag}", e); - } - processedIDs.add(file.localID!); - } catch (e) { - _logger.severe("livePhoto check failed", e); - } finally {} - } - _logger.info('completed check for ${localIDsToProcess.length} files'); - await _fileUpdationDB.deleteByLocalIDs( - processedIDs.toList(), - FileUpdationDB.livePhotoCheck, - ); - } - - Future _importLivePhotoReUploadCandidates() async { - if (_prefs.containsKey(_doneLivePhotoImport)) { - return; - } - _logger.info('_importLivePhotoReUploadCandidates'); - final EnteWatch watch = EnteWatch("_importLivePhotoReUploadCandidates"); - final int ownerID = Configuration.instance.getUserID()!; - final List localIDs = - await FilesDB.instance.getLivePhotosForUser(ownerID); - await _fileUpdationDB.insertMultiple( - localIDs, - FileUpdationDB.livePhotoCheck, - ); - watch.log("imported ${localIDs.length} files"); - await _prefs.setBool(_doneLivePhotoImport, true); - } - //#region Android Missing GPS specific methods ### Future _androidMissingGPSCheck() async { diff --git a/mobile/lib/services/location_service.dart b/mobile/lib/services/location_service.dart index 1e2a181828..cd00ac4c85 100644 --- a/mobile/lib/services/location_service.dart +++ b/mobile/lib/services/location_service.dart @@ -18,6 +18,8 @@ import "package:photos/service_locator.dart"; import "package:photos/services/remote_assets_service.dart"; import "package:shared_preferences/shared_preferences.dart"; +const double earthRadius = 6371; // Earth's radius in kilometers + class LocationService { final SharedPreferences prefs; final Logger _logger = Logger((LocationService).toString()); @@ -82,6 +84,19 @@ class LocationService { return result; } + /// WARNING: This method does not use computer, consider using [getFilesInCity] instead + Map> getFilesInCitySync( + List allFiles, + ) { + if (allFiles.isEmpty) reloadLocationDiscoverySection = true; + final result = getCityResults({ + "query": '', + "cities": _cities, + "files": allFiles, + }); + return result; + } + Future>> getLocationTags() { return _getStoredLocationTags(); } @@ -344,6 +359,26 @@ bool isFileInsideLocationTag( return false; } +double calculateDistance(Location point1, Location point2) { + final lat1 = point1.latitude! * (pi / 180); + final lat2 = point2.latitude! * (pi / 180); + final long1 = point1.longitude! * (pi / 180); + final long2 = point2.longitude! * (pi / 180); + + // Difference in latitude and longitude + final dLat = lat2 - lat1; + final dLong = long2 - long1; + + // Haversine formula + final a = sin(dLat / 2) * sin(dLat / 2) + + cos(lat1) * cos(lat2) * sin(dLong / 2) * sin(dLong / 2); + + // Angular distance in radians + final c = 2 * atan2(sqrt(a), sqrt(1 - a)); + + return earthRadius * c; // Distance in kilometers +} + ///The area bounded by the location tag becomes more elliptical with increase ///in the magnitude of the latitude on the caritesian plane. When latitude is ///0 degrees, the ellipse is a circle with a = b = r. When latitude incrases, diff --git a/mobile/lib/services/machine_learning/face_ml/face_detection/detection.dart b/mobile/lib/services/machine_learning/face_ml/face_detection/detection.dart index dbe2055eb6..f7a7e630e2 100644 --- a/mobile/lib/services/machine_learning/face_ml/face_detection/detection.dart +++ b/mobile/lib/services/machine_learning/face_ml/face_detection/detection.dart @@ -57,7 +57,7 @@ class FaceDetectionRelative extends Detection { List get rightMouth => allKeypoints[4]; FaceDetectionRelative({ - required double score, + required super.score, required List box, required List> allKeypoints, }) : assert( @@ -75,8 +75,7 @@ class FaceDetectionRelative extends Detection { (sublist) => List.from(sublist.map((e) => e.clamp(0.0, 1.0))), ) - .toList(), - super(score: score); + .toList(); void correctForMaintainedAspectRatio( Dimensions originalSize, @@ -252,10 +251,10 @@ class FaceDetectionAbsolute extends Detection { List get rightMouth => allKeypoints[4]; FaceDetectionAbsolute({ - required double score, + required super.score, required this.box, required this.allKeypoints, - }) : super(score: score); + }); @override String toString() { 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 bca190786b..69ba7bfde9 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,6 +4,7 @@ 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"; @@ -483,37 +484,48 @@ class PersonService { } } - Future getRecentFileOfPerson( + Future getThumbnailFileOfPerson( 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; + 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}"); + } } } - } - if (resultFile == null) { - debugPrint( - "Person ${kDebugMode ? person.data.name : person.remoteID} has no files", + + final clustersToFiles = + await SearchService.instance.getClusterFilesForPersonID( + person.remoteID, ); - return EnteFile(); + + 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; } - return resultFile; } } diff --git a/mobile/lib/services/machine_learning/ml_computer.dart b/mobile/lib/services/machine_learning/ml_computer.dart index bce51b674a..3301f6b7c6 100644 --- a/mobile/lib/services/machine_learning/ml_computer.dart +++ b/mobile/lib/services/machine_learning/ml_computer.dart @@ -2,7 +2,9 @@ import 'dart:async'; import 'dart:typed_data' show Uint8List; import "package:logging/logging.dart"; +import "package:ml_linalg/linalg.dart"; import "package:photos/models/ml/face/box.dart"; +import "package:photos/models/ml/vector.dart"; import "package:photos/services/isolate_functions.dart"; import "package:photos/services/isolate_service.dart"; import "package:photos/services/machine_learning/semantic_search/clip/clip_text_encoder.dart"; @@ -64,6 +66,19 @@ class MLComputer extends SuperIsolate { } } + Future> compareEmbeddings(List embeddings, Vector otherEmbedding) async { + try { + final fileIdToSimilarity = await runInIsolate(IsolateOperation.compareEmbeddings, { + "embeddings": embeddings.map((e) => e.toJsonString()).toList(), + "otherEmbedding": otherEmbedding.toList(), + }) as Map; + return fileIdToSimilarity; + } catch (e, s) { + _logger.severe("Could not compare embeddings MLComputer isolate", e, s); + rethrow; + } + } + Future _ensureLoadedClipTextModel() async { return _initModelLock.synchronized(() async { if (ClipTextEncoder.instance.isInitialized) return; diff --git a/mobile/lib/services/machine_learning/ml_indexing_isolate.dart b/mobile/lib/services/machine_learning/ml_indexing_isolate.dart index 11e5375a61..52da789eb7 100644 --- a/mobile/lib/services/machine_learning/ml_indexing_isolate.dart +++ b/mobile/lib/services/machine_learning/ml_indexing_isolate.dart @@ -193,6 +193,28 @@ class MLIndexingIsolate extends SuperIsolate { } } + /// WARNING: This method is only for debugging purposes. It should not be used in production. + Future debugLoadSingleModel(MLModels model) { + return _initModelLock.synchronized(() async { + final modelInstance = model.model; + if (modelInstance.isInitialized) { + _logger.info("Model ${model.name} already loaded"); + return; + } + final modelName = modelInstance.modelName; + final modelPath = await modelInstance.downloadModelSafe(); + if (modelPath == null) { + _logger.severe("Could not download model, no wifi"); + return; + } + final address = await runInIsolate(IsolateOperation.loadModel, { + "modelName": modelName, + "modelPath": modelPath, + }) as int; + modelInstance.storeSessionAddress(address); + }); + } + Future cleanupLocalIndexingModels({bool delete = false}) async { if (!areModelsDownloaded) return; await _releaseModels(); diff --git a/mobile/lib/services/machine_learning/ml_service.dart b/mobile/lib/services/machine_learning/ml_service.dart index d6dcdb3bfc..696adb074b 100644 --- a/mobile/lib/services/machine_learning/ml_service.dart +++ b/mobile/lib/services/machine_learning/ml_service.dart @@ -21,7 +21,6 @@ import "package:photos/services/machine_learning/face_ml/person/person_service.d import "package:photos/services/machine_learning/ml_indexing_isolate.dart"; import 'package:photos/services/machine_learning/ml_result.dart'; import "package:photos/services/machine_learning/semantic_search/semantic_search_service.dart"; -import "package:photos/services/user_remote_flag_service.dart"; import "package:photos/utils/ml_util.dart"; import "package:photos/utils/network_util.dart"; import "package:photos/utils/ram_check_util.dart"; @@ -58,8 +57,7 @@ class MLService { /// Only call this function once at app startup, after that you can directly call [runAllML] Future init() async { if (_isInitialized) return; - if (!userRemoteFlagService - .getCachedBoolValue(UserRemoteFlagService.mlEnabled)) { + if (!flagService.hasGrantedMLConsent) { return; } _logger.info("init called"); @@ -74,8 +72,7 @@ class MLService { // Listen on MachineLearningController Bus.instance.on().listen((event) { - if (!userRemoteFlagService - .getCachedBoolValue(UserRemoteFlagService.mlEnabled)) { + if (!flagService.hasGrantedMLConsent) { return; } @@ -143,6 +140,8 @@ class MLService { if (_mlControllerStatus == true) { // refresh discover section magicCacheService.updateCache(forced: force).ignore(); + // refresh memories section + memoriesCacheService.updateCache(forced: force).ignore(); } if (canFetch()) { await fetchAndIndexAllImages(); @@ -153,6 +152,8 @@ class MLService { if (_mlControllerStatus == true) { // refresh discover section magicCacheService.updateCache().ignore(); + // refresh memories section + memoriesCacheService.updateCache(forced: force).ignore(); } } catch (e, s) { _logger.severe("runAllML failed", e, s); diff --git a/mobile/lib/services/machine_learning/semantic_search/semantic_search_service.dart b/mobile/lib/services/machine_learning/semantic_search/semantic_search_service.dart index df56796cd1..23759af721 100644 --- a/mobile/lib/services/machine_learning/semantic_search/semantic_search_service.dart +++ b/mobile/lib/services/machine_learning/semantic_search/semantic_search_service.dart @@ -21,7 +21,6 @@ import "package:photos/services/collections_service.dart"; import "package:photos/services/machine_learning/ml_computer.dart"; import "package:photos/services/machine_learning/ml_result.dart"; import "package:photos/services/machine_learning/semantic_search/clip/clip_image_encoder.dart"; -import "package:photos/services/user_remote_flag_service.dart"; import "package:shared_preferences/shared_preferences.dart"; class SemanticSearchService { @@ -48,8 +47,7 @@ class SemanticSearchService { _logger.info("Initialized already"); return; } - final hasGivenConsent = userRemoteFlagService - .getCachedBoolValue(UserRemoteFlagService.mlEnabled); + final hasGivenConsent = flagService.hasGrantedMLConsent; if (!hasGivenConsent) return; _logger.info("init called"); @@ -57,7 +55,7 @@ class SemanticSearchService { // call getClipEmbeddings after 5 seconds Future.delayed(const Duration(seconds: 5), () async { - await getClipVectors(); + await _getClipVectors(); }); Bus.instance.on().listen((event) { _cachedImageEmbeddingVectors = null; @@ -67,9 +65,7 @@ class SemanticSearchService { } bool isMagicSearchEnabledAndReady() { - return userRemoteFlagService - .getCachedBoolValue(UserRemoteFlagService.mlEnabled) && - _textModelIsLoaded; + return flagService.hasGrantedMLConsent && _textModelIsLoaded; } // searchScreenQuery should only be used for the user initiate query on the search screen. @@ -78,7 +74,7 @@ class SemanticSearchService { if (!isMagicSearchEnabledAndReady()) { if (flagService.internalUser) { _logger.info( - "ML global consent: ${userRemoteFlagService.getCachedBoolValue(UserRemoteFlagService.mlEnabled)}, loaded: $_textModelIsLoaded ", + "ML global consent: ${flagService.hasGrantedMLConsent}, loaded: $_textModelIsLoaded ", ); } return (query, []); @@ -112,7 +108,20 @@ class SemanticSearchService { _logger.info("Indexes cleared"); } - Future> getClipVectors() async { + Future> getClipVectorsForFileIDs( + Iterable fileIDs, + ) async { + final embeddings = await _getClipVectors(); + final result = []; + for (final embedding in embeddings) { + if (fileIDs.contains(embedding.fileID)) { + result.add(embedding); + } + } + return result; + } + + Future> _getClipVectors() async { if (_cachedImageEmbeddingVectors != null) { return _cachedImageEmbeddingVectors!; } @@ -245,7 +254,7 @@ class SemanticSearchService { double? minimumSimilarity, }) async { final startTime = DateTime.now(); - final imageEmbeddings = await getClipVectors(); + final imageEmbeddings = await _getClipVectors(); final List queryResults = await _computer.compute( computeBulkSimilarities, param: { diff --git a/mobile/lib/services/magic_cache_service.dart b/mobile/lib/services/magic_cache_service.dart index fc9d02a0a9..70379590c5 100644 --- a/mobile/lib/services/magic_cache_service.dart +++ b/mobile/lib/services/magic_cache_service.dart @@ -23,7 +23,6 @@ import "package:photos/service_locator.dart"; import "package:photos/services/machine_learning/semantic_search/semantic_search_service.dart"; import "package:photos/services/remote_assets_service.dart"; import "package:photos/services/search_service.dart"; -import "package:photos/services/user_remote_flag_service.dart"; import "package:photos/ui/viewer/search/result/magic_result_screen.dart"; import "package:photos/utils/file_util.dart"; import "package:photos/utils/navigation_util.dart"; @@ -203,8 +202,7 @@ class MagicCacheService { return _prefs.getInt(_lastMagicCacheUpdateTime) ?? 0; } - bool get enableDiscover => - userRemoteFlagService.getCachedBoolValue(UserRemoteFlagService.mlEnabled); + bool get enableDiscover => flagService.hasGrantedMLConsent; void queueUpdate(String reason) { _pendingUpdateReason.add(reason); diff --git a/mobile/lib/services/memories_cache_service.dart b/mobile/lib/services/memories_cache_service.dart new file mode 100644 index 0000000000..fdbb362d3f --- /dev/null +++ b/mobile/lib/services/memories_cache_service.dart @@ -0,0 +1,305 @@ +import "dart:async"; +import "dart:io" show File; + +import "package:flutter/foundation.dart" show kDebugMode; +import "package:logging/logging.dart"; +import "package:path_provider/path_provider.dart"; +import "package:photos/core/event_bus.dart"; +import "package:photos/db/memories_db.dart"; +import "package:photos/events/files_updated_event.dart"; +import "package:photos/extensions/stop_watch.dart"; +import "package:photos/models/file/file.dart"; +import "package:photos/models/memories/memories_cache.dart"; +import "package:photos/models/memories/memory.dart"; +import "package:photos/models/memories/smart_memory.dart"; +import "package:photos/service_locator.dart"; +import "package:photos/services/location_service.dart"; +import "package:photos/services/search_service.dart"; +import "package:shared_preferences/shared_preferences.dart"; + +class MemoriesCacheService { + static const _lastMemoriesCacheUpdateTimeKey = "lastMemoriesCacheUpdateTime"; + static const _showAnyMemoryKey = "memories.enabled"; + + /// Delay is for cache update to be done not during app init, during which a + /// lot of other things are happening. + static const _kCacheUpdateDelay = Duration(seconds: 10); + + final SharedPreferences _prefs; + late final Logger _logger = Logger("MemoriesCacheService"); + + final _memoriesDB = MemoriesDB.instance; + + List? _cachedMemories; + bool _shouldUpdate = false; + bool _isUpdateInProgress = false; + + late Map _seenTimes; + + MemoriesCacheService(this._prefs) { + _logger.fine("MemoriesCacheService constructor"); + + Future.delayed(_kCacheUpdateDelay, () { + _checkIfTimeToUpdateCache(); + }); + + unawaited(_memoriesDB.getSeenTimes().then((value) => _seenTimes = value)); + unawaited( + _memoriesDB.clearMemoriesSeenBeforeTime( + DateTime.now() + .subtract(kMemoriesUpdateFrequency) + .microsecondsSinceEpoch, + ), + ); + + Bus.instance.on().where((event) { + return event.type == EventType.deletedFromEverywhere; + }).listen((event) { + if (_cachedMemories == null) return; + final generatedIDs = event.updatedFiles + .where((element) => element.generatedID != null) + .map((e) => e.generatedID!) + .toSet(); + for (final memory in _cachedMemories!) { + memory.memories + .removeWhere((m) => generatedIDs.contains(m.file.generatedID)); + } + }); + } + + Future _resetLastMemoriesCacheUpdateTime() async { + await _prefs.setInt( + _lastMemoriesCacheUpdateTimeKey, + DateTime.now().microsecondsSinceEpoch, + ); + } + + int get lastMemoriesCacheUpdateTime { + return _prefs.getInt(_lastMemoriesCacheUpdateTimeKey) ?? 0; + } + + bool get showAnyMemories { + return _prefs.getBool(_showAnyMemoryKey) ?? true; + } + + bool get enableSmartMemories => flagService.showSmartMemories; + + Future _checkIfTimeToUpdateCache() async { + if (lastMemoriesCacheUpdateTime < + DateTime.now() + .subtract(kMemoriesUpdateFrequency) + .microsecondsSinceEpoch) { + _shouldUpdate = true; + } + } + + Future _getCachePath() async { + return (await getApplicationSupportDirectory()).path + + "/cache/test3/memories_cache"; + // TODO: lau: remove the test1 directory after testing + } + + Future markMemoryAsSeen(Memory memory) async { + memory.markSeen(); + await _memoriesDB.markMemoryAsSeen( + memory, + DateTime.now().microsecondsSinceEpoch, + ); + if (_cachedMemories != null && memory.file.generatedID != null) { + final generatedID = memory.file.generatedID!; + for (final smartMemory in _cachedMemories!) { + for (final mem in smartMemory.memories) { + if (mem.file.generatedID == generatedID) { + mem.markSeen(); + } + } + } + } + } + + Future updateCache({bool forced = false}) async { + if (!showAnyMemories || !enableSmartMemories) { + return; + } + try { + if ((!_shouldUpdate && !forced) || _isUpdateInProgress) { + _logger.info( + "No update needed as shouldUpdate: $_shouldUpdate, forced: $forced and isUpdateInProgress $_isUpdateInProgress", + ); + return; + } + _logger.info("updating memories cache"); + _isUpdateInProgress = true; + final EnteWatch? w = + kDebugMode ? EnteWatch("MemoriesCacheService") : null; + w?.start(); + final oldCache = await _readCacheFromDisk(); + w?.log("gotten old cache"); + final MemoriesCache newCache = _processOldCache(oldCache); + w?.log("processed old cache"); + // calculate memories for this period and for the next period + final now = DateTime.now(); + final next = now.add(kMemoriesUpdateFrequency); + final nowMemories = + await smartMemoriesService.calcMemories(now, newCache); + final nextMemories = + await smartMemoriesService.calcMemories(next, newCache); + w?.log("calculated new memories"); + for (final nowMemory in nowMemories) { + newCache.toShowMemories + .add(ToShowMemory.fromSmartMemory(nowMemory, now)); + } + for (final nextMemory in nextMemories) { + newCache.toShowMemories + .add(ToShowMemory.fromSmartMemory(nextMemory, next)); + } + w?.log("added memories to cache"); + final file = File(await _getCachePath()); + if (!file.existsSync()) { + file.createSync(recursive: true); + } + _cachedMemories = + nowMemories.where((memory) => memory.shouldShowNow()).toList(); + await file.writeAsBytes( + MemoriesCache.encodeToJsonString(newCache).codeUnits, + ); + w?.log("cacheWritten"); + await _resetLastMemoriesCacheUpdateTime(); + w?.logAndReset('done'); + _shouldUpdate = false; + } catch (e, s) { + _logger.info("Error updating memories cache", e, s); + } finally { + _isUpdateInProgress = false; + } + } + + MemoriesCache _processOldCache(MemoriesCache? oldCache) { + final List toShowMemories = []; + final List peopleShownLogs = []; + final List tripsShownLogs = []; + if (oldCache != null) { + final now = DateTime.now(); + for (final peopleLog in oldCache.peopleShownLogs) { + if (now.difference( + DateTime.fromMicrosecondsSinceEpoch(peopleLog.lastTimeShown), + ) < + maxShowTimeout) { + peopleShownLogs.add(peopleLog); + } + } + for (final tripsLog in oldCache.tripsShownLogs) { + if (now.difference( + DateTime.fromMicrosecondsSinceEpoch(tripsLog.lastTimeShown), + ) < + maxShowTimeout) { + tripsShownLogs.add(tripsLog); + } + } + for (final oldMemory in oldCache.toShowMemories) { + if (oldMemory.isOld) { + if (oldMemory.type == MemoryType.people) { + if (!peopleShownLogs.any( + (person) => + (person.personID == oldMemory.personID) && + (person.peopleMemoryType == oldMemory.peopleMemoryType), + )) { + peopleShownLogs.add(PeopleShownLog.fromOldCacheMemory(oldMemory)); + } + } else if (oldMemory.type == MemoryType.trips) { + if (!tripsShownLogs.any( + (trip) => isFileInsideLocationTag( + oldMemory.location!, + trip.location, + 10.0, + ), + )) { + tripsShownLogs.add(TripsShownLog.fromOldCacheMemory(oldMemory)); + } + } + } + } + } + return MemoriesCache( + toShowMemories: toShowMemories, + peopleShownLogs: peopleShownLogs, + tripsShownLogs: tripsShownLogs, + ); + } + + Future> _fromCacheToMemories(MemoriesCache cache) async { + try { + _logger.info('Processing disk cache memories to smart memories'); + final List memories = []; + final allFiles = Set.from( + await SearchService.instance.getAllFilesForSearch(), + ); + final allFileIdsToFile = {}; + for (final file in allFiles) { + if (file.uploadedFileID != null) { + allFileIdsToFile[file.uploadedFileID!] = file; + } + } + + for (final ToShowMemory memory in cache.toShowMemories) { + if (memory.shouldShowNow()) { + memories.add( + SmartMemory( + memory.fileUploadedIDs + .map( + (fileID) => + Memory.fromFile(allFileIdsToFile[fileID]!, _seenTimes), + ) + .toList(), + memory.type, + memory.title, + memory.firstTimeToShow, + memory.lastTimeToShow, + ), + ); + } + } + _logger.info('Processing of disk cache memories done'); + return memories; + } catch (e, s) { + _logger.severe("Error converting cache to memories", e, s); + return []; + } + } + + Future> _getMemoriesFromCache() async { + final cache = await _readCacheFromDisk(); + if (cache == null) { + return []; + } + final result = await _fromCacheToMemories(cache); + return result; + } + + Future> getMemories(int? limit) async { + if (!showAnyMemories) { + return []; + } + if (_cachedMemories != null) { + return _cachedMemories!; + } + _cachedMemories = await _getMemoriesFromCache(); + return _cachedMemories!; + } + + Future _readCacheFromDisk() async { + _logger.info("Reading memories cache result from disk"); + final file = File(await _getCachePath()); + if (!file.existsSync()) { + _logger.info("No memories cache found"); + return null; + } + final jsonString = file.readAsStringSync(); + return MemoriesCache.decodeFromJsonString(jsonString); + } + + Future clearMemoriesCache() async { + await File(await _getCachePath()).delete(); + _cachedMemories = null; + } +} diff --git a/mobile/lib/services/memories_service.dart b/mobile/lib/services/memories_service.dart index 6461131289..50e63b24b0 100644 --- a/mobile/lib/services/memories_service.dart +++ b/mobile/lib/services/memories_service.dart @@ -7,7 +7,7 @@ import 'package:photos/db/memories_db.dart'; import "package:photos/events/files_updated_event.dart"; import "package:photos/events/memories_setting_changed.dart"; import 'package:photos/models/filters/important_items_filter.dart'; -import 'package:photos/models/memory.dart'; +import 'package:photos/models/memories/memory.dart'; import "package:photos/models/metadata/common_keys.dart"; import 'package:photos/services/collections_service.dart'; import "package:shared_preferences/shared_preferences.dart"; diff --git a/mobile/lib/services/notification_service.dart b/mobile/lib/services/notification_service.dart index b99c3bcb0f..86cabfe014 100644 --- a/mobile/lib/services/notification_service.dart +++ b/mobile/lib/services/notification_service.dart @@ -2,7 +2,7 @@ import 'dart:io'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import "package:logging/logging.dart"; -import "package:photos/services/remote_sync_service.dart"; +import "package:photos/services/sync/remote_sync_service.dart"; import "package:shared_preferences/shared_preferences.dart"; class NotificationService { diff --git a/mobile/lib/services/preview_video_store.dart b/mobile/lib/services/preview_video_store.dart index 37baf778d6..0e97983bb5 100644 --- a/mobile/lib/services/preview_video_store.dart +++ b/mobile/lib/services/preview_video_store.dart @@ -20,6 +20,7 @@ import "package:photos/core/configuration.dart"; import "package:photos/core/event_bus.dart"; import "package:photos/core/network/network.dart"; import 'package:photos/db/files_db.dart'; +import "package:photos/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"; @@ -30,16 +31,22 @@ 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/ui/notification/toast.dart"; import "package:photos/utils/exif_util.dart"; import "package:photos/utils/file_key.dart"; import "package:photos/utils/file_util.dart"; import "package:photos/utils/gzip.dart"; -import "package:photos/utils/toast_util.dart"; +import "package:photos/utils/network_util.dart"; import "package:shared_preferences/shared_preferences.dart"; +const _maxRetryCount = 3; + class PreviewVideoStore { final LinkedHashMap _items = LinkedHashMap(); LinkedHashMap get previews => _items; + Set? _failureFiles; + + bool _initSuccess = false; PreviewVideoStore._privateConstructor(); @@ -50,18 +57,13 @@ class PreviewVideoStore { final cacheManager = DefaultCacheManager(); final videoCacheManager = VideoCacheManager.instance; - LinkedHashSet files = LinkedHashSet(); + LinkedHashSet fileQueue = LinkedHashSet(); int uploadingFileId = -1; final _dio = NetworkClient.instance.enteDio; void init(SharedPreferences prefs) { _prefs = prefs; - - Future.delayed( - const Duration(seconds: 10), - PreviewVideoStore.instance.putFilesForPreviewCreation, - ); } late final SharedPreferences _prefs; @@ -74,24 +76,27 @@ class PreviewVideoStore { Future setIsVideoStreamingEnabled(bool value) async { final oneMonthBack = DateTime.now().subtract(const Duration(days: 30)); - await _prefs.setBool(_videoStreamingEnabled, value); - await _prefs.setInt( - _videoStreamingCutoff, - oneMonthBack.millisecondsSinceEpoch, - ); + _prefs.setBool(_videoStreamingEnabled, value).ignore(); + _prefs + .setInt( + _videoStreamingCutoff, + oneMonthBack.millisecondsSinceEpoch, + ) + .ignore(); Bus.instance.fire(VideoStreamingChanged()); if (isVideoStreamingEnabled) { - putFilesForPreviewCreation().ignore(); + await FileDataService.instance.syncFDStatus(); + _putFilesForPreviewCreation().ignore(); } else { clearQueue(); } } - clearQueue() { + void clearQueue() { + fileQueue.clear(); _items.clear(); Bus.instance.fire(PreviewUpdatedEvent(_items)); - files.clear(); } DateTime? get videoStreamingCutoff { @@ -110,48 +115,45 @@ class PreviewVideoStore { return; } + Object? error; + bool removeFile = false; try { - if (!enteFile.isUploaded) return; - final file = await getFile(enteFile, isOrigin: true); - if (file == null) return; + if (!enteFile.isUploaded) { + removeFile = true; + return; + } try { // check if playlist already exist await getPlaylist(enteFile); - final resultUrl = await getPreviewUrl(enteFile); + final _ = await getPreviewUrl(enteFile); + if (ctx != null && ctx.mounted) { showShortToast(ctx, 'Video preview already exists'); } - debugPrint("previewUrl $resultUrl"); - _items.removeWhere((key, value) => value.file == enteFile); - Bus.instance.fire(PreviewUpdatedEvent(_items)); + removeFile = true; return; } catch (e, s) { - if (e is DioError && e.response?.statusCode == 404) { + if (e is DioException && e.response?.statusCode == 404) { _logger.info("No preview found for $enteFile"); } else { _logger.warning("Failed to get playlist for $enteFile", e, s); - rethrow; - } - } - - final fileSize = file.lengthSync(); - FFProbeProps? props; - - if (fileSize <= 10 * 1024 * 1024) { - props = await getVideoPropsAsync(file); - final videoData = List.from(props?.propData?["streams"] ?? []) - .firstWhereOrNull((e) => e["type"] == "video"); - - final codec = videoData["codec_name"]?.toString().toLowerCase(); - final codecIsH264 = codec?.contains("h264") ?? false; - if (codecIsH264) { - _items.removeWhere((key, value) => value.file == enteFile); - Bus.instance.fire(PreviewUpdatedEvent(_items)); + error = e; return; } } + + // elimination case for <=10 MB with H.264 + var (props, result, file) = await _checkFileForPreviewCreation(enteFile); + if (result) { + removeFile = true; + return; + } + + // check if there is already a preview in processing if (uploadingFileId >= 0) { + if (uploadingFileId == enteFile.uploadedFileID) return; + _items[enteFile.uploadedFileID!] = PreviewItem( status: PreviewItemStatus.inQueue, file: enteFile, @@ -161,9 +163,11 @@ class PreviewVideoStore { collectionID: enteFile.collectionID ?? 0, ); Bus.instance.fire(PreviewUpdatedEvent(_items)); - files.add(enteFile); + fileQueue.add(enteFile); return; } + + // everything is fine, let's process uploadingFileId = enteFile.uploadedFileID!; _items[enteFile.uploadedFileID!] = PreviewItem( status: PreviewItemStatus.compressing, @@ -174,16 +178,31 @@ class PreviewVideoStore { ); Bus.instance.fire(PreviewUpdatedEvent(_items)); + // get file + file ??= await getFile(enteFile, isOrigin: true); + if (file == null) { + error = "Unable to fetch file"; + return; + } + + // check metadata for bitrate, codec, color space props ??= await getVideoPropsAsync(file); + final fileSize = enteFile.fileSize ?? file.lengthSync(); final videoData = List.from(props?.propData?["streams"] ?? []) .firstWhereOrNull((e) => e["type"] == "video"); final codec = videoData["codec_name"]?.toString().toLowerCase(); + final codecIsH264 = codec?.contains("h264") ?? false; + final bitrate = props?.duration?.inSeconds != null ? (fileSize * 8) / props!.duration!.inSeconds : null; + final colorSpace = videoData["color_space"]?.toString().toLowerCase(); + final isColorGood = colorSpace == "bt709"; + + // create temp file & directory for preview generation final String tempDir = Configuration.instance.getTempDirectory(); final String prefix = "${tempDir}_${enteFile.uploadedFileID}_${newID("pv")}"; @@ -197,72 +216,65 @@ class PreviewVideoStore { final keyinfo = File('$prefix/mykey.keyinfo'); keyinfo.writeAsStringSync("data:text/plain;base64,${key.base64}\n" "${keyfile.path}\n"); + _logger.info( 'Generating HLS Playlist ${enteFile.displayName} at $prefix/output.m3u8}', ); FFmpegSession? session; - final colorSpace = videoData["color_space"]?.toString().toLowerCase(); - final isColorGood = colorSpace == "bt709"; - final codecIsH264 = codec?.contains("h264") ?? false; + // case 1, if it's already a good stream if (bitrate != null && bitrate <= 4000 * 1000 && codecIsH264) { - // create playlist without compression, as is session = await FFmpegKit.execute( '-i "${file.path}" ' - '-metadata:s:v:0 rotate=0 ' // Adjust metadata if needed - '-c:v copy ' // Copy the original video codec - '-c:a copy ' // Copy the original audio codec - '-f hls -hls_time 10 -hls_flags single_file ' + '-c:v copy -c:a copy ' + '-f hls -hls_time 2 -hls_flags single_file ' '-hls_list_size 0 -hls_key_info_file ${keyinfo.path} ' '$prefix/output.m3u8', ); - } else if (bitrate != null && + } // case 2, if it's bitrate is good, but codec is not + else if (bitrate != null && codec != null && bitrate <= 2000 * 1000 && !codecIsH264) { - // compress video with crf=21, h264 no change in resolution or frame rate, - // just change color scheme session = await FFmpegKit.execute( '-i "${file.path}" ' - '-metadata:s:v:0 rotate=0 ' // Keep rotation metadata - '-vf "format=yuv420p10le,zscale=transfer=linear,tonemap=tonemap=hable:desat=0:peak=10,zscale=transfer=bt709:matrix=bt709:primaries=bt709,format=yuv420p" ' // Adjust color scheme - '-color_primaries bt709 -color_trc bt709 -colorspace bt709 ' // Set color profile to BT.709 - '-c:v libx264 -crf 21 -preset medium ' // Compress with CRF=21 using H.264 - '-c:a copy ' // Keep original audio - '-f hls -hls_time 10 -hls_flags single_file ' + '-vf "format=yuv420p10le,zscale=transfer=linear,tonemap=tonemap=hable:desat=0:peak=10,zscale=transfer=bt709:matrix=bt709:primaries=bt709,format=yuv420p" ' + '-color_primaries bt709 -color_trc bt709 -colorspace bt709 ' + '-c:v libx264 -crf 23 -preset medium ' + '-c:a copy ' + '-f hls -hls_time 2 -hls_flags single_file ' '-hls_list_size 0 -hls_key_info_file ${keyinfo.path} ' '$prefix/output.m3u8', ); - } - - if (colorSpace != null && isColorGood) { - session ??= await FFmpegKit.execute( + } // case 3, if it's color space is good + else if (colorSpace != null && isColorGood) { + session = await FFmpegKit.execute( '-i "${file.path}" ' - '-metadata:s:v:0 rotate=0 ' '-vf "scale=-2:720,fps=30" ' - '-c:v libx264 -b:v 2000k -preset medium ' - '-c:a aac -b:a 128k -f hls -hls_time 10 -hls_flags single_file ' + '-c:v libx264 -b:v 2000k -crf 23 -preset medium ' + '-c:a aac -b:a 128k -f hls -hls_time 2 -hls_flags single_file ' + '-hls_list_size 0 -hls_key_info_file ${keyinfo.path} ' + '$prefix/output.m3u8', + ); + } // case 4, make it compatible + else { + session = await FFmpegKit.execute( + '-i "${file.path}" ' + '-vf "scale=-2:720,fps=30,format=yuv420p10le,zscale=transfer=linear,tonemap=tonemap=hable:desat=0:peak=10,zscale=transfer=bt709:matrix=bt709:primaries=bt709,format=yuv420p" ' + '-color_primaries bt709 -color_trc bt709 -colorspace bt709 ' + '-x264-params "colorprim=bt709:transfer=bt709:colormatrix=bt709" ' + '-c:v libx264 -b:v 2000k -crf 23 -preset medium ' + '-c:a aac -b:a 128k -f hls -hls_time 2 -hls_flags single_file ' '-hls_list_size 0 -hls_key_info_file ${keyinfo.path} ' '$prefix/output.m3u8', ); } - session ??= await FFmpegKit.execute( - '-i "${file.path}" ' - '-metadata:s:v:0 rotate=0 ' - '-vf "scale=-2:720,fps=30,format=yuv420p10le,zscale=transfer=linear,tonemap=tonemap=hable:desat=0:peak=10,zscale=transfer=bt709:matrix=bt709:primaries=bt709,format=yuv420p" ' - '-color_primaries bt709 -color_trc bt709 -colorspace bt709 ' - '-x264-params "colorprim=bt709:transfer=bt709:colormatrix=bt709" ' - '-c:v libx264 -b:v 2000k -preset medium ' - '-c:a aac -b:a 128k -f hls -hls_time 10 -hls_flags single_file ' - '-hls_list_size 0 -hls_key_info_file ${keyinfo.path} ' - '$prefix/output.m3u8', - ); - final returnCode = await session.getReturnCode(); - String? error; + String? objectId; + int? objectSize; if (ReturnCode.isSuccess(returnCode)) { try { @@ -275,14 +287,15 @@ class PreviewVideoStore { Bus.instance.fire(PreviewUpdatedEvent(_items)); _logger.info('Playlist Generated ${enteFile.displayName}'); + final playlistFile = File("$prefix/output.m3u8"); final previewFile = File("$prefix/output.ts"); final result = await _uploadPreviewVideo(enteFile, previewFile); - final String objectID = result.$1; - final objectSize = result.$2; - // Logic to fetch width & height of preview - //-allowed_extensions ALL -i "https://example.com/stream.m3u8" -frames:v 1 -c copy frame.ts + objectId = result.$1; + objectSize = result.$2; + + // Fetch resolution of generated stream by decrypting a single frame final FFmpegSession session2 = await FFmpegKit.execute( '-allowed_extensions ALL -i "$prefix/output.m3u8" -frames:v 1 -c copy "$prefix/frame.ts"', ); @@ -297,14 +310,14 @@ class PreviewVideoStore { width = props2?.width; height = props2?.height; } - } catch (_) { - _logger.warning("Failed to get width and height", _); + } catch (err, sT) { + _logger.warning("Failed to fetch resolution of stream", err, sT); } await _reportVideoPreview( enteFile, playlistFile, - objectID: objectID, + objectId: objectId, objectSize: objectSize, width: width, height: height, @@ -313,7 +326,7 @@ class PreviewVideoStore { _logger.info("Video preview uploaded for $enteFile"); } catch (err, sT) { error = "Failed to upload video preview\nError: $err"; - _logger.shout("Video preview uploaded for $enteFile", err, sT); + _logger.shout("Something went wrong with preview upload", err, sT); } } else if (ReturnCode.isCancel(returnCode)) { _logger.warning("FFmpeg command cancelled"); @@ -324,56 +337,104 @@ class PreviewVideoStore { "FFmpeg command failed with return code $returnCode", output ?? "Error not found", ); - if (kDebugMode) { - _logger.severe(output); - } error = "Failed to generate video preview\nError: $output"; } if (error == null) { - FileDataService.instance.syncFDStatus().ignore(); + // update previewIds + FileDataService.instance.appendPreview( + enteFile.uploadedFileID!, + objectId!, + objectSize!, + ); + _items[enteFile.uploadedFileID!] = PreviewItem( status: PreviewItemStatus.uploaded, file: enteFile, retryCount: _items[enteFile.uploadedFileID!]!.retryCount, collectionID: enteFile.collectionID ?? 0, ); - } else { - if (_items[enteFile.uploadedFileID!]!.retryCount < 3) { - _items[enteFile.uploadedFileID!] = PreviewItem( - status: PreviewItemStatus.retry, - file: enteFile, - retryCount: _items[enteFile.uploadedFileID!]!.retryCount + 1, - collectionID: enteFile.collectionID ?? 0, - ); - files.add(enteFile); - } else { - _items[enteFile.uploadedFileID!] = PreviewItem( - status: PreviewItemStatus.failed, - file: enteFile, - retryCount: _items[enteFile.uploadedFileID!]!.retryCount, - collectionID: enteFile.collectionID ?? 0, - error: error, - ); - } + _removeFromLocks(enteFile).ignore(); + Bus.instance.fire(PreviewUpdatedEvent(_items)); } - 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!) { uploadingFileId = -1; } - if (files.isNotEmpty) { - final file = files.first; - files.remove(file); + _logger.info("[chunk] Processing ${_items.length} items for streaming"); + // process next file + if (fileQueue.isNotEmpty) { + final file = fileQueue.first; + fileQueue.remove(file); await chunkAndUploadVideo(ctx, file); } } } + Future _removeFromLocks(EnteFile enteFile) async { + final bool isFailurePresent = + _failureFiles?.contains(enteFile.uploadedFileID!) ?? false; + + if (isFailurePresent) { + await UploadLocksDB.instance + .deleteStreamUploadErrorEntry(enteFile.uploadedFileID!); + _failureFiles?.remove(enteFile.uploadedFileID!); + } + } + + void _removeFile(EnteFile enteFile) { + _items.remove(enteFile.uploadedFileID!); + } + + void _retryFile(EnteFile enteFile, Object error) { + if (_items[enteFile.uploadedFileID!]!.retryCount < _maxRetryCount) { + _items[enteFile.uploadedFileID!] = PreviewItem( + status: PreviewItemStatus.retry, + file: enteFile, + retryCount: _items[enteFile.uploadedFileID!]!.retryCount + 1, + collectionID: enteFile.collectionID ?? 0, + ); + fileQueue.add(enteFile); + } else { + _items[enteFile.uploadedFileID!] = PreviewItem( + status: PreviewItemStatus.failed, + file: enteFile, + retryCount: _items[enteFile.uploadedFileID!]!.retryCount, + collectionID: enteFile.collectionID ?? 0, + error: error, + ); + + final bool isFailurePresent = + _failureFiles?.contains(enteFile.uploadedFileID!) ?? false; + + if (isFailurePresent) { + UploadLocksDB.instance.appendStreamEntry( + enteFile.uploadedFileID!, + error.toString(), + ); + } else { + UploadLocksDB.instance.appendStreamEntry( + enteFile.uploadedFileID!, + error.toString(), + ); + _failureFiles?.add(enteFile.uploadedFileID!); + } + } + } + Future _reportVideoPreview( EnteFile file, File playlist, { - required String objectID, + required String objectId, required int objectSize, required int? width, required int? height, @@ -396,7 +457,7 @@ class PreviewVideoStore { "/files/video-data", data: { "fileID": file.uploadedFileID!, - "objectID": objectID, + "objectID": objectId, "objectSize": objectSize, "playlist": result.encData, "playlistHeader": result.header, @@ -539,7 +600,7 @@ class PreviewVideoStore { final previewURL = response2.data["url"]; if (objectKey != null) { unawaited( - downloadAndCacheVideo( + _downloadAndCacheVideo( previewURL, _getVideoPreviewKey(objectKey), ), @@ -568,7 +629,7 @@ class PreviewVideoStore { } } - Future downloadAndCacheVideo(String url, String key) async { + Future _downloadAndCacheVideo(String url, String key) async { final file = await videoCacheManager.downloadFile(url, key: key); return file; } @@ -590,39 +651,124 @@ class PreviewVideoStore { } } - // get all files after cutoff date and add it to queue for preview creation - // only run when video streaming is enabled - Future putFilesForPreviewCreation() async { - if (!isVideoStreamingEnabled) return; + Future<(FFProbeProps?, bool, File?)> _checkFileForPreviewCreation( + EnteFile enteFile, + ) async { + final fileSize = enteFile.fileSize; + FFProbeProps? props; + File? file; + bool result = false; + + try { + final isFileUnder10MB = fileSize != null && fileSize <= 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(); + result = codec?.contains("h264") ?? false; + } + } + } catch (e, sT) { + _logger.warning("Failed to check props", e, sT); + } + return (props, result, file); + } + + // generate stream for all files after cutoff date + Future _putFilesForPreviewCreation([bool updateInit = false]) async { + if (!isVideoStreamingEnabled || !await canUseHighBandwidth()) return; final cutoff = videoStreamingCutoff; if (cutoff == null) return; + if (updateInit) _initSuccess = true; + + Map failureFiles = {}; + try { + failureFiles = await UploadLocksDB.instance.getStreamUploadError(); + _failureFiles = {...failureFiles.keys}; + + // handle case when failures are already previewed + for (final failure in _failureFiles!) { + if (previews.containsKey(failure)) { + UploadLocksDB.instance.deleteStreamUploadErrorEntry(failure).ignore(); + } + } + } catch (_) {} final files = await FilesDB.instance.getAllFilesAfterDate( fileType: FileType.video, beginDate: cutoff, + userID: Configuration.instance.getUserID()!, ); final previewIds = FileDataService.instance.previewIds; final allFiles = files .where((file) => previewIds?[file.uploadedFileID] == null) - .toList(); + .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(); - // set all video status to be in queue - for (final file in allFiles) { - _items[file.uploadedFileID!] = PreviewItem( + // 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, + file: enteFile, + collectionID: enteFile.collectionID ?? 0, + retryCount: _maxRetryCount, + error: failureFiles[enteFile.uploadedFileID!], + ); + } + if (result || isFailure) { + allFiles.removeAt(i); + n--; + continue; + } + + _items[enteFile.uploadedFileID!] = PreviewItem( status: PreviewItemStatus.inQueue, - file: file, - collectionID: file.collectionID ?? 0, + file: enteFile, + collectionID: enteFile.collectionID ?? 0, ); + + i++; } + Bus.instance.fire(PreviewUpdatedEvent(_items)); + if (allFiles.isEmpty) { + _logger.info("[init] No preview to cache"); + return; + } - final file = allFiles.first; - allFiles.remove(file); + _logger.info("[init] Processing ${allFiles.length} items for streaming"); - this.files.addAll(allFiles); + // take first file and put it for stream generation + final file = allFiles.removeAt(0); + fileQueue.addAll(allFiles); + chunkAndUploadVideo(null, file).ignore(); + } - await chunkAndUploadVideo(null, file); + void queueFiles() { + if (!_initSuccess) { + _putFilesForPreviewCreation(true).catchError((_) { + _initSuccess = false; + }); + } } } diff --git a/mobile/lib/services/search_service.dart b/mobile/lib/services/search_service.dart index e8747bd350..040f3d5d2c 100644 --- a/mobile/lib/services/search_service.dart +++ b/mobile/lib/services/search_service.dart @@ -1,10 +1,10 @@ +import "dart:async"; import "dart:math"; import "package:flutter/cupertino.dart"; import "package:flutter/material.dart"; import "package:intl/intl.dart"; import 'package:logging/logging.dart'; -import "package:ml_linalg/linalg.dart"; import "package:photos/core/constants.dart"; import 'package:photos/core/event_bus.dart'; import 'package:photos/data/holidays.dart'; @@ -23,6 +23,7 @@ import 'package:photos/models/file/file_type.dart'; import "package:photos/models/local_entity_data.dart"; import "package:photos/models/location/location.dart"; import "package:photos/models/location_tag/location_tag.dart"; +import "package:photos/models/memories/memory.dart"; import "package:photos/models/ml/face/person.dart"; import 'package:photos/models/search/album_search_result.dart'; import 'package:photos/models/search/generic_search_result.dart'; @@ -36,24 +37,22 @@ import "package:photos/models/search/hierarchical/top_level_generic_filter.dart" import "package:photos/models/search/search_constants.dart"; import "package:photos/models/search/search_types.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/filter/db_filters.dart"; import "package:photos/services/location_service.dart"; import "package:photos/services/machine_learning/face_ml/face_filtering/face_filtering_constants.dart"; import "package:photos/services/machine_learning/face_ml/person/person_service.dart"; -import "package:photos/services/machine_learning/ml_computer.dart"; import 'package:photos/services/machine_learning/semantic_search/semantic_search_service.dart'; -import "package:photos/services/user_remote_flag_service.dart"; -import "package:photos/services/user_service.dart"; import "package:photos/states/location_screen_state.dart"; import "package:photos/ui/viewer/location/add_location_sheet.dart"; import "package:photos/ui/viewer/location/location_screen.dart"; import "package:photos/ui/viewer/people/cluster_page.dart"; import "package:photos/ui/viewer/people/people_page.dart"; import "package:photos/ui/viewer/search/result/magic_result_screen.dart"; -import 'package:photos/utils/date_time_util.dart'; import "package:photos/utils/file_util.dart"; import "package:photos/utils/navigation_util.dart"; +import 'package:photos/utils/standalone/date_time.dart'; import 'package:tuple/tuple.dart'; class SearchService { @@ -153,6 +152,7 @@ class SearchService { _cachedFilesForSearch = null; _cachedFilesForHierarchicalSearch = null; _cachedHiddenFilesFuture = null; + unawaited(memoriesCacheService.clearMemoriesCache()); } // getFilteredCollectionsWithThumbnail removes deleted or archived or @@ -247,8 +247,7 @@ class SearchService { Future> getMagicSectionResults( BuildContext context, ) async { - if (userRemoteFlagService - .getCachedBoolValue(UserRemoteFlagService.mlEnabled)) { + if (flagService.hasGrantedMLConsent) { return magicCacheService.getMagicGenericSearchResult(context); } else { return []; @@ -1189,442 +1188,33 @@ class SearchService { return searchResults; } - Future> onThisDayOrWeekResults( + /// For debug purposes only, don't use this in production! + Future> smartMemories( BuildContext context, int? limit, ) async { - final List searchResults = []; - final allFiles = await getAllFilesForSearch(); - if (allFiles.isEmpty) return []; - - final currentTime = DateTime.now().toLocal(); - final currentDayMonth = currentTime.month * 100 + currentTime.day; - final currentWeek = _getWeekNumber(currentTime); - final currentMonth = currentTime.month; - final cutOffTime = currentTime.subtract(const Duration(days: 365)); - final averageDailyPhotos = allFiles.length / 365; - final significantDayThreshold = averageDailyPhotos * 0.25; - final significantWeekThreshold = averageDailyPhotos * 0.40; - - // Group files by day-month and year - final dayMonthYearGroups = >>{}; - - for (final file in allFiles) { - if (file.creationTime! > cutOffTime.microsecondsSinceEpoch) continue; - - final creationTime = - DateTime.fromMicrosecondsSinceEpoch(file.creationTime!); - final dayMonth = creationTime.month * 100 + creationTime.day; - final year = creationTime.year; - - dayMonthYearGroups - .putIfAbsent(dayMonth, () => {}) - .putIfAbsent(year, () => []) - .add(file); - } - - // Process each nearby day-month to find significant days - for (final dayMonth in dayMonthYearGroups.keys) { - final dayDiff = dayMonth - currentDayMonth; - if (dayDiff < 0 || dayDiff > 2) continue; - // TODO: lau: this doesn't cover month changes properly - - final yearGroups = dayMonthYearGroups[dayMonth]!; - final significantDays = yearGroups.entries - .where((e) => e.value.length > significantDayThreshold) - .map((e) => e.key) - .toList(); - - if (significantDays.length >= 3) { - // Combine all years for this day-month - final date = - DateTime(currentTime.year, dayMonth ~/ 100, dayMonth % 100); - final allPhotos = yearGroups.values.expand((x) => x).toList(); - final photoSelection = await _bestSelection(allPhotos); - - searchResults.add( - GenericSearchResult( - ResultType.event, - "${DateFormat('MMMM d').format(date)} through the years", - photoSelection, - hierarchicalSearchFilter: TopLevelGenericFilter( - filterName: DateFormat('MMMM d').format(date), - occurrence: kMostRelevantFilter, - filterResultType: ResultType.event, - matchedUploadedIDs: filesToUploadedFileIDs(photoSelection), - filterIcon: Icons.event_outlined, - ), + final memories = await memoriesCacheService.getMemories(limit); + final searchResults = []; + for (final memory in memories) { + final files = Memory.filesFromMemories(memory.memories); + searchResults.add( + GenericSearchResult( + ResultType.event, + memory.title, + files, + hierarchicalSearchFilter: TopLevelGenericFilter( + filterName: memory.title, + occurrence: kMostRelevantFilter, + filterResultType: ResultType.event, + matchedUploadedIDs: filesToUploadedFileIDs(files), + filterIcon: Icons.event_outlined, ), - ); - } else { - // Individual entries for significant years - for (final year in significantDays) { - final date = DateTime(year, dayMonth ~/ 100, dayMonth % 100); - final files = yearGroups[year]!; - final photoSelection = await _bestSelection(files); - String name = - DateFormat.yMMMd(Localizations.localeOf(context).languageCode) - .format(date); - if (date.day == currentTime.day && date.month == currentTime.month) { - name = "This day, ${currentTime.year - date.year} years back"; - } - - searchResults.add( - GenericSearchResult( - ResultType.event, - name, - photoSelection, - hierarchicalSearchFilter: TopLevelGenericFilter( - filterName: name, - occurrence: kMostRelevantFilter, - filterResultType: ResultType.event, - matchedUploadedIDs: filesToUploadedFileIDs(photoSelection), - filterIcon: Icons.event_outlined, - ), - ), - ); - } - } - - if (limit != null && searchResults.length >= limit) return searchResults; - } - - // process to find significant weeks (only if there are no significant days) - if (searchResults.isEmpty) { - // Group files by week and year - final currentWeekYearGroups = >{}; - for (final file in allFiles) { - if (file.creationTime! > cutOffTime.microsecondsSinceEpoch) continue; - - final creationTime = - DateTime.fromMicrosecondsSinceEpoch(file.creationTime!); - final week = _getWeekNumber(creationTime); - if (week != currentWeek) continue; - final year = creationTime.year; - - currentWeekYearGroups.putIfAbsent(year, () => []).add(file); - } - - // Process the week and see if it's significant - if (currentWeekYearGroups.isNotEmpty) { - final significantWeeks = currentWeekYearGroups.entries - .where((e) => e.value.length > significantWeekThreshold) - .map((e) => e.key) - .toList(); - if (significantWeeks.length >= 3) { - // Combine all years for this week - final allPhotos = - currentWeekYearGroups.values.expand((x) => x).toList(); - final photoSelection = await _bestSelection(allPhotos); - - searchResults.add( - GenericSearchResult( - ResultType.event, - "This week through the years", - photoSelection, - hierarchicalSearchFilter: TopLevelGenericFilter( - filterName: "Week $currentWeek", - occurrence: kMostRelevantFilter, - filterResultType: ResultType.event, - matchedUploadedIDs: filesToUploadedFileIDs(photoSelection), - filterIcon: Icons.event_outlined, - ), - ), - ); - } else { - // Individual entries for significant years - for (final year in significantWeeks) { - final date = DateTime(year, 1, 1).add( - Duration(days: (currentWeek - 1) * 7), - ); - final files = currentWeekYearGroups[year]!; - final photoSelection = await _bestSelection(files); - final name = - "This week, ${currentTime.year - date.year} years back"; - - searchResults.add( - GenericSearchResult( - ResultType.event, - name, - photoSelection, - hierarchicalSearchFilter: TopLevelGenericFilter( - filterName: name, - occurrence: kMostRelevantFilter, - filterResultType: ResultType.event, - matchedUploadedIDs: filesToUploadedFileIDs(photoSelection), - filterIcon: Icons.event_outlined, - ), - ), - ); - } - } - } - } - - if (limit != null && searchResults.length >= limit) return searchResults; - - // process to find fillers (months) - const wantedMemories = 3; - final neededMemories = wantedMemories - searchResults.length; - if (neededMemories <= 0) return searchResults; - const monthSelectionSize = 20; - - // Group files by month and year - final currentMonthYearGroups = >{}; - for (final file in allFiles) { - if (file.creationTime! > cutOffTime.microsecondsSinceEpoch) continue; - - final creationTime = - DateTime.fromMicrosecondsSinceEpoch(file.creationTime!); - final month = creationTime.month; - if (month != currentMonth) continue; - final year = creationTime.year; - - currentMonthYearGroups.putIfAbsent(year, () => []).add(file); - } - - // Add the largest two months plus the month through the years - final sortedYearsForCurrentMonth = currentMonthYearGroups.keys.toList() - ..sort( - (a, b) => currentMonthYearGroups[b]!.length.compareTo( - currentMonthYearGroups[a]!.length, - ), - ); - if (neededMemories > 1) { - for (int i = neededMemories; i > 1; i--) { - if (sortedYearsForCurrentMonth.isEmpty) break; - final year = sortedYearsForCurrentMonth.removeAt(0); - final monthYearFiles = currentMonthYearGroups[year]!; - final photoSelection = await _bestSelection( - monthYearFiles, - prefferedSize: monthSelectionSize, - ); - final monthName = - DateFormat.MMMM(Localizations.localeOf(context).languageCode) - .format(DateTime(year, currentMonth)); - final name = monthName + ", ${currentTime.year - year} years back"; - searchResults.add( - GenericSearchResult( - ResultType.event, - name, - photoSelection, - hierarchicalSearchFilter: TopLevelGenericFilter( - filterName: name, - occurrence: kMostRelevantFilter, - filterResultType: ResultType.event, - matchedUploadedIDs: filesToUploadedFileIDs(photoSelection), - filterIcon: Icons.event_outlined, - ), - ), - ); - } - } - // Show the month through the remaining years - if (sortedYearsForCurrentMonth.isEmpty) return searchResults; - final allPhotos = sortedYearsForCurrentMonth - .expand((year) => currentMonthYearGroups[year]!) - .toList(); - final photoSelection = - await _bestSelection(allPhotos, prefferedSize: monthSelectionSize); - final monthName = - DateFormat.MMMM(Localizations.localeOf(context).languageCode) - .format(DateTime(currentTime.year, currentMonth)); - final name = monthName + " through the years"; - searchResults.add( - GenericSearchResult( - ResultType.event, - name, - photoSelection, - hierarchicalSearchFilter: TopLevelGenericFilter( - filterName: name, - occurrence: kMostRelevantFilter, - filterResultType: ResultType.event, - matchedUploadedIDs: filesToUploadedFileIDs(photoSelection), - filterIcon: Icons.event_outlined, ), - ), - ); - + ); + } return searchResults; } - int _getWeekNumber(DateTime date) { - // Get day of year (1-366) - final int dayOfYear = int.parse(DateFormat('D').format(date)); - // Integer division by 7 and add 1 to start from week 1 - return ((dayOfYear - 1) ~/ 7) + 1; - } - - /// Returns the best selection of files from the given list. - /// Makes sure that the selection is not more than [prefferedSize] or 10 files, - /// and that each year of the original list is represented. - Future> _bestSelection( - List files, { - int? prefferedSize, - }) async { - final fileCount = files.length; - int targetSize = prefferedSize ?? 10; - if (fileCount <= targetSize) return files; - final safeFiles = - files.where((file) => file.uploadedFileID != null).toList(); - final safeCount = safeFiles.length; - final fileIDs = safeFiles.map((e) => e.uploadedFileID!).toSet(); - final fileIdToFace = await MLDataDB.instance.getFacesForFileIDs(fileIDs); - final faceIDs = - fileIdToFace.values.expand((x) => x.map((face) => face.faceID)).toSet(); - final faceIDsToPersonID = - await MLDataDB.instance.getFaceIdToPersonIdForFaces(faceIDs); - final fileIdToClip = - await MLDataDB.instance.getClipVectorsForFileIDs(fileIDs); - final allYears = safeFiles.map((e) { - final creationTime = DateTime.fromMicrosecondsSinceEpoch(e.creationTime!); - return creationTime.year; - }).toSet(); - - // Get clip scores for each file - const query = - 'Photo of a precious memory radiating warmth, vibrant energy, or quiet beauty — alive with color, light, or emotion'; - // TODO: lau: optimize this later so we don't keep computing embedding - final textEmbedding = await MLComputer.instance.runClipText(query); - final textVector = Vector.fromList(textEmbedding); - const clipThreshold = 0.75; - final fileToScore = {}; - for (final file in safeFiles) { - final clip = fileIdToClip[file.uploadedFileID!]; - if (clip == null) { - fileToScore[file.uploadedFileID!] = 0; - continue; - } - final score = clip.vector.dot(textVector); - fileToScore[file.uploadedFileID!] = score; - } - - // Get face scores for each file - final fileToFaceCount = {}; - for (final file in safeFiles) { - final fileID = file.uploadedFileID!; - fileToFaceCount[fileID] = 0; - final faces = fileIdToFace[fileID]; - if (faces == null || faces.isEmpty) { - continue; - } - for (final face in faces) { - if (faceIDsToPersonID.containsKey(face.faceID)) { - fileToFaceCount[fileID] = fileToFaceCount[fileID]! + 10; - } else { - fileToFaceCount[fileID] = fileToFaceCount[fileID]! + 1; - } - } - } - - final filteredFiles = []; - if (allYears.length <= 1) { - // TODO: lau: eventually this sorting might have to be replaced with some scoring system - // sort first on clip embeddings score (descending) - safeFiles.sort( - (a, b) => fileToScore[b.uploadedFileID!]! - .compareTo(fileToScore[a.uploadedFileID!]!), - ); - // then sort on faces (descending), heavily prioritizing named faces - safeFiles.sort( - (a, b) => fileToFaceCount[b.uploadedFileID!]! - .compareTo(fileToFaceCount[a.uploadedFileID!]!), - ); - - // then filter out similar images as much as possible - filteredFiles.add(safeFiles.first); - int skipped = 0; - filesLoop: - for (final file in safeFiles.sublist(1)) { - if (filteredFiles.length >= targetSize) break; - final clip = fileIdToClip[file.uploadedFileID!]; - if (clip != null && (safeCount - skipped) > targetSize) { - for (final filteredFile in filteredFiles) { - final fClip = fileIdToClip[filteredFile.uploadedFileID!]; - if (fClip == null) continue; - final similarity = clip.vector.dot(fClip.vector); - if (similarity > clipThreshold) { - skipped++; - continue filesLoop; - } - } - } - filteredFiles.add(file); - } - } else { - // Multiple years, each represented and roughly equally distributed - if (prefferedSize == null && (allYears.length * 2) > 10) { - targetSize = allYears.length * 3; - if (safeCount < targetSize) return safeFiles; - } - - // Group files by year and sort each year's list by CLIP then face count - final yearToFiles = >{}; - for (final file in safeFiles) { - final creationTime = - DateTime.fromMicrosecondsSinceEpoch(file.creationTime!); - final year = creationTime.year; - yearToFiles.putIfAbsent(year, () => []).add(file); - } - - for (final year in yearToFiles.keys) { - final yearFiles = yearToFiles[year]!; - // sort first on clip embeddings score (descending) - yearFiles.sort( - (a, b) => fileToScore[b.uploadedFileID!]! - .compareTo(fileToScore[a.uploadedFileID!]!), - ); - // then sort on faces (descending), heavily prioritizing named faces - yearFiles.sort( - (a, b) => fileToFaceCount[b.uploadedFileID!]! - .compareTo(fileToFaceCount[a.uploadedFileID!]!), - ); - } - - // Then join the years together one by one and filter similar images - final years = yearToFiles.keys.toList() - ..sort((a, b) => b.compareTo(a)); // Recent years first - int round = 0; - int skipped = 0; - whileLoop: - while (filteredFiles.length + skipped < safeCount) { - yearLoop: - for (final year in years) { - final yearFiles = yearToFiles[year]!; - if (yearFiles.isEmpty) continue; - final newFile = yearFiles.removeAt(0); - if (round != 0 && (safeCount - skipped) > targetSize) { - // check for filtering - final clip = fileIdToClip[newFile.uploadedFileID!]; - if (clip != null) { - for (final filteredFile in filteredFiles) { - final fClip = fileIdToClip[filteredFile.uploadedFileID!]; - if (fClip == null) continue; - final similarity = clip.vector.dot(fClip.vector); - if (similarity > clipThreshold) { - skipped++; - continue yearLoop; - } - } - } - } - filteredFiles.add(newFile); - if (filteredFiles.length >= targetSize || - filteredFiles.length + skipped >= safeCount) { - break whileLoop; - } - } - round++; - // Extra safety to prevent infinite loops - if (round > safeCount) break; - } - } - - // Order the final selection chronologically - filteredFiles.sort((a, b) => b.creationTime!.compareTo(a.creationTime!)); - return filteredFiles; - } - Future getRandomDateResults( BuildContext context, ) async { diff --git a/mobile/lib/services/smart_memories_service.dart b/mobile/lib/services/smart_memories_service.dart new file mode 100644 index 0000000000..2afe4190aa --- /dev/null +++ b/mobile/lib/services/smart_memories_service.dart @@ -0,0 +1,1487 @@ +import "dart:async"; +import "dart:math" show min, max; + +import "package:flutter/material.dart"; +import "package:intl/intl.dart"; +import "package:logging/logging.dart"; +import "package:ml_linalg/vector.dart"; +import "package:photos/core/configuration.dart"; +import "package:photos/core/constants.dart"; +import "package:photos/db/memories_db.dart"; +import "package:photos/db/ml/db.dart"; +import "package:photos/l10n/l10n.dart"; +import "package:photos/models/base_location.dart"; +import "package:photos/models/file/extensions/file_props.dart"; +import "package:photos/models/file/file.dart"; +import "package:photos/models/local_entity_data.dart"; +import "package:photos/models/location/location.dart"; +import "package:photos/models/location_tag/location_tag.dart"; +import "package:photos/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/people_memory.dart"; +import "package:photos/models/memories/smart_memory.dart"; +import "package:photos/models/memories/time_memory.dart"; +import "package:photos/models/memories/trip_memory.dart"; +import "package:photos/models/ml/face/face.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/location_service.dart"; +import "package:photos/services/machine_learning/face_ml/person/person_service.dart"; +import "package:photos/services/machine_learning/ml_computer.dart"; +import "package:photos/services/machine_learning/ml_result.dart"; +import "package:photos/services/machine_learning/semantic_search/semantic_search_service.dart"; +import "package:photos/services/search_service.dart"; + +class SmartMemoriesService { + final _logger = Logger("SmartMemoriesService"); + final _memoriesDB = MemoriesDB.instance; + + bool _isInit = false; + + Locale? _locale; + late Map _seenTimes; + + Vector? _clipPositiveTextVector; + static const String clipPositiveQuery = + 'Photo of a precious and nostalgic memory radiating warmth, vibrant energy, or quiet beauty — alive with color, light, or emotion'; + + final Map _clipPeopleActivityVectors = {}; + + static const _clipSimilarImageThreshold = 0.75; + static const _clipActivityQueryThreshold = 0.25; + + static const yearsBefore = 30; + + SmartMemoriesService(); + + Future init() async { + if (_isInit) return; + _locale = await getLocale(); + + _clipPositiveTextVector ??= Vector.fromList( + await MLComputer.instance.runClipText(clipPositiveQuery), + ); + for (final peopleActivity in PeopleActivity.values) { + _clipPeopleActivityVectors[peopleActivity] = Vector.fromList( + await MLComputer.instance.runClipText(activityQuery(peopleActivity)), + ); + } + _isInit = true; + _logger.info("Smart memories service initialized"); + } + + // One general method to get all memories, which calls on internal methods for each separate memory type + Future> calcMemories( + DateTime now, + MemoriesCache oldCache, + ) async { + try { + _logger.finest('calcMemories called with time: $now'); + await init(); + final List memories = []; + final allFiles = Set.from( + await SearchService.instance.getAllFilesForSearch(), + ); + _seenTimes = await _memoriesDB.getSeenTimes(); + _logger.finest("All files length: ${allFiles.length}"); + + final peopleMemories = + await _getPeopleResults(allFiles, now, oldCache.peopleShownLogs); + _deductUsedMemories(allFiles, peopleMemories); + memories.addAll(peopleMemories); + _logger.finest("All files length: ${allFiles.length}"); + + // Trip memories + final tripMemories = await _getTripsResults(allFiles, now); + _deductUsedMemories(allFiles, tripMemories); + memories.addAll(tripMemories); + _logger.finest("All files length: ${allFiles.length}"); + + // Time memories + final timeMemories = await _onThisDayOrWeekResults(allFiles, now); + _deductUsedMemories(allFiles, timeMemories); + memories.addAll(timeMemories); + _logger.finest("All files length: ${allFiles.length}"); + + // Filler memories + final fillerMemories = await _getFillerResults(allFiles, now); + memories.addAll(fillerMemories); + return memories; + } catch (e, s) { + _logger.severe("Error calculating smart memories", e, s); + return []; + } + } + + void _deductUsedMemories( + Set files, + List memories, + ) { + final usedFiles = {}; + for (final memory in memories) { + usedFiles.addAll(memory.memories.map((m) => m.file)); + } + files.removeAll(usedFiles); + } + + Future> _getPeopleResults( + Iterable allFiles, + DateTime currentTime, + List shownPeople, + ) async { + final List memoryResults = []; + if (allFiles.isEmpty) return []; + final allFileIdsToFile = {}; + for (final file in allFiles) { + if (file.uploadedFileID != null) { + allFileIdsToFile[file.uploadedFileID!] = file; + } + } + final nowInMicroseconds = currentTime.microsecondsSinceEpoch; + final windowEnd = + currentTime.add(kMemoriesUpdateFrequency).microsecondsSinceEpoch; + + // Get ordered list of important people (all named, from most to least files) + final persons = await PersonService.instance.getPersons(); + if (persons.length < 5) return []; // Stop if not enough named persons + final personIdToPerson = {}; + final personIdToFaceIDs = >{}; + final personIdToFileIDs = >{}; + // final personIdToFaceIdToFace = >{}; TODO: lau: try using relative face size as metric of importance + for (final person in persons) { + final personID = person.remoteID; + personIdToPerson[personID] = person; + personIdToFaceIDs[personID] = {}; + personIdToFileIDs[personID] = {}; + for (final cluster in person.data.assigned) { + if (cluster.faces.isEmpty) continue; + personIdToFaceIDs[personID]!.addAll(cluster.faces); + personIdToFileIDs[personID]! + .addAll(cluster.faces.map((faceID) => getFileIdFromFaceId(faceID))); + } + } + final List orderedImportantPersonsID = + persons.map((p) => p.remoteID).toList(); + orderedImportantPersonsID.sort((a, b) { + final aFaces = personIdToFaceIDs[a]!.length; + final bFaces = personIdToFaceIDs[b]!.length; + return bFaces.compareTo(aFaces); + }); + + // Check if the user has assignmed "me" + String? meID; + final currentUserEmail = Configuration.instance.getEmail(); + for (final personEntity in persons) { + if (personEntity.data.email == currentUserEmail) { + meID = personEntity.remoteID; + break; + } + } + final bool isMeAssigned = meID != null; + Map>? meFilesToFaces; + if (isMeAssigned) { + final meFileIDs = personIdToFileIDs[meID]!; + meFilesToFaces = await MLDataDB.instance.getFacesForFileIDs( + meFileIDs, + ); + } + + // Loop through the people and find all memories + final Map> personToMemories = + {}; + for (final personID in orderedImportantPersonsID) { + final personFileIDs = personIdToFileIDs[personID]!; + final personName = personIdToPerson[personID]!.data.name; + final Map> personFilesToFaces = + await MLDataDB.instance.getFacesForFileIDs( + personFileIDs, + ); + // Inside people loop, check for spotlight (Most likely every person will have a spotlight) + final spotlightFiles = []; + for (final fileID in personFileIDs) { + final int personsPresent = personFilesToFaces[fileID]?.length ?? 10; + if (personsPresent > 1) continue; + final file = allFileIdsToFile[fileID]; + if (file != null) { + spotlightFiles.add(file); + } + } + if (spotlightFiles.length > 5) { + String title = "Spotlight on $personName"; + if (isMeAssigned && meID == personID) { + title = "Spotlight on yourself"; + } + final selectSpotlightMemories = await _bestSelectionPeople( + spotlightFiles.map((f) => Memory.fromFile(f, _seenTimes)).toList(), + ); + final spotlightMemory = PeopleMemory( + selectSpotlightMemories, + title, + nowInMicroseconds, + windowEnd, + PeopleMemoryType.spotlight, + personID, + ); + personToMemories + .putIfAbsent(personID, () => {}) + .putIfAbsent(PeopleMemoryType.spotlight, () => spotlightMemory); + } + + // Inside people loop, check for youAndThem + if (isMeAssigned && meID != personID) { + final youAndThemFiles = []; + for (final fileID in personFileIDs) { + final meFaces = meFilesToFaces![fileID]; + final personFaces = personFilesToFaces[fileID] ?? []; + if (meFaces == null || personFaces.length != 2) continue; + final file = allFileIdsToFile[fileID]; + if (file != null) { + youAndThemFiles.add(file); + } + } + if (youAndThemFiles.length > 5) { + final String title = "You and $personName"; + final selectYouAndThemMemories = await _bestSelectionPeople( + youAndThemFiles.map((f) => Memory.fromFile(f, _seenTimes)).toList(), + ); + final youAndThemMemory = PeopleMemory( + selectYouAndThemMemories, + title, + nowInMicroseconds, + windowEnd, + PeopleMemoryType.youAndThem, + personID, + ); + personToMemories + .putIfAbsent(personID, () => {}) + .putIfAbsent(PeopleMemoryType.youAndThem, () => youAndThemMemory); + } + } + + // Inside people loop, check for doingSomethingTogether + if (isMeAssigned && meID != personID) { + final vectors = await SemanticSearchService.instance + .getClipVectorsForFileIDs(personFileIDs); + final activityFiles = []; + PeopleActivity lastActivity = PeopleActivity.values.first; + activityLoop: + for (final activity in PeopleActivity.values) { + activityFiles.clear(); + lastActivity = activity; + final Vector? activityVector = _clipPeopleActivityVectors[activity]; + if (activityVector == null) { + _logger.severe("No vector for activity $activity"); + continue activityLoop; + } + final similarities = await MLComputer.instance + .compareEmbeddings(vectors, activityVector); + for (final fileID in personFileIDs) { + final similarity = similarities[fileID]; + if (similarity == null) continue; + if (similarity > _clipActivityQueryThreshold) { + final file = allFileIdsToFile[fileID]; + if (file != null) { + activityFiles.add(file); + } + } + } + if (activityFiles.length > 5) break activityLoop; + } + if (activityFiles.length > 5) { + final String title = activityTitle(lastActivity, personName); + final selectActivityMemories = await _bestSelectionPeople( + activityFiles.map((f) => Memory.fromFile(f, _seenTimes)).toList(), + ); + final activityMemory = PeopleMemory( + selectActivityMemories, + title, + nowInMicroseconds, + windowEnd, + PeopleMemoryType.doingSomethingTogether, + personID, + ); + personToMemories.putIfAbsent(personID, () => {}).putIfAbsent( + PeopleMemoryType.doingSomethingTogether, + () => activityMemory, + ); + } + } + + // Inside people loop, check for lastTimeYouSawThem + final lastTimeYouSawThemFiles = []; + int lastCreationTime = 0; + bool longAgo = true; + fileLoop: + for (final fileID in personFileIDs) { + final file = allFileIdsToFile[fileID]; + if (file != null && file.creationTime != null) { + final creationTime = file.creationTime!; + final creationDateTime = + DateTime.fromMicrosecondsSinceEpoch(creationTime); + if (currentTime.difference(creationDateTime).inDays < 365) { + longAgo = false; + break fileLoop; + } + if (creationTime > lastCreationTime - microSecondsInDay) { + final lastDateTime = + DateTime.fromMicrosecondsSinceEpoch(lastCreationTime); + if (creationDateTime.difference(lastDateTime).inHours > 24) { + lastTimeYouSawThemFiles.clear(); + } + if (creationTime > lastCreationTime) { + lastCreationTime = creationTime; + } + lastTimeYouSawThemFiles.add(file); + } + } + } + if (longAgo && lastTimeYouSawThemFiles.length >= 2 && meID != personID) { + final String title = "Last time with $personName"; + final lastTimeMemory = PeopleMemory( + lastTimeYouSawThemFiles + .map((f) => Memory.fromFile(f, _seenTimes)) + .toList(), + title, + nowInMicroseconds, + windowEnd, + PeopleMemoryType.lastTimeYouSawThem, + personID, + lastCreationTime: lastCreationTime, + ); + personToMemories.putIfAbsent(personID, () => {}).putIfAbsent( + PeopleMemoryType.lastTimeYouSawThem, + () => lastTimeMemory, + ); + } + } + + // // Surface everything just for debug checking + // for (final personID in personToMemories.keys) { + // for (final memoryType in PeopleMemoryType.values) { + // if (personToMemories[personID]!.containsKey(memoryType)) { + // memoryResults.add(personToMemories[personID]![memoryType]!); + // } + // } + // } + + // Loop through the people and check if we should surface anything based on relevancy (bday, last met) + personRelevancyLoop: + for (final personID in orderedImportantPersonsID) { + final personMemories = personToMemories[personID]; + if (personID == meID || personMemories == null) continue; + final person = personIdToPerson[personID]!; + // Check if we should surface memory based on birthday + final birthdate = DateTime.tryParse(person.data.birthDate ?? ""); + if (birthdate != null) { + final thisBirthday = + DateTime(currentTime.year, birthdate.month, birthdate.day); + final daysTillBirthday = thisBirthday.difference(currentTime).inDays; + if (daysTillBirthday < 7 && daysTillBirthday >= 0) { + final personName = person.data.name; + final int newAge = currentTime.year - birthdate.year; + final spotlightMem = personMemories[PeopleMemoryType.spotlight]; + if (spotlightMem != null) { + final String firstTitle = "$personName turning $newAge!"; + final String secondTitle = "$personName is $newAge!"; + final thisBirthday = birthdate.copyWith(year: currentTime.year); + memoryResults.add( + spotlightMem.copyWith( + title: firstTitle, + firstDateToShow: thisBirthday + .subtract(const Duration(days: 6)) + .microsecondsSinceEpoch, + lastDateToShow: thisBirthday.microsecondsSinceEpoch, + ), + ); + memoryResults.add( + spotlightMem.copyWith( + title: secondTitle, + firstDateToShow: thisBirthday.microsecondsSinceEpoch, + lastDateToShow: + thisBirthday.add(kDayItself).microsecondsSinceEpoch, + ), + ); + } + final youAndThemMem = personMemories[PeopleMemoryType.youAndThem]; + if (youAndThemMem != null) { + memoryResults.add( + youAndThemMem.copyWith( + firstDateToShow: thisBirthday + .subtract(const Duration(days: 6)) + .microsecondsSinceEpoch, + lastDateToShow: + thisBirthday.add(kDayItself).microsecondsSinceEpoch, + ), + ); + } + continue personRelevancyLoop; + } + } + + // Check if we should surface memory based on last met + final lastMetMemory = personMemories[PeopleMemoryType.lastTimeYouSawThem]; + if (lastMetMemory != null) { + final lastMetTime = DateTime.fromMicrosecondsSinceEpoch( + lastMetMemory.lastCreationTime!, + ).copyWith(year: currentTime.year); + final daysSinceLastMet = lastMetTime.difference(currentTime).inDays; + if (daysSinceLastMet < 7 && daysSinceLastMet >= 0) { + memoryResults.add(lastMetMemory); + } + } + } + + // Loop through the people (and memory types) and add based on rotation + if (memoryResults.length >= 3) return memoryResults; + peopleRotationLoop: + for (final personID in orderedImportantPersonsID) { + for (final memory in memoryResults) { + if (memory.personID == personID) { + continue peopleRotationLoop; + } + } + for (final shownLog in shownPeople) { + if (shownLog.personID != personID) continue; + final shownDate = + DateTime.fromMicrosecondsSinceEpoch(shownLog.lastTimeShown); + final bool seenPersonRecently = + currentTime.difference(shownDate) < kPersonShowTimeout; + if (seenPersonRecently) continue peopleRotationLoop; + } + if (personToMemories[personID] == null) continue peopleRotationLoop; + int added = 0; + potentialMemoryLoop: + for (final potentialMemory in personToMemories[personID]!.values) { + for (final shownLog in shownPeople) { + if (shownLog.personID != personID) continue; + if (shownLog.peopleMemoryType != potentialMemory.peopleMemoryType) { + continue; + } + final shownTypeDate = + DateTime.fromMicrosecondsSinceEpoch(shownLog.lastTimeShown); + final bool seenPersonTypeRecently = + currentTime.difference(shownTypeDate) < kPersonAndTypeShowTimeout; + if (seenPersonTypeRecently) continue potentialMemoryLoop; + } + memoryResults.add(potentialMemory); + added++; + if (added >= 2) break peopleRotationLoop; + } + if (added > 0) break peopleRotationLoop; + } + + return memoryResults; + } + + Future> _getTripsResults( + Iterable allFiles, + DateTime currentTime, + ) async { + final List memoryResults = []; + final Iterable> locationTagEntities = + (await locationService.getLocationTags()); + if (allFiles.isEmpty) return []; + final nowInMicroseconds = currentTime.microsecondsSinceEpoch; + final windowEnd = + currentTime.add(kMemoriesUpdateFrequency).microsecondsSinceEpoch; + final currentMonth = currentTime.month; + final cutOffTime = currentTime.subtract(const Duration(days: 365)); + + final Map, List> tagToItemsMap = {}; + for (int i = 0; i < locationTagEntities.length; i++) { + tagToItemsMap[locationTagEntities.elementAt(i)] = []; + } + final List<(List, Location)> smallRadiusClusters = []; + final List<(List, Location)> wideRadiusClusters = []; + // Go through all files and cluster the ones not inside any location tag + for (EnteFile file in allFiles) { + if (!file.hasLocation || + file.uploadedFileID == null || + !file.isOwner || + file.creationTime == null) { + continue; + } + // Check if the file is inside any location tag + bool hasLocationTag = false; + for (LocalEntity tag in tagToItemsMap.keys) { + if (isFileInsideLocationTag( + tag.item.centerPoint, + file.location!, + tag.item.radius, + )) { + hasLocationTag = true; + tagToItemsMap[tag]!.add(file); + } + } + // Cluster the files not inside any location tag (incremental clustering) + if (!hasLocationTag) { + // Small radius clustering for base locations + bool foundSmallCluster = false; + for (final cluster in smallRadiusClusters) { + final clusterLocation = cluster.$2; + if (isFileInsideLocationTag( + clusterLocation, + file.location!, + 0.6, + )) { + cluster.$1.add(file); + foundSmallCluster = true; + break; + } + } + if (!foundSmallCluster) { + smallRadiusClusters.add(([file], file.location!)); + } + // Wide radius clustering for trip locations + bool foundWideCluster = false; + for (final cluster in wideRadiusClusters) { + final clusterLocation = cluster.$2; + if (isFileInsideLocationTag( + clusterLocation, + file.location!, + 100.0, + )) { + cluster.$1.add(file); + foundWideCluster = true; + break; + } + } + if (!foundWideCluster) { + wideRadiusClusters.add(([file], file.location!)); + } + } + } + + // Identify base locations + final List baseLocations = []; + for (final cluster in smallRadiusClusters) { + final files = cluster.$1; + final location = cluster.$2; + // Check that the photos are distributed over a longer time range (3+ months) + final creationTimes = []; + final Set uniqueDays = {}; + for (final file in files) { + creationTimes.add(file.creationTime!); + final date = DateTime.fromMicrosecondsSinceEpoch(file.creationTime!); + final dayStamp = + DateTime(date.year, date.month, date.day).microsecondsSinceEpoch; + uniqueDays.add(dayStamp); + } + creationTimes.sort(); + if (creationTimes.length < 10) continue; + final firstCreationTime = DateTime.fromMicrosecondsSinceEpoch( + creationTimes.first, + ); + final lastCreationTime = DateTime.fromMicrosecondsSinceEpoch( + creationTimes.last, + ); + if (lastCreationTime.difference(firstCreationTime).inDays < 90) { + continue; + } + // Check for a minimum average number of days photos are clicked in range + final daysRange = lastCreationTime.difference(firstCreationTime).inDays; + if (uniqueDays.length < daysRange * 0.1) continue; + // Check if it's a current or old base location + final bool isCurrent = lastCreationTime.isAfter( + DateTime.now().subtract( + const Duration(days: 90), + ), + ); + baseLocations.add(BaseLocation(files, location, isCurrent)); + } + + // Identify trip locations + final List tripLocations = []; + clusteredLocations: + for (final cluster in wideRadiusClusters) { + final files = cluster.$1; + final location = cluster.$2; + // Check that it's at least 10km away from any base or tag location + bool tooClose = false; + for (final baseLocation in baseLocations) { + if (isFileInsideLocationTag( + baseLocation.location, + location, + 10.0, + )) { + tooClose = true; + break; + } + } + for (final tag in tagToItemsMap.keys) { + if (isFileInsideLocationTag( + tag.item.centerPoint, + location, + 10.0, + )) { + tooClose = true; + break; + } + } + if (tooClose) continue clusteredLocations; + + // Check that the photos are distributed over a short time range (2-30 days) or multiple short time ranges only + files.sort((a, b) => a.creationTime!.compareTo(b.creationTime!)); + // Find distinct time blocks (potential trips) + List currentBlockFiles = [files.first]; + int blockStart = files.first.creationTime!; + int lastTime = files.first.creationTime!; + DateTime lastDateTime = DateTime.fromMicrosecondsSinceEpoch(lastTime); + + for (int i = 1; i < files.length; i++) { + final currentFile = files[i]; + final currentTime = currentFile.creationTime!; + final gap = DateTime.fromMicrosecondsSinceEpoch(currentTime) + .difference(lastDateTime) + .inDays; + + // If gap is too large, end current block and check if it's a valid trip + if (gap > 15) { + // 10 days gap to separate trips. If gap is small, it's likely not a trip + if (gap < 90) continue clusteredLocations; + + final blockDuration = lastDateTime + .difference(DateTime.fromMicrosecondsSinceEpoch(blockStart)) + .inDays; + + // Check if current block is a valid trip (2-30 days) + if (blockDuration >= 2 && blockDuration <= 30) { + tripLocations.add( + TripMemory( + Memory.fromFiles( + currentBlockFiles, + _seenTimes, + ), + 'Trip1', + 0, + 0, + location, + firstCreationTime: blockStart, + lastCreationTime: lastTime, + ), + ); + } + + // Start new block + currentBlockFiles = []; + blockStart = currentTime; + } + + currentBlockFiles.add(currentFile); + lastTime = currentTime; + lastDateTime = DateTime.fromMicrosecondsSinceEpoch(lastTime); + } + // Check final block + final lastBlockDuration = lastDateTime + .difference(DateTime.fromMicrosecondsSinceEpoch(blockStart)) + .inDays; + if (lastBlockDuration >= 2 && lastBlockDuration <= 30) { + tripLocations.add( + TripMemory( + Memory.fromFiles(currentBlockFiles, _seenTimes), + 'Trip2', + 0, + 0, + location, + firstCreationTime: blockStart, + lastCreationTime: lastTime, + ), + ); + } + } + + // Check if any trip locations should be merged + final List mergedTrips = []; + for (final trip in tripLocations) { + final tripFirstTime = DateTime.fromMicrosecondsSinceEpoch( + trip.firstCreationTime!, + ); + final tripLastTime = DateTime.fromMicrosecondsSinceEpoch( + trip.lastCreationTime!, + ); + bool merged = false; + for (int idx = 0; idx < mergedTrips.length; idx++) { + final otherTrip = mergedTrips[idx]; + final otherTripFirstTime = + DateTime.fromMicrosecondsSinceEpoch(otherTrip.firstCreationTime!); + final otherTripLastTime = + DateTime.fromMicrosecondsSinceEpoch(otherTrip.lastCreationTime!); + if (tripFirstTime + .isBefore(otherTripLastTime.add(const Duration(days: 3))) && + tripLastTime.isAfter( + otherTripFirstTime.subtract(const Duration(days: 3)), + )) { + mergedTrips[idx] = TripMemory( + otherTrip.memories + trip.memories, + 'Trip3', + 0, + 0, + otherTrip.location, + firstCreationTime: + min(otherTrip.firstCreationTime!, trip.firstCreationTime!), + lastCreationTime: + max(otherTrip.lastCreationTime!, trip.lastCreationTime!), + ); + _logger.finest('Merged two trip locations'); + merged = true; + break; + } + } + if (merged) continue; + mergedTrips.add( + TripMemory( + trip.memories, + 'Trip4', + 0, + 0, + trip.location, + firstCreationTime: trip.firstCreationTime, + lastCreationTime: trip.lastCreationTime, + ), + ); + } + + // Remove too small and too recent trips + final List validTrips = []; + for (final trip in mergedTrips) { + if (trip.memories.length >= 20 && + trip.averageCreationTime() < cutOffTime.microsecondsSinceEpoch) { + validTrips.add(trip); + } + } + + // For now for testing let's just surface all base locations + for (final baseLocation in baseLocations) { + String name = "Base (${baseLocation.isCurrentBase ? 'current' : 'old'})"; + final String? locationName = _tryFindLocationName( + Memory.fromFiles(baseLocation.files, _seenTimes), + base: true, + ); + if (locationName != null) { + name = + "$locationName (Base, ${baseLocation.isCurrentBase ? 'current' : 'old'})"; + } + memoryResults.add( + TripMemory( + Memory.fromFiles(baseLocation.files, _seenTimes), + name, + 0, + 0, + baseLocation.location, + ), + ); + } + + // For now we surface the two most recent trips of current month, and if none, the earliest upcoming redundant trip + // Group the trips per month and then year + final Map>> tripsByMonthYear = {}; + for (final trip in validTrips) { + final tripDate = + DateTime.fromMicrosecondsSinceEpoch(trip.averageCreationTime()); + tripsByMonthYear + .putIfAbsent(tripDate.month, () => {}) + .putIfAbsent(tripDate.year, () => []) + .add(trip); + } + + // Flatten trips for the current month and annotate with their average date. + final List currentMonthTrips = []; + if (tripsByMonthYear.containsKey(currentMonth)) { + for (final trips in tripsByMonthYear[currentMonth]!.values) { + for (final trip in trips) { + currentMonthTrips.add(trip); + } + } + } + + // If there are past trips this month, show the one or two most recent ones. + if (currentMonthTrips.isNotEmpty) { + currentMonthTrips.sort( + (a, b) => b.averageCreationTime().compareTo(a.averageCreationTime()), + ); + final tripsToShow = currentMonthTrips.take(2); + for (final trip in tripsToShow) { + final year = + DateTime.fromMicrosecondsSinceEpoch(trip.averageCreationTime()) + .year; + final String? locationName = _tryFindLocationName(trip.memories); + String name = + "Trip in $year"; // TODO lau: extract strings for translation + if (locationName != null) { + name = "Trip to $locationName"; + } else if (year == currentTime.year - 1) { + name = "Last year's trip"; + } + final photoSelection = await _bestSelection(trip.memories); + final firstCreationDate = DateTime.fromMicrosecondsSinceEpoch( + trip.firstCreationTime!, + ); + final firstDateToShow = DateTime( + currentTime.year, + firstCreationDate.month, + firstCreationDate.day, + ).subtract(kMemoriesMargin).microsecondsSinceEpoch; + final lastCreationDate = DateTime.fromMicrosecondsSinceEpoch( + trip.lastCreationTime!, + ); + final lastDateToShow = DateTime( + currentTime.year, + lastCreationDate.month, + lastCreationDate.day, + ).add(kMemoriesMargin).microsecondsSinceEpoch; + memoryResults.add( + trip.copyWith( + memories: photoSelection, + title: name, + firstDateToShow: firstDateToShow, + lastDateToShow: lastDateToShow, + ), + ); + } + } + // Otherwise, if no trips happened in the current month, + // look for the earliest upcoming trip in another month that has 3+ trips. + else { + // TODO lau: make sure the same upcoming trip isn't shown multiple times over multiple months + final sortedUpcomingMonths = + List.generate(12, (i) => ((currentMonth + i) % 12) + 1); + checkUpcomingMonths: + for (final month in sortedUpcomingMonths) { + if (tripsByMonthYear.containsKey(month)) { + final List thatMonthTrips = []; + for (final trips in tripsByMonthYear[month]!.values) { + for (final trip in trips) { + thatMonthTrips.add(trip); + } + } + if (thatMonthTrips.length >= 3) { + // take and use the third earliest trip + thatMonthTrips.sort( + (a, b) => + a.averageCreationTime().compareTo(b.averageCreationTime()), + ); + final trip = thatMonthTrips[2]; + final year = + DateTime.fromMicrosecondsSinceEpoch(trip.averageCreationTime()) + .year; + final String? locationName = _tryFindLocationName(trip.memories); + String name = "Trip in $year"; + if (locationName != null) { + name = "Trip to $locationName"; + } else if (year == currentTime.year - 1) { + name = "Last year's trip"; + } + final photoSelection = await _bestSelection(trip.memories); + memoryResults.add( + trip.copyWith( + memories: photoSelection, + title: name, + firstDateToShow: nowInMicroseconds, + lastDateToShow: windowEnd, + ), + ); + break checkUpcomingMonths; + } + } + } + } + return memoryResults; + } + + Future> _onThisDayOrWeekResults( + Iterable allFiles, + DateTime currentTime, + ) async { + final List memoryResult = []; + if (allFiles.isEmpty) return []; + + final currentDayMonth = currentTime.month * 100 + currentTime.day; + final currentWeek = _getWeekNumber(currentTime); + final currentMonth = currentTime.month; + final currentYear = currentTime.year; + final cutOffTime = currentTime.subtract(const Duration(days: 365)); + final averageDailyPhotos = allFiles.length / 365; + final significantDayThreshold = averageDailyPhotos * 0.25; + final significantWeekThreshold = averageDailyPhotos * 0.40; + + // Group files by day-month and year + final dayMonthYearGroups = >>{}; + + for (final file in allFiles) { + if (file.creationTime! > cutOffTime.microsecondsSinceEpoch) continue; + + final creationTime = + DateTime.fromMicrosecondsSinceEpoch(file.creationTime!); + final dayMonth = creationTime.month * 100 + creationTime.day; + final year = creationTime.year; + + dayMonthYearGroups + .putIfAbsent(dayMonth, () => {}) + .putIfAbsent(year, () => []) + .add(Memory.fromFile(file, _seenTimes)); + } + + // Process each nearby day-month to find significant days + for (final dayMonth in dayMonthYearGroups.keys) { + final dayDiff = dayMonth - currentDayMonth; + if (dayDiff < 0 || dayDiff > kMemoriesUpdateFrequency.inDays) continue; + // TODO: lau: this doesn't cover month changes properly + + final yearGroups = dayMonthYearGroups[dayMonth]!; + final significantDays = yearGroups.entries + .where((e) => e.value.length > significantDayThreshold) + .map((e) => e.key) + .toList(); + + if (significantDays.length >= 3) { + // Combine all years for this day-month + final date = + DateTime(currentTime.year, dayMonth ~/ 100, dayMonth % 100); + final allPhotos = yearGroups.values.expand((x) => x).toList(); + final photoSelection = await _bestSelection(allPhotos); + + memoryResult.add( + TimeMemory( + photoSelection, + "${DateFormat('MMMM d').format(date)} through the years", + date.subtract(kMemoriesMargin).microsecondsSinceEpoch, + date.add(kDayItself).microsecondsSinceEpoch, + ), + ); + } else { + // Individual entries for significant years + for (final year in significantDays) { + final date = DateTime(year, dayMonth ~/ 100, dayMonth % 100); + final showDate = + DateTime(currentYear, dayMonth ~/ 100, dayMonth % 100); + final files = yearGroups[year]!; + final photoSelection = await _bestSelection(files); + String name = DateFormat.yMMMd(_locale?.languageCode).format(date); + memoryResult.add( + TimeMemory( + photoSelection, + name, + showDate.subtract(kMemoriesMargin).microsecondsSinceEpoch, + showDate.microsecondsSinceEpoch, + ), + ); + name = "This day, ${currentTime.year - date.year} years back"; + memoryResult.add( + TimeMemory( + photoSelection, + name, + showDate.microsecondsSinceEpoch, + showDate.add(kDayItself).microsecondsSinceEpoch, + ), + ); + } + } + } + + // process to find significant weeks (only if there are no significant days) + if (memoryResult.isEmpty) { + // Group files by week and year + final currentWeekYearGroups = >{}; + for (final file in allFiles) { + if (file.creationTime! > cutOffTime.microsecondsSinceEpoch) continue; + + final creationTime = + DateTime.fromMicrosecondsSinceEpoch(file.creationTime!); + final week = _getWeekNumber(creationTime); + if (week != currentWeek) continue; + final year = creationTime.year; + + currentWeekYearGroups + .putIfAbsent(year, () => []) + .add(Memory.fromFile(file, _seenTimes)); + } + + // Process the week and see if it's significant + if (currentWeekYearGroups.isNotEmpty) { + final significantWeeks = currentWeekYearGroups.entries + .where((e) => e.value.length > significantWeekThreshold) + .map((e) => e.key) + .toList(); + if (significantWeeks.length >= 3) { + // Combine all years for this week + final allPhotos = + currentWeekYearGroups.values.expand((x) => x).toList(); + final photoSelection = await _bestSelection(allPhotos); + const name = "This week through the years"; + memoryResult.add( + TimeMemory( + photoSelection, + name, + currentTime.subtract(kMemoriesMargin).microsecondsSinceEpoch, + currentTime.add(kMemoriesUpdateFrequency).microsecondsSinceEpoch, + ), + ); + } else { + // Individual entries for significant years + for (final year in significantWeeks) { + final date = DateTime(year, 1, 1).add( + Duration(days: (currentWeek - 1) * 7), + ); + final files = currentWeekYearGroups[year]!; + final photoSelection = await _bestSelection(files); + final name = + "This week, ${currentTime.year - date.year} years back"; + + memoryResult.add( + TimeMemory( + photoSelection, + name, + currentTime.subtract(kMemoriesMargin).microsecondsSinceEpoch, + currentTime + .add(kMemoriesUpdateFrequency) + .microsecondsSinceEpoch, + ), + ); + } + } + } + } + + // process to find fillers (months) + const wantedMemories = 3; + final neededMemories = wantedMemories - memoryResult.length; + if (neededMemories <= 0) return memoryResult; + const monthSelectionSize = 20; + + // Group files by month and year + final currentMonthYearGroups = >{}; + for (final file in allFiles) { + if (file.creationTime! > cutOffTime.microsecondsSinceEpoch) continue; + + final creationTime = + DateTime.fromMicrosecondsSinceEpoch(file.creationTime!); + final month = creationTime.month; + if (month != currentMonth) continue; + final year = creationTime.year; + + currentMonthYearGroups + .putIfAbsent(year, () => []) + .add(Memory.fromFile(file, _seenTimes)); + } + + // Add the largest two months plus the month through the years + final sortedYearsForCurrentMonth = currentMonthYearGroups.keys.toList() + ..sort( + (a, b) => currentMonthYearGroups[b]!.length.compareTo( + currentMonthYearGroups[a]!.length, + ), + ); + if (neededMemories > 1) { + for (int i = neededMemories; i > 1; i--) { + if (sortedYearsForCurrentMonth.isEmpty) break; + final year = sortedYearsForCurrentMonth.removeAt(0); + final monthYearFiles = currentMonthYearGroups[year]!; + final photoSelection = await _bestSelection( + monthYearFiles, + prefferedSize: monthSelectionSize, + ); + final monthName = DateFormat.MMMM(_locale?.languageCode) + .format(DateTime(year, currentMonth)); + final daysLeftInMonth = DateTime(currentYear, currentMonth + 1, 0).day - + currentTime.day + + 1; + final name = monthName + ", ${currentTime.year - year} years back"; + memoryResult.add( + TimeMemory( + photoSelection, + name, + currentTime.microsecondsSinceEpoch, + currentTime + .add(Duration(days: daysLeftInMonth)) + .microsecondsSinceEpoch, + ), + ); + } + } + // Show the month through the remaining years + if (sortedYearsForCurrentMonth.isEmpty) return memoryResult; + final allPhotos = sortedYearsForCurrentMonth + .expand((year) => currentMonthYearGroups[year]!) + .toList(); + final photoSelection = + await _bestSelection(allPhotos, prefferedSize: monthSelectionSize); + final monthName = DateFormat.MMMM(_locale?.languageCode) + .format(DateTime(currentTime.year, currentMonth)); + final daysLeftInMonth = + DateTime(currentYear, currentMonth + 1, 0).day - currentTime.day + 1; + final name = monthName + " through the years"; + memoryResult.add( + TimeMemory( + photoSelection, + name, + currentTime.microsecondsSinceEpoch, + currentTime.add(Duration(days: daysLeftInMonth)).microsecondsSinceEpoch, + ), + ); + + return memoryResult; + } + + Future> _getFillerResults( + Iterable allFiles, + DateTime currentTime, + ) async { + final List memoryResults = []; + if (allFiles.isEmpty) return []; + final nowInMicroseconds = currentTime.microsecondsSinceEpoch; + final windowEnd = + currentTime.add(kMemoriesUpdateFrequency).microsecondsSinceEpoch; + final currentYear = currentTime.year; + final cutOffTime = currentTime + .subtract(const Duration(days: 364) - kMemoriesUpdateFrequency); + final timeTillYearEnd = DateTime(currentYear + 1).difference(currentTime); + final bool almostYearEnd = timeTillYearEnd < kMemoriesUpdateFrequency; + + final Map> yearsAgoToMemories = {}; + for (final file in allFiles) { + if (file.creationTime == null || + file.creationTime! > cutOffTime.microsecondsSinceEpoch) { + continue; + } + final fileDate = DateTime.fromMicrosecondsSinceEpoch(file.creationTime!); + final fileTimeInYear = fileDate.copyWith(year: currentYear); + final diff = fileTimeInYear.difference(currentTime); + if (!diff.isNegative && diff < kMemoriesUpdateFrequency) { + final yearsAgo = currentYear - fileDate.year; + yearsAgoToMemories + .putIfAbsent(yearsAgo, () => []) + .add(Memory.fromFile(file, _seenTimes)); + } else if (almostYearEnd) { + final altDiff = fileDate.copyWith(year: currentYear + 1).difference( + currentTime, + ); + if (!altDiff.isNegative && altDiff < kMemoriesUpdateFrequency) { + final yearsAgo = currentYear - fileDate.year + 1; + yearsAgoToMemories + .putIfAbsent(yearsAgo, () => []) + .add(Memory.fromFile(file, _seenTimes)); + } + } + } + for (var yearAgo = 1; yearAgo <= yearsBefore; yearAgo++) { + final memories = yearsAgoToMemories[yearAgo]; + if (memories == null) continue; + memories.sort( + (a, b) => a.file.creationTime!.compareTo(b.file.creationTime!), + ); + final fillerMemory = FillerMemory( + memories, + "filler", + nowInMicroseconds, + windowEnd, + ); + memoryResults.add(fillerMemory); + } + return memoryResults; + } + + /// TODO: lau: replace this by just taking next 7 days + int _getWeekNumber(DateTime date) { + // Get day of year (1-366) + final int dayOfYear = int.parse(DateFormat('D').format(date)); + // Integer division by 7 and add 1 to start from week 1 + return ((dayOfYear - 1) ~/ 7) + 1; + } + + String? _tryFindLocationName( + List memories, { + bool base = false, + }) { + final files = Memory.filesFromMemories(memories); + final results = locationService.getFilesInCitySync(files); + final List sortedByResultCount = results.keys.toList() + ..sort((a, b) => results[b]!.length.compareTo(results[a]!.length)); + if (sortedByResultCount.isEmpty) return null; + final biggestPlace = sortedByResultCount.first; + if (results[biggestPlace]!.length > files.length / 2) { + return biggestPlace.city; + } + if (results.length > 2 && + results.keys.map((city) => city.country).toSet().length == 1 && + !base) { + return biggestPlace.country; + } + return null; + } + + /// Creates a curated selection of memories for the People memories. + /// The selection is based on the following things: + /// - Distribution of photos over time + /// - Nostalgia score of photos + /// - Distribution of photos over locations + Future> _bestSelectionPeople( + List memories, { + int? prefferedSize, + }) async { + try { + final fileCount = memories.length; + final int targetSize = prefferedSize ?? 10; + if (fileCount <= targetSize) return memories; + final safeMemories = memories + .where((memory) => memory.file.uploadedFileID != null) + .toList(); + + // Sort by time + final sortedTimeMemories = []; + for (final memory in safeMemories) { + if (memory.file.creationTime != null) { + sortedTimeMemories.add(memory); + } + } + sortedTimeMemories.sort( + (a, b) => a.file.creationTime!.compareTo(b.file.creationTime!), + ); + if (sortedTimeMemories.length < targetSize) return sortedTimeMemories; + + // Divide into 10 time buckets distributing all memories as evenly as possible. + final int total = sortedTimeMemories.length; + final int numBuckets = targetSize; + final int quotient = total ~/ numBuckets; + final int remainder = total % numBuckets; + final List> timeBuckets = []; + int offset = 0; + for (int i = 0; i < numBuckets; i++) { + final int bucketSize = quotient + (i < remainder ? 1 : 0); + timeBuckets + .add(sortedTimeMemories.sublist(offset, offset + bucketSize)); + offset += bucketSize; + } + + final finalSelection = []; + bucketLoop: + for (final bucket in timeBuckets) { + // Get X% most nostalgic photos + final bucketFileIDs = bucket + .map((memory) => memory.file.uploadedFileID!) + .toSet() + .toList(); + final bucketVectors = await SemanticSearchService.instance + .getClipVectorsForFileIDs(bucketFileIDs); + final nostalgiaScores = await MLComputer.instance + .compareEmbeddings(bucketVectors, _clipPositiveTextVector!); + final sortedNostalgia = bucket + ..sort( + (a, b) => nostalgiaScores[b.file.uploadedFileID!]! + .compareTo(nostalgiaScores[a.file.uploadedFileID!]!), + ); + final mostNostalgic = sortedNostalgia + .take((max(bucket.length * 0.3, 1)).toInt()) + .toList(); + + if (mostNostalgic.isEmpty) { + _logger.severe('No nostalgic photos in bucket'); + } + + // If no selection yet, take the most nostalgic photo + if (finalSelection.isEmpty) { + finalSelection.add(mostNostalgic.first); + continue bucketLoop; + } + + // From nostalgic selection, take the photo furthest away from all currently selected ones + double globalMaxMinDistance = 0; + int farthestDistanceIdx = 0; + for (var i = 0; i < mostNostalgic.length; i++) { + final mem = mostNostalgic[i]; + double minDistance = double.infinity; + for (final selected in finalSelection) { + if (selected.file.location == null || mem.file.location == null) { + continue; + } + final distance = + calculateDistance(mem.file.location!, selected.file.location!); + if (distance < minDistance) { + minDistance = distance; + } + } + if (minDistance > globalMaxMinDistance) { + globalMaxMinDistance = minDistance; + farthestDistanceIdx = i; + } + } + finalSelection.add(mostNostalgic[farthestDistanceIdx]); + } + + finalSelection + .sort((a, b) => b.file.creationTime!.compareTo(a.file.creationTime!)); + + _logger.finest( + 'People memories selection done, returning ${finalSelection.length} memories', + ); + return finalSelection; + } catch (e, s) { + _logger.severe('Error in _bestSelectionPeople', e, s); + return []; + } + } + + /// Returns the best selection of files from the given list, for time and trip memories. + /// Makes sure that the selection is not more than [prefferedSize] or 10 files, + /// and that each year of the original list is represented. + Future> _bestSelection( + List memories, { + int? prefferedSize, + }) async { + final fileCount = memories.length; + int targetSize = prefferedSize ?? 10; + if (fileCount <= targetSize) return memories; + final safeMemories = + memories.where((memory) => memory.file.uploadedFileID != null).toList(); + final safeCount = safeMemories.length; + final fileIDs = safeMemories.map((e) => e.file.uploadedFileID!).toSet(); + final fileIdToFace = await MLDataDB.instance.getFacesForFileIDs(fileIDs); + final faceIDs = + fileIdToFace.values.expand((x) => x.map((face) => face.faceID)).toSet(); + final faceIDsToPersonID = + await MLDataDB.instance.getFaceIdToPersonIdForFaces(faceIDs); + + final allYears = safeMemories.map((e) { + final creationTime = + DateTime.fromMicrosecondsSinceEpoch(e.file.creationTime!); + return creationTime.year; + }).toSet(); + + // Get clip scores for each file + final vectors = + await SemanticSearchService.instance.getClipVectorsForFileIDs(fileIDs); + final fileToScore = await MLComputer.instance + .compareEmbeddings(vectors, _clipPositiveTextVector!); + final fileIdToClip = {}; + for (final vector in vectors) { + fileIdToClip[vector.fileID] = vector; + } + + // Get face scores for each file + final fileToFaceCount = {}; + for (final mem in safeMemories) { + final fileID = mem.file.uploadedFileID!; + fileToFaceCount[fileID] = 0; + final faces = fileIdToFace[fileID]; + if (faces == null || faces.isEmpty) { + continue; + } + for (final face in faces) { + if (faceIDsToPersonID.containsKey(face.faceID)) { + fileToFaceCount[fileID] = fileToFaceCount[fileID]! + 10; + } else { + fileToFaceCount[fileID] = fileToFaceCount[fileID]! + 1; + } + } + } + + final filteredMemories = []; + if (allYears.length <= 1) { + // TODO: lau: eventually this sorting might have to be replaced with some scoring system + // sort first on clip embeddings score (descending) + safeMemories.sort( + (a, b) => fileToScore[b.file.uploadedFileID!]! + .compareTo(fileToScore[a.file.uploadedFileID!]!), + ); + // then sort on faces (descending), heavily prioritizing named faces + safeMemories.sort( + (a, b) => fileToFaceCount[b.file.uploadedFileID!]! + .compareTo(fileToFaceCount[a.file.uploadedFileID!]!), + ); + + // then filter out similar images as much as possible + filteredMemories.add(safeMemories.first); + int skipped = 0; + filesLoop: + for (final mem in safeMemories.sublist(1)) { + if (filteredMemories.length >= targetSize) break; + final clip = fileIdToClip[mem.file.uploadedFileID!]; + if (clip != null && (safeCount - skipped) > targetSize) { + for (final filteredMem in filteredMemories) { + final fClip = fileIdToClip[filteredMem.file.uploadedFileID!]; + if (fClip == null) continue; + final similarity = clip.vector.dot(fClip.vector); + if (similarity > _clipSimilarImageThreshold) { + skipped++; + continue filesLoop; + } + } + } + filteredMemories.add(mem); + } + } else { + // Multiple years, each represented and roughly equally distributed + if (prefferedSize == null && (allYears.length * 2) > 10) { + targetSize = allYears.length * 3; + if (safeCount < targetSize) return safeMemories; + } + + // Group files by year and sort each year's list by CLIP then face count + final yearToFiles = >{}; + for (final safeMem in safeMemories) { + final creationTime = + DateTime.fromMicrosecondsSinceEpoch(safeMem.file.creationTime!); + final year = creationTime.year; + yearToFiles.putIfAbsent(year, () => []).add(safeMem); + } + + for (final year in yearToFiles.keys) { + final yearFiles = yearToFiles[year]!; + // sort first on clip embeddings score (descending) + yearFiles.sort( + (a, b) => fileToScore[b.file.uploadedFileID!]! + .compareTo(fileToScore[a.file.uploadedFileID!]!), + ); + // then sort on faces (descending), heavily prioritizing named faces + yearFiles.sort( + (a, b) => fileToFaceCount[b.file.uploadedFileID!]! + .compareTo(fileToFaceCount[a.file.uploadedFileID!]!), + ); + } + + // Then join the years together one by one and filter similar images + final years = yearToFiles.keys.toList() + ..sort((a, b) => b.compareTo(a)); // Recent years first + int round = 0; + int skipped = 0; + whileLoop: + while (filteredMemories.length + skipped < safeCount) { + yearLoop: + for (final year in years) { + final yearFiles = yearToFiles[year]!; + if (yearFiles.isEmpty) continue; + final newMem = yearFiles.removeAt(0); + if (round != 0 && (safeCount - skipped) > targetSize) { + // check for filtering + final clip = fileIdToClip[newMem.file.uploadedFileID!]; + if (clip != null) { + for (final filteredMem in filteredMemories) { + final fClip = fileIdToClip[filteredMem.file.uploadedFileID!]; + if (fClip == null) continue; + final similarity = clip.vector.dot(fClip.vector); + if (similarity > _clipSimilarImageThreshold) { + skipped++; + continue yearLoop; + } + } + } + } + filteredMemories.add(newMem); + if (filteredMemories.length >= targetSize || + filteredMemories.length + skipped >= safeCount) { + break whileLoop; + } + } + round++; + // Extra safety to prevent infinite loops + if (round > safeCount) break; + } + } + + // Order the final selection chronologically + filteredMemories + .sort((a, b) => a.file.creationTime!.compareTo(b.file.creationTime!)); + return filteredMemories; + } +} diff --git a/mobile/lib/utils/diff_fetcher.dart b/mobile/lib/services/sync/diff_fetcher.dart similarity index 97% rename from mobile/lib/utils/diff_fetcher.dart rename to mobile/lib/services/sync/diff_fetcher.dart index 3bcf22b7d0..405978f2f8 100644 --- a/mobile/lib/utils/diff_fetcher.dart +++ b/mobile/lib/services/sync/diff_fetcher.dart @@ -1,16 +1,16 @@ import 'dart:convert'; import 'dart:math'; -import "package:dio/dio.dart"; -import "package:flutter/material.dart"; +import 'package:dio/dio.dart'; +import 'package:ente_crypto/ente_crypto.dart'; +import 'package:flutter/material.dart'; import 'package:logging/logging.dart'; import 'package:photos/core/network/network.dart'; import 'package:photos/db/files_db.dart'; -import "package:photos/generated/l10n.dart"; +import 'package:photos/generated/l10n.dart'; import 'package:photos/models/file/file.dart'; import "package:photos/models/metadata/file_magic.dart"; import "package:photos/services/collections_service.dart"; -import 'package:photos/utils/crypto_util.dart'; import "package:photos/utils/dialog_util.dart"; import "package:photos/utils/file_key.dart"; diff --git a/mobile/lib/services/local_sync_service.dart b/mobile/lib/services/sync/local_sync_service.dart similarity index 93% rename from mobile/lib/services/local_sync_service.dart rename to mobile/lib/services/sync/local_sync_service.dart index d8237dd92a..92dab92310 100644 --- a/mobile/lib/services/local_sync_service.dart +++ b/mobile/lib/services/sync/local_sync_service.dart @@ -8,6 +8,7 @@ import 'package:photos/core/configuration.dart'; import "package:photos/core/errors.dart"; import 'package:photos/core/event_bus.dart'; import 'package:photos/db/device_files_db.dart'; +import "package:photos/db/enum/conflict_algo.dart"; import 'package:photos/db/file_updation_db.dart'; import 'package:photos/db/files_db.dart'; import 'package:photos/events/backup_folders_updated_event.dart'; @@ -19,9 +20,8 @@ import "package:photos/models/ignored_file.dart"; import 'package:photos/services/app_lifecycle_service.dart'; import "package:photos/services/ignored_files_service.dart"; import 'package:photos/services/local/local_sync_util.dart'; -import "package:photos/utils/debouncer.dart"; import "package:photos/utils/photo_manager_util.dart"; -import "package:photos/utils/sqlite_util.dart"; +import "package:photos/utils/standalone/debouncer.dart"; import 'package:shared_preferences/shared_preferences.dart'; import 'package:synchronized/synchronized.dart'; import 'package:tuple/tuple.dart'; @@ -39,10 +39,6 @@ class LocalSyncService { static const kHasGrantedPermissionsKey = "has_granted_permissions"; static const kPermissionStateKey = "permission_state"; - // Adding `_2` as a suffic to pull files that were earlier ignored due to permission errors - // See https://github.com/CaiJingLong/flutter_photo_manager/issues/589 - static const kInvalidFileIDsKey = "invalid_file_ids_2"; - LocalSyncService._privateConstructor(); static final LocalSyncService instance = @@ -79,7 +75,7 @@ class LocalSyncService { } _existingSync = Completer(); final int ownerID = Configuration.instance.getUserID()!; - + // We use a lock to prevent synchronisation to occur while it is downloading // as this introduces wrong entry in FilesDB due to race condition // This is a fix for https://github.com/ente-io/ente/issues/4296 @@ -98,7 +94,8 @@ class LocalSyncService { ); } else { // Load from 0 - 01.01.2010 - Bus.instance.fire(SyncStatusUpdate(SyncStatus.startedFirstGalleryImport)); + Bus.instance + .fire(SyncStatusUpdate(SyncStatus.startedFirstGalleryImport)); var startTime = 0; var toYear = 2010; var toTime = DateTime(toYear).microsecondsSinceEpoch; @@ -170,12 +167,11 @@ class LocalSyncService { final existingLocalFileIDs = await _db.getExistingLocalFileIDs(ownerID); final Map> pathToLocalIDs = await _db.getDevicePathIDToLocalIDMap(); - final invalidIDs = _getInvalidFileIDs().toSet(); + final localDiffResult = await getDiffWithLocal( localAssets, existingLocalFileIDs, pathToLocalIDs, - invalidIDs, ); bool hasAnyMappingChanged = false; if (localDiffResult.newPathToLocalIDs?.isNotEmpty ?? false) { @@ -237,18 +233,6 @@ class LocalSyncService { await IgnoredFilesService.instance.cacheAndInsert([ignored]); } - @Deprecated( - "remove usage after few releases as we will switch to ignored files. Keeping it now to clear the invalid file ids from shared prefs", - ) - List _getInvalidFileIDs() { - if (_prefs.containsKey(kInvalidFileIDsKey)) { - _prefs.remove(kInvalidFileIDsKey); - return []; - } else { - return []; - } - } - Lock getLock() { return _lock; } diff --git a/mobile/lib/services/remote_sync_service.dart b/mobile/lib/services/sync/remote_sync_service.dart similarity index 99% rename from mobile/lib/services/remote_sync_service.dart rename to mobile/lib/services/sync/remote_sync_service.dart index 3c60a0009c..bedbc773d4 100644 --- a/mobile/lib/services/remote_sync_service.dart +++ b/mobile/lib/services/sync/remote_sync_service.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'dart:io'; import 'dart:math'; -import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:logging/logging.dart'; import 'package:photos/core/configuration.dart'; @@ -26,11 +25,13 @@ 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/local_file_update_service.dart'; import "package:photos/services/notification_service.dart"; -import 'package:photos/services/sync_service.dart'; -import 'package:photos/utils/diff_fetcher.dart'; +import "package:photos/services/preview_video_store.dart"; +import 'package:photos/services/sync/diff_fetcher.dart'; +import 'package:photos/services/sync/sync_service.dart'; import 'package:photos/utils/file_uploader.dart'; import 'package:photos/utils/file_util.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -125,6 +126,10 @@ class RemoteSyncService { await _prefs.setBool(_isFirstRemoteSyncDone, true); await syncDeviceCollectionFilesForUpload(); } + + FileDataService.instance.syncFDStatus().then((_) { + PreviewVideoStore.instance.queueFiles(); + }).ignore(); final filesToBeUploaded = await _getFilesToBeUploaded(); final hasUploadedFiles = await _uploadFiles(filesToBeUploaded); if (filesToBeUploaded.isNotEmpty) { @@ -309,7 +314,6 @@ class RemoteSyncService { Future joinAndSyncCollection( BuildContext context, int collectionID, - ) async { await _collectionsService.joinPublicCollection(context, collectionID); await _collectionsService.sync(); diff --git a/mobile/lib/services/sync_service.dart b/mobile/lib/services/sync/sync_service.dart similarity index 80% rename from mobile/lib/services/sync_service.dart rename to mobile/lib/services/sync/sync_service.dart index 2eaafe2545..8172c1150f 100644 --- a/mobile/lib/services/sync_service.dart +++ b/mobile/lib/services/sync/sync_service.dart @@ -9,20 +9,14 @@ import 'package:photos/core/configuration.dart'; import 'package:photos/core/constants.dart'; import 'package:photos/core/errors.dart'; import 'package:photos/core/event_bus.dart'; -import 'package:photos/core/network/network.dart'; -import 'package:photos/db/device_files_db.dart'; -import 'package:photos/db/files_db.dart'; import 'package:photos/events/permission_granted_event.dart'; 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/backup_status.dart'; import 'package:photos/models/file/file_type.dart'; -import "package:photos/services/filedata/filedata_service.dart"; -import "package:photos/services/files_service.dart"; -import 'package:photos/services/local_sync_service.dart'; import 'package:photos/services/notification_service.dart'; -import 'package:photos/services/remote_sync_service.dart'; +import 'package:photos/services/sync/local_sync_service.dart'; +import 'package:photos/services/sync/remote_sync_service.dart'; import 'package:photos/utils/file_uploader.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -30,7 +24,6 @@ class SyncService { final _logger = Logger("SyncService"); final _localSyncService = LocalSyncService.instance; final _remoteSyncService = RemoteSyncService.instance; - final _enteDio = NetworkClient.instance.enteDio; final _uploader = FileUploader.instance; bool _syncStopRequested = false; Completer? _existingSync; @@ -87,7 +80,6 @@ class SyncService { _existingSync = Completer(); bool successful = false; try { - FileDataService.instance.syncFDStatus().ignore(); await _doSync(); if (_lastSyncStatusEvent != null && _lastSyncStatusEvent!.status != @@ -133,11 +125,11 @@ class SyncService { ), ); } catch (e) { - if (e is DioError) { - if (e.type == DioErrorType.connectTimeout || - e.type == DioErrorType.sendTimeout || - e.type == DioErrorType.receiveTimeout || - e.type == DioErrorType.other) { + if (e is DioException) { + if (e.type == DioExceptionType.connectionTimeout || + e.type == DioExceptionType.sendTimeout || + e.type == DioExceptionType.receiveTimeout || + e.type == DioExceptionType.unknown) { Bus.instance.fire( SyncStatusUpdate( SyncStatus.paused, @@ -202,39 +194,6 @@ class SyncService { ); } - Future getBackupStatus({String? pathID}) async { - BackedUpFileIDs ids; - final bool hasMigratedSize = await FilesService.instance.hasMigratedSizes(); - if (pathID == null) { - ids = await FilesDB.instance.getBackedUpIDs(); - } else { - ids = await FilesDB.instance.getBackedUpForDeviceCollection( - pathID, - Configuration.instance.getUserID()!, - ); - } - late int size; - if (hasMigratedSize) { - size = ids.localSize; - } else { - size = await _getFileSize(ids.uploadedIDs); - } - return BackupStatus(ids.localIDs, size); - } - - Future _getFileSize(List fileIDs) async { - try { - final response = await _enteDio.post( - "/files/size", - data: {"fileIDs": fileIDs}, - ); - return response.data["size"]; - } catch (e) { - _logger.severe(e); - rethrow; - } - } - Future _doSync() async { await _localSyncService.sync(); if (_localSyncService.hasCompletedFirstImport()) { diff --git a/mobile/lib/services/trash_sync_service.dart b/mobile/lib/services/sync/trash_sync_service.dart similarity index 54% rename from mobile/lib/services/trash_sync_service.dart rename to mobile/lib/services/sync/trash_sync_service.dart index d1224ab25f..53c94f16c7 100644 --- a/mobile/lib/services/trash_sync_service.dart +++ b/mobile/lib/services/sync/trash_sync_service.dart @@ -1,6 +1,9 @@ import 'dart:async'; +import "dart:convert"; +import "dart:math"; import 'package:dio/dio.dart'; +import "package:ente_crypto/ente_crypto.dart"; import 'package:logging/logging.dart'; import 'package:photos/core/constants.dart'; import 'package:photos/core/event_bus.dart'; @@ -9,24 +12,24 @@ import 'package:photos/events/collection_updated_event.dart'; import 'package:photos/events/force_reload_trash_page_event.dart'; import 'package:photos/events/trash_updated_event.dart'; import 'package:photos/extensions/list.dart'; +import 'package:photos/models/api/collection/trash_item_request.dart'; import 'package:photos/models/file/file.dart'; import 'package:photos/models/file/trash_file.dart'; import 'package:photos/models/ignored_file.dart'; -import 'package:photos/models/trash_item_request.dart'; +import "package:photos/models/metadata/file_magic.dart"; import 'package:photos/services/ignored_files_service.dart'; -import 'package:photos/utils/trash_diff_fetcher.dart'; +import "package:photos/utils/file_key.dart"; import 'package:shared_preferences/shared_preferences.dart'; class TrashSyncService { final _logger = Logger("TrashSyncService"); - final TrashDiffFetcher _diffFetcher; + final _trashDB = TrashDB.instance; static const kLastTrashSyncTime = "last_trash_sync_time"; late SharedPreferences _prefs; final Dio _enteDio; - TrashSyncService(this._prefs, this._enteDio) - : _diffFetcher = TrashDiffFetcher(_enteDio) { + TrashSyncService(this._prefs, this._enteDio) { _logger.fine("TrashSyncService constructor"); } @@ -38,7 +41,7 @@ class TrashSyncService { final lastSyncTime = _getSyncTime(); bool isLocalTrashUpdated = false; _logger.fine('sync trash sinceTime : $lastSyncTime'); - final diff = await _diffFetcher.getTrashFilesDiff(lastSyncTime); + final diff = await getTrashFilesDiff(lastSyncTime); if (diff.trashedFiles.isNotEmpty) { isLocalTrashUpdated = true; _logger.fine("inserting ${diff.trashedFiles.length} items in trash"); @@ -80,7 +83,7 @@ class TrashSyncService { } } - Future _updateIgnoredFiles(Diff diff) async { + Future _updateIgnoredFiles(TrashDiff diff) async { final ignoredFiles = []; for (TrashFile t in diff.trashedFiles) { final file = IgnoredFile.fromTrashItem(t); @@ -122,6 +125,103 @@ class TrashSyncService { } } + Future getTrashFilesDiff(int sinceTime) async { + try { + final response = await _enteDio.get( + "/trash/v2/diff", + queryParameters: { + "sinceTime": sinceTime, + }, + ); + int latestUpdatedAtTime = 0; + final trashedFiles = []; + final deletedUploadIDs = []; + final restoredFiles = []; + + final diff = response.data["diff"] as List; + final bool hasMore = response.data["hasMore"] as bool; + final startTime = DateTime.now(); + for (final item in diff) { + final trash = TrashFile(); + trash.createdAt = item['createdAt']; + trash.updateAt = item['updatedAt']; + latestUpdatedAtTime = max(latestUpdatedAtTime, trash.updateAt); + if (item["isDeleted"]) { + deletedUploadIDs.add(item["file"]["id"]); + continue; + } + + trash.deleteBy = item['deleteBy']; + trash.uploadedFileID = item["file"]["id"]; + trash.collectionID = item["file"]["collectionID"]; + trash.updationTime = item["file"]["updationTime"]; + trash.ownerID = item["file"]["ownerID"]; + trash.encryptedKey = item["file"]["encryptedKey"]; + trash.keyDecryptionNonce = item["file"]["keyDecryptionNonce"]; + trash.fileDecryptionHeader = item["file"]["file"]["decryptionHeader"]; + trash.thumbnailDecryptionHeader = + item["file"]["thumbnail"]["decryptionHeader"]; + trash.metadataDecryptionHeader = + item["file"]["metadata"]["decryptionHeader"]; + final fileDecryptionKey = getFileKey(trash); + final encodedMetadata = await CryptoUtil.decryptChaCha( + CryptoUtil.base642bin(item["file"]["metadata"]["encryptedData"]), + fileDecryptionKey, + CryptoUtil.base642bin(trash.metadataDecryptionHeader!), + ); + final Map metadata = + jsonDecode(utf8.decode(encodedMetadata)); + trash.applyMetadata(metadata); + if (item["file"]['magicMetadata'] != null) { + final utfEncodedMmd = await CryptoUtil.decryptChaCha( + CryptoUtil.base642bin(item["file"]['magicMetadata']['data']), + fileDecryptionKey, + CryptoUtil.base642bin(item["file"]['magicMetadata']['header']), + ); + trash.mMdEncodedJson = utf8.decode(utfEncodedMmd); + trash.mMdVersion = item["file"]['magicMetadata']['version']; + } + if (item["file"]['pubMagicMetadata'] != null) { + final utfEncodedMmd = await CryptoUtil.decryptChaCha( + CryptoUtil.base642bin(item["file"]['pubMagicMetadata']['data']), + fileDecryptionKey, + CryptoUtil.base642bin(item["file"]['pubMagicMetadata']['header']), + ); + trash.pubMmdEncodedJson = utf8.decode(utfEncodedMmd); + trash.pubMmdVersion = item["file"]['pubMagicMetadata']['version']; + trash.pubMagicMetadata = + PubMagicMetadata.fromEncodedJson(trash.pubMmdEncodedJson!); + } + if (item['isRestored']) { + restoredFiles.add(trash); + continue; + } + trashedFiles.add(trash); + } + + final endTime = DateTime.now(); + _logger.info( + "time for parsing " + + diff.length.toString() + + ": " + + Duration( + microseconds: (endTime.microsecondsSinceEpoch - + startTime.microsecondsSinceEpoch), + ).inMilliseconds.toString(), + ); + return TrashDiff( + trashedFiles, + restoredFiles, + deletedUploadIDs, + hasMore, + latestUpdatedAtTime, + ); + } catch (e, s) { + _logger.severe(e, s); + rethrow; + } + } + Future> _trashFiles( Map requestData, ) async { @@ -174,3 +274,18 @@ class TrashSyncService { } } } + +class TrashDiff { + final List trashedFiles; + final List restoredFiles; + final List deletedUploadIDs; + final bool hasMore; + final int lastSyncedTimeStamp; + TrashDiff( + this.trashedFiles, + this.restoredFiles, + this.deletedUploadIDs, + this.hasMore, + this.lastSyncedTimeStamp, + ); +} diff --git a/mobile/lib/services/user_remote_flag_service.dart b/mobile/lib/services/user_remote_flag_service.dart deleted file mode 100644 index 42bc45a60d..0000000000 --- a/mobile/lib/services/user_remote_flag_service.dart +++ /dev/null @@ -1,141 +0,0 @@ -import 'dart:async'; -import 'dart:io'; - -import 'package:collection/collection.dart'; -import "package:dio/dio.dart"; -import "package:flutter/foundation.dart"; -import 'package:logging/logging.dart'; -import 'package:photos/core/event_bus.dart'; -import 'package:photos/events/notification_event.dart'; -import "package:photos/service_locator.dart"; -import 'package:photos/services/user_service.dart'; -import 'package:shared_preferences/shared_preferences.dart'; - -class UserRemoteFlagService { - final Dio _enteDio; - late final _logger = Logger((UserRemoteFlagService).toString()); - final SharedPreferences _prefs; - - static const String recoveryVerificationFlag = "recoveryKeyVerified"; - static const String mapEnabled = "mapEnabled"; - static const String mlEnabled = "faceSearchEnabled"; - static const String videoStreamingEnabled = "videoStreamingEnabled"; - static const String needRecoveryKeyVerification = - "needRecoveryKeyVerification"; - - UserRemoteFlagService(this._enteDio, this._prefs) { - debugPrint("UserRemoteFlagService constructor"); - } - - bool shouldShowRecoveryVerification() { - if (!_prefs.containsKey(needRecoveryKeyVerification)) { - // fetch the status from remote - _refreshRecoveryVerificationFlag().ignore(); - return false; - } else { - final bool shouldShow = _prefs.getBool(needRecoveryKeyVerification)!; - if (shouldShow) { - // refresh the status to check if user marked it as done on another device - _refreshRecoveryVerificationFlag().ignore(); - } - return shouldShow; - } - } - - bool getCachedBoolValue(String key) { - bool defaultValue = false; - if (key == mapEnabled) { - defaultValue = flagService.mapEnabled; - } else if (key == mlEnabled) { - defaultValue = flagService.hasGrantedMLConsent; - } - return _prefs.getBool(key) ?? defaultValue; - } - - Future setBoolValue(String key, bool value) async { - await _updateKeyValue(key, value.toString()); - return _prefs.setBool(key, value); - } - - // markRecoveryVerificationAsDone is used to track if user has verified their - // recovery key in the past or not. This helps in avoid showing the same - // prompt to the user on re-install or signing into a different device - Future markRecoveryVerificationAsDone() async { - await _updateKeyValue(recoveryVerificationFlag, true.toString()); - await _prefs.setBool(needRecoveryKeyVerification, false); - } - - Future _refreshRecoveryVerificationFlag() async { - _logger.finest('refresh recovery key verification flag'); - final remoteStatusValue = - await _getValue(recoveryVerificationFlag, "false"); - final bool isNeedVerificationFlagSet = - _prefs.containsKey(needRecoveryKeyVerification); - if (remoteStatusValue.toLowerCase() == "true") { - await _prefs.setBool(needRecoveryKeyVerification, false); - // If the user verified on different device, then we should refresh - // the UI to dismiss the Notification. - if (isNeedVerificationFlagSet) { - Bus.instance.fire(NotificationEvent()); - } - } else if (!isNeedVerificationFlagSet) { - // Verification is not done yet as remoteStatus is false and local flag to - // show notification isn't set. Set the flag to true if any active - // session is older than 1 day. - final activeSessions = await UserService.instance.getActiveSessions(); - final int microSecondsInADay = const Duration(days: 1).inMicroseconds; - final bool anyActiveSessionOlderThanADay = - activeSessions.sessions.firstWhereOrNull( - (e) => - (e.creationTime + microSecondsInADay) < - DateTime.now().microsecondsSinceEpoch, - ) != - null; - if (anyActiveSessionOlderThanADay) { - await _prefs.setBool(needRecoveryKeyVerification, true); - Bus.instance.fire(NotificationEvent()); - } else { - // continue defaulting to no verification prompt - _logger.finest('No active session older than 1 day'); - } - } - } - - Future _getValue(String key, String? defaultValue) async { - try { - final Map queryParams = {"key": key}; - if (defaultValue != null) { - queryParams["defaultValue"] = defaultValue; - } - final response = - await _enteDio.get("/remote-store", queryParameters: queryParams); - if (response.statusCode != HttpStatus.ok) { - throw Exception("Unexpected status code ${response.statusCode}"); - } - return response.data["value"]; - } catch (e) { - _logger.info("Error while fetching bool status for $key", e); - rethrow; - } - } - - // _setBooleanFlag sets the corresponding flag on remote - // to mark recovery as completed - Future _updateKeyValue(String key, String value) async { - try { - final response = await _enteDio.post( - "/remote-store/update", - data: { - "key": key, - "value": value, - }, - ); - if (response.statusCode != HttpStatus.ok) { - throw Exception("Unexpected state"); - } - } catch (e) { - _logger.warning("Failed to set flag for $key", e); - rethrow; - } - } -} diff --git a/mobile/lib/services/video_memory_service.dart b/mobile/lib/services/video_memory_service.dart new file mode 100644 index 0000000000..9bab841ad2 --- /dev/null +++ b/mobile/lib/services/video_memory_service.dart @@ -0,0 +1,288 @@ +import "dart:async"; +import "dart:io"; +import "dart:typed_data"; + +import "package:ffmpeg_kit_flutter_full_gpl/ffmpeg_kit.dart"; +import "package:ffmpeg_kit_flutter_full_gpl/return_code.dart"; +import "package:flutter/cupertino.dart"; +import "package:image/image.dart" as img; +import "package:logging/logging.dart"; +import "package:path_provider/path_provider.dart"; +import "package:photos/models/file/file.dart"; +import "package:photos/models/file/file_type.dart"; +import "package:photos/ui/notification/toast.dart"; +import "package:photos/utils/dialog_util.dart"; +import "package:photos/utils/file_util.dart"; + +final _logger = Logger("VideoMemoryService"); + +Future createSlideshow( + BuildContext context, + List files, +) async { + final dialog = createProgressDialog( + context, + "Creating video...", + ); + + try { + await dialog.show(); + + final imageData = await _prepareImageFiles(files); + + if (imageData.paths.isEmpty) { + await dialog.hide(); + } + + final command = _buildFFmpegCommand( + context, + imageData.paths, + imageData.heights, + imageData.widths, + ); + + await _executeFFmpegProcess( + context: context, + command: command, + onComplete: () { + dialog.hide(); + }, + ); + } catch (e) { + _logger.severe("Error creating slideshow: $e"); + await dialog.hide(); + } +} + +Future _prepareImageFiles( + List files, +) async { + final List paths = []; + final List heights = []; + final List widths = []; + + final Directory tempDir = await getTemporaryDirectory(); + final String tempPath = tempDir.path; + + for (EnteFile file in files) { + if (file.fileType == FileType.livePhoto || + file.fileType == FileType.video) { + continue; + } + + final File? originalImage = await getFile(file); + if (originalImage == null) continue; + + final List bytes = await originalImage.readAsBytes(); + final img.Image? decodedImage = img.decodeImage(Uint8List.fromList(bytes)); + + if (decodedImage != null) { + String processedPath; + + if (_isJpegFile(originalImage.path)) { + processedPath = originalImage.path; + } else { + processedPath = + '$tempPath/${DateTime.now().millisecondsSinceEpoch}.jpg'; + File(processedPath) + .writeAsBytesSync(img.encodeJpg(decodedImage, quality: 95)); + } + + paths.add(processedPath); + widths.add(decodedImage.width.toDouble()); + heights.add(decodedImage.height.toDouble()); + } + } + + return ImageProcessingResult( + paths: paths, + heights: heights, + widths: widths, + ); +} + +bool _isJpegFile(String path) { + return path.toLowerCase().endsWith("jpg") || + path.toLowerCase().endsWith("jpeg"); +} + +Future _executeFFmpegProcess({ + required BuildContext context, + required String command, + required Function onComplete, +}) async { + try { + final completer = Completer(); + final startTime = DateTime.now().millisecondsSinceEpoch; + + await FFmpegKit.executeAsync( + command, + (session) async { + final returnCode = await session.getReturnCode(); + final executionTime = _calculateExecutionTime(startTime); + + if (ReturnCode.isSuccess(returnCode)) { + _logger.info( + "FFmpeg command executed successfully in $executionTime seconds", + ); + _completeOperation(completer, onComplete); + showToast( + context, + "Video successfully create at ${_generateOutputPath()}", + ); + } else { + _logger.warning( + "FFmpeg process failed with return code $returnCode in $executionTime seconds", + ); + showToast(context, "Video creation failed. Please try again."); + _completeOperation(completer, onComplete); + await FFmpegKit.cancel(); + } + }, + (log) { + final String logMessage = log.getMessage(); + + if (logMessage.contains("Invalid data found") || + logMessage.contains("Error")) { + _logger.warning("Error detected in FFmpeg log: $logMessage"); + FFmpegKit.cancel(); + + if (!completer.isCompleted) { + _completeOperation(completer, onComplete); + } + } + }, + ); + + return completer.future; + } catch (e) { + await FFmpegKit.cancel(); + _logger.severe("Error during FFmpeg execution: $e"); + onComplete(); + rethrow; + } +} + +void _completeOperation( + Completer completer, + Function onComplete, +) { + onComplete(); + if (!completer.isCompleted) { + completer.complete(); + } +} + +double _calculateExecutionTime(int startTimeMs) { + final endTime = DateTime.now().millisecondsSinceEpoch; + return (endTime - startTimeMs) / 1000; +} + +String _buildFFmpegCommand( + BuildContext context, + List imagePaths, + List imageHeights, + List imageWidths, +) { + final String outputPath = _generateOutputPath(); + + final screenDimensions = MediaQuery.sizeOf(context); + final int screenWidth = screenDimensions.width.toInt(); + final int screenHeight = screenDimensions.height.toInt(); + + final StringBuffer command = StringBuffer(); + + command.write('-y '); + + for (int i = 0; i < imagePaths.length; i++) { + command.write('-loop 1 -t 2 -i "${imagePaths[i]}" '); + } + + command.write('-filter_complex "'); + + for (int i = 0; i < imagePaths.length; i++) { + final double aspectRatioOfImage = imageWidths[i] / imageHeights[i]; + final double aspectRatioOfScreen = screenWidth / screenHeight; + int scaledWidth, scaledHeight; + + if (aspectRatioOfImage > aspectRatioOfScreen) { + scaledWidth = screenWidth; + scaledHeight = (screenWidth / aspectRatioOfImage).toInt(); + } else { + scaledHeight = screenHeight; + scaledWidth = (screenHeight * aspectRatioOfImage).toInt(); + } + + command.write( + '[$i:v]scale=$scaledWidth:$scaledHeight:force_original_aspect_ratio=decrease,' + 'pad=$screenWidth:$screenHeight:(ow-iw)/2:(oh-ih)/2,' + 'zoompan=z=\'zoom+0.001\':x=\'iw/2-(iw/zoom/2)\':y=\'ih/2-(ih/zoom/2)\':d=150:s=${screenWidth}x${screenHeight}:fps=60[v$i];', + ); + } + + for (int i = 0; i < imagePaths.length - 1; i++) { + final String transition = _getTransitionType(i % 5); + + if (i == 0) { + command.write( + '[v0][v1]xfade=transition=$transition:duration=0.5:offset=2[f0];', + ); + } else { + command.write( + '[f${i - 1}][v${i + 1}]xfade=transition=$transition:duration=0.5:offset=${2 * (i + 1)}[f$i];', + ); + } + } + + command.write('" -map "[f${imagePaths.length - 2}]" ' + '-c:v libx264 -crf 18 -preset slow -movflags +faststart -pix_fmt yuv420p -r 60 ' + '-t ${imagePaths.length * 2} "$outputPath"'); + + return command.toString(); +} + +String _getTransitionType(int index) { + switch (index) { + case 0: + return 'fade'; + case 1: + return 'smoothleft'; + case 2: + return 'smoothright'; + case 3: + return 'slideright'; + default: + return 'fadeblack'; + } +} + +String _generateOutputPath() { + Directory? directory; + if (Platform.isAndroid) { + try { + directory = Directory('/storage/emulated/0/Download'); + if (!directory.existsSync()) { + directory.createSync(recursive: true); + } + } catch (e) { + _logger.warning("Failed to create download directory: $e"); + directory = Directory('/storage/emulated/0/Download'); + } + } else { + directory = Directory('/storage/emulated/0/Download'); + } + + return '${directory.path}/ente_video_memory_${DateTime.now().millisecondsSinceEpoch}.mp4'; +} + +class ImageProcessingResult { + final List paths; + final List heights; + final List widths; + + ImageProcessingResult({ + required this.paths, + required this.heights, + required this.widths, + }); +} diff --git a/mobile/lib/states/all_sections_examples_state.dart b/mobile/lib/states/all_sections_examples_state.dart index abb0077ef3..19e640d318 100644 --- a/mobile/lib/states/all_sections_examples_state.dart +++ b/mobile/lib/states/all_sections_examples_state.dart @@ -10,7 +10,7 @@ import "package:photos/events/people_changed_event.dart"; import "package:photos/events/tab_changed_event.dart"; import "package:photos/models/search/search_result.dart"; import "package:photos/models/search/search_types.dart"; -import "package:photos/utils/debouncer.dart"; +import "package:photos/utils/standalone/debouncer.dart"; class AllSectionsExamplesProvider extends StatefulWidget { final Widget child; diff --git a/mobile/lib/states/location_state.dart b/mobile/lib/states/location_state.dart index 8ab32e55e8..2227f324e3 100644 --- a/mobile/lib/states/location_state.dart +++ b/mobile/lib/states/location_state.dart @@ -9,7 +9,7 @@ import "package:photos/models/local_entity_data.dart"; import "package:photos/models/location/location.dart"; import "package:photos/models/location_tag/location_tag.dart"; import "package:photos/models/typedefs.dart"; -import "package:photos/utils/debouncer.dart"; +import "package:photos/utils/standalone/debouncer.dart"; class LocationTagStateProvider extends StatefulWidget { final LocalEntity? locationTagEntity; diff --git a/mobile/lib/states/user_details_state.dart b/mobile/lib/states/user_details_state.dart index 07e90fd939..5c366b3fef 100644 --- a/mobile/lib/states/user_details_state.dart +++ b/mobile/lib/states/user_details_state.dart @@ -4,15 +4,15 @@ import 'package:flutter/material.dart'; import 'package:photos/core/event_bus.dart'; import 'package:photos/events/opened_settings_event.dart'; import 'package:photos/models/user_details.dart'; -import 'package:photos/services/user_service.dart'; +import 'package:photos/services/account/user_service.dart'; class UserDetailsStateWidget extends StatefulWidget { final Widget child; const UserDetailsStateWidget({ required this.child, - Key? key, - }) : super(key: key); + super.key, + }); @override State createState() => UserDetailsStateWidgetState(); @@ -65,12 +65,12 @@ class InheritedUserDetails extends InheritedWidget { final bool isCached; const InheritedUserDetails({ - Key? key, - required Widget child, + super.key, + required super.child, required this.userDetails, required this.isCached, required this.userDetailsState, - }) : super(key: key, child: child); + }); static InheritedUserDetails? of(BuildContext context) => context.dependOnInheritedWidgetOfExactType(); diff --git a/mobile/lib/ui/account/change_email_dialog.dart b/mobile/lib/ui/account/change_email_dialog.dart index 18c70d00b0..3f0c83d9c1 100644 --- a/mobile/lib/ui/account/change_email_dialog.dart +++ b/mobile/lib/ui/account/change_email_dialog.dart @@ -1,11 +1,11 @@ import 'package:flutter/material.dart'; import "package:photos/l10n/l10n.dart"; -import 'package:photos/services/user_service.dart'; +import 'package:photos/services/account/user_service.dart'; import 'package:photos/utils/dialog_util.dart'; import 'package:photos/utils/email_util.dart'; class ChangeEmailDialog extends StatefulWidget { - const ChangeEmailDialog({Key? key}) : super(key: key); + const ChangeEmailDialog({super.key}); @override State createState() => _ChangeEmailDialogState(); diff --git a/mobile/lib/ui/account/delete_account_page.dart b/mobile/lib/ui/account/delete_account_page.dart index e4259a6077..961549a859 100644 --- a/mobile/lib/ui/account/delete_account_page.dart +++ b/mobile/lib/ui/account/delete_account_page.dart @@ -2,18 +2,18 @@ import "dart:async"; import 'dart:convert'; import "package:dropdown_button2/dropdown_button2.dart"; +import 'package:ente_crypto/ente_crypto.dart'; import 'package:flutter/material.dart'; import "package:logging/logging.dart"; import 'package:photos/core/configuration.dart'; import "package:photos/generated/l10n.dart"; -import 'package:photos/models/delete_account.dart'; -import 'package:photos/services/user_service.dart'; +import 'package:photos/models/api/user/delete_account.dart'; +import 'package:photos/services/account/user_service.dart'; 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/utils/crypto_util.dart'; +import "package:photos/ui/notification/toast.dart"; import 'package:photos/utils/dialog_util.dart'; -import "package:photos/utils/toast_util.dart"; class DeleteAccountPage extends StatefulWidget { const DeleteAccountPage({ diff --git a/mobile/lib/ui/account/email_entry_page.dart b/mobile/lib/ui/account/email_entry_page.dart index 262bd0fc6c..73e04e0f61 100644 --- a/mobile/lib/ui/account/email_entry_page.dart +++ b/mobile/lib/ui/account/email_entry_page.dart @@ -5,12 +5,12 @@ import 'package:password_strength/password_strength.dart'; import 'package:photos/core/configuration.dart'; import 'package:photos/ente_theme_data.dart'; import "package:photos/generated/l10n.dart"; -import 'package:photos/services/user_service.dart'; +import 'package:photos/services/account/user_service.dart'; import "package:photos/theme/ente_theme.dart"; import 'package:photos/ui/common/dynamic_fab.dart'; import 'package:photos/ui/common/web_page.dart'; +import "package:photos/ui/notification/toast.dart"; import "package:photos/utils/dialog_util.dart"; -import "package:photos/utils/toast_util.dart"; import 'package:step_progress_indicator/step_progress_indicator.dart'; import "package:styled_text/styled_text.dart"; diff --git a/mobile/lib/ui/account/login_page.dart b/mobile/lib/ui/account/login_page.dart index f5f77a4bbf..dd3e5cc92c 100644 --- a/mobile/lib/ui/account/login_page.dart +++ b/mobile/lib/ui/account/login_page.dart @@ -7,7 +7,7 @@ import "package:photos/core/errors.dart"; import "package:photos/generated/l10n.dart"; import "package:photos/l10n/l10n.dart"; import "package:photos/models/api/user/srp.dart"; -import 'package:photos/services/user_service.dart'; +import 'package:photos/services/account/user_service.dart'; import "package:photos/theme/ente_theme.dart"; import "package:photos/ui/account/login_pwd_verification_page.dart"; import 'package:photos/ui/common/dynamic_fab.dart'; @@ -15,7 +15,7 @@ import 'package:photos/ui/common/web_page.dart'; import "package:styled_text/styled_text.dart"; class LoginPage extends StatefulWidget { - const LoginPage({Key? key}) : super(key: key); + const LoginPage({super.key}); @override State createState() => _LoginPageState(); diff --git a/mobile/lib/ui/account/login_pwd_verification_page.dart b/mobile/lib/ui/account/login_pwd_verification_page.dart index ac974c20da..1969a033f3 100644 --- a/mobile/lib/ui/account/login_pwd_verification_page.dart +++ b/mobile/lib/ui/account/login_pwd_verification_page.dart @@ -1,12 +1,12 @@ import "package:dio/dio.dart"; +import "package:ente_crypto/ente_crypto.dart"; import "package:flutter/foundation.dart"; import 'package:flutter/material.dart'; import "package:logging/logging.dart"; import 'package:photos/core/configuration.dart'; -import "package:photos/core/errors.dart"; import "package:photos/generated/l10n.dart"; import "package:photos/models/api/user/srp.dart"; -import "package:photos/services/user_service.dart"; +import "package:photos/services/account/user_service.dart"; import "package:photos/theme/ente_theme.dart"; import 'package:photos/ui/common/dynamic_fab.dart'; import "package:photos/ui/components/buttons/button_widget.dart"; @@ -21,8 +21,7 @@ import "package:photos/utils/email_util.dart"; class LoginPasswordVerificationPage extends StatefulWidget { final SrpAttributes srpAttributes; - const LoginPasswordVerificationPage({Key? key, required this.srpAttributes}) - : super(key: key); + const LoginPasswordVerificationPage({super.key, required this.srpAttributes}); @override State createState() => @@ -113,7 +112,7 @@ class _LoginPasswordVerificationPageState password, dialog, ); - } on DioError catch (e, s) { + } on DioException catch (e, s) { await dialog.hide(); if (e.response != null && e.response!.statusCode == 401) { _logger.severe('server reject, failed verify SRP login', e, s); @@ -123,8 +122,10 @@ class _LoginPasswordVerificationPageState S.of(context).pleaseTryAgain, ); } else { - _logger.severe('API failure during SRP login', e, s); - if (e.type == DioErrorType.other) { + _logger.severe('API failure during SRP login ${e.type}', e, s); + if (e.type == DioExceptionType.connectionTimeout || + e.type == DioExceptionType.receiveTimeout || + e.type == DioExceptionType.sendTimeout) { await _showContactSupportDialog( context, S.of(context).noInternetConnection, diff --git a/mobile/lib/ui/account/ott_verification_page.dart b/mobile/lib/ui/account/ott_verification_page.dart index d03861055c..cca13d1da0 100644 --- a/mobile/lib/ui/account/ott_verification_page.dart +++ b/mobile/lib/ui/account/ott_verification_page.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:photos/ente_theme_data.dart'; import "package:photos/generated/l10n.dart"; -import 'package:photos/services/user_service.dart'; +import 'package:photos/services/account/user_service.dart'; import "package:photos/theme/ente_theme.dart"; import 'package:photos/ui/common/dynamic_fab.dart'; import 'package:step_progress_indicator/step_progress_indicator.dart'; @@ -18,8 +18,8 @@ class OTTVerificationPage extends StatefulWidget { this.isChangeEmail = false, this.isCreateAccountScreen = false, this.isResetPasswordScreen = false, - Key? key, - }) : super(key: key); + super.key, + }); @override State createState() => _OTTVerificationPageState(); diff --git a/mobile/lib/ui/account/passkey_page.dart b/mobile/lib/ui/account/passkey_page.dart index 11ca53adea..8db4c3a130 100644 --- a/mobile/lib/ui/account/passkey_page.dart +++ b/mobile/lib/ui/account/passkey_page.dart @@ -8,13 +8,13 @@ import "package:photos/core/errors.dart"; import "package:photos/generated/l10n.dart"; import "package:photos/l10n/l10n.dart"; import "package:photos/models/account/two_factor.dart"; -import 'package:photos/services/user_service.dart'; +import 'package:photos/services/account/user_service.dart'; import "package:photos/ui/account/two_factor_authentication_page.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/utils/dialog_util.dart"; import "package:photos/utils/navigation_util.dart"; -import "package:photos/utils/toast_util.dart"; import "package:uni_links/uni_links.dart"; import 'package:url_launcher/url_launcher_string.dart'; diff --git a/mobile/lib/ui/account/password_entry_page.dart b/mobile/lib/ui/account/password_entry_page.dart index 54b7af8507..a8d7e5d82f 100644 --- a/mobile/lib/ui/account/password_entry_page.dart +++ b/mobile/lib/ui/account/password_entry_page.dart @@ -10,17 +10,17 @@ import 'package:photos/events/account_configured_event.dart'; import 'package:photos/events/subscription_purchased_event.dart'; import "package:photos/generated/l10n.dart"; import "package:photos/l10n/l10n.dart"; -import "package:photos/models/key_gen_result.dart"; -import 'package:photos/services/user_service.dart'; +import "package:photos/models/api/user/key_gen_result.dart"; +import 'package:photos/services/account/user_service.dart'; import "package:photos/theme/ente_theme.dart"; import 'package:photos/ui/account/recovery_key_page.dart'; import 'package:photos/ui/common/dynamic_fab.dart'; import 'package:photos/ui/common/web_page.dart'; import "package:photos/ui/components/models/button_type.dart"; +import 'package:photos/ui/notification/toast.dart'; import 'package:photos/ui/payment/subscription.dart'; import 'package:photos/utils/dialog_util.dart'; import 'package:photos/utils/navigation_util.dart'; -import 'package:photos/utils/toast_util.dart'; import "package:styled_text/styled_text.dart"; enum PasswordEntryMode { @@ -34,8 +34,8 @@ class PasswordEntryPage extends StatefulWidget { const PasswordEntryPage({ required this.mode, - Key? key, - }) : super(key: key); + super.key, + }); @override State createState() => _PasswordEntryPageState(); diff --git a/mobile/lib/ui/account/password_reentry_page.dart b/mobile/lib/ui/account/password_reentry_page.dart index d3f6be5644..f54b82c832 100644 --- a/mobile/lib/ui/account/password_reentry_page.dart +++ b/mobile/lib/ui/account/password_reentry_page.dart @@ -1,6 +1,7 @@ import 'dart:async'; import "dart:typed_data"; +import "package:ente_crypto/ente_crypto.dart"; import 'package:flutter/material.dart'; import 'package:logging/logging.dart'; import 'package:photos/core/configuration.dart'; @@ -8,18 +9,17 @@ import 'package:photos/core/errors.dart'; import 'package:photos/core/event_bus.dart'; import 'package:photos/events/subscription_purchased_event.dart'; import "package:photos/generated/l10n.dart"; -import "package:photos/services/user_service.dart"; +import "package:photos/services/account/user_service.dart"; import "package:photos/theme/ente_theme.dart"; import 'package:photos/ui/account/recovery_page.dart'; import 'package:photos/ui/common/dynamic_fab.dart'; import 'package:photos/ui/components/buttons/button_widget.dart'; import 'package:photos/ui/tabs/home_widget.dart'; -import "package:photos/utils/crypto_util.dart"; import 'package:photos/utils/dialog_util.dart'; import 'package:photos/utils/email_util.dart'; class PasswordReentryPage extends StatefulWidget { - const PasswordReentryPage({Key? key}) : super(key: key); + const PasswordReentryPage({super.key}); @override State createState() => _PasswordReentryPageState(); diff --git a/mobile/lib/ui/account/recovery_key_page.dart b/mobile/lib/ui/account/recovery_key_page.dart index 9ea3107096..1d4d9a57d7 100644 --- a/mobile/lib/ui/account/recovery_key_page.dart +++ b/mobile/lib/ui/account/recovery_key_page.dart @@ -10,7 +10,7 @@ import 'package:photos/core/constants.dart'; import 'package:photos/ente_theme_data.dart'; import "package:photos/generated/l10n.dart"; import 'package:photos/ui/common/gradient_button.dart'; -import 'package:photos/utils/toast_util.dart'; +import 'package:photos/ui/notification/toast.dart'; import "package:share_plus/share_plus.dart"; import 'package:step_progress_indicator/step_progress_indicator.dart'; @@ -28,7 +28,7 @@ class RecoveryKeyPage extends StatefulWidget { const RecoveryKeyPage( this.recoveryKey, this.doneText, { - Key? key, + super.key, this.showAppBar, this.onDone, this.isDismissible, @@ -36,7 +36,7 @@ class RecoveryKeyPage extends StatefulWidget { this.text, this.subText, this.showProgressBar = false, - }) : super(key: key); + }); @override State createState() => _RecoveryKeyPageState(); diff --git a/mobile/lib/ui/account/recovery_page.dart b/mobile/lib/ui/account/recovery_page.dart index 881b0792dd..1d3dd142be 100644 --- a/mobile/lib/ui/account/recovery_page.dart +++ b/mobile/lib/ui/account/recovery_page.dart @@ -1,16 +1,14 @@ -import 'dart:ui'; - import 'package:flutter/material.dart'; import 'package:photos/core/configuration.dart'; import "package:photos/generated/l10n.dart"; import "package:photos/theme/ente_theme.dart"; import 'package:photos/ui/account/password_entry_page.dart'; import 'package:photos/ui/common/dynamic_fab.dart'; +import 'package:photos/ui/notification/toast.dart'; import 'package:photos/utils/dialog_util.dart'; -import 'package:photos/utils/toast_util.dart'; class RecoveryPage extends StatefulWidget { - const RecoveryPage({Key? key}) : super(key: key); + const RecoveryPage({super.key}); @override State createState() => _RecoveryPageState(); diff --git a/mobile/lib/ui/account/request_pwd_verification_page.dart b/mobile/lib/ui/account/request_pwd_verification_page.dart index e29d568867..a9b5063495 100644 --- a/mobile/lib/ui/account/request_pwd_verification_page.dart +++ b/mobile/lib/ui/account/request_pwd_verification_page.dart @@ -1,13 +1,13 @@ import "dart:convert"; import "dart:typed_data"; +import "package:ente_crypto/ente_crypto.dart"; import 'package:flutter/material.dart'; import "package:logging/logging.dart"; import 'package:photos/core/configuration.dart'; import "package:photos/l10n/l10n.dart"; import "package:photos/theme/ente_theme.dart"; import 'package:photos/ui/common/dynamic_fab.dart'; -import "package:photos/utils/crypto_util.dart"; import "package:photos/utils/dialog_util.dart"; typedef OnPasswordVerifiedFn = Future Function(Uint8List bytes); @@ -91,7 +91,7 @@ class _RequestPasswordVerificationPageState try { final attributes = Configuration.instance.getKeyAttributes()!; final Uint8List keyEncryptionKey = await CryptoUtil.deriveKey( - utf8.encode(_passwordController.text) as Uint8List, + utf8.encode(_passwordController.text), CryptoUtil.base642bin(attributes.kekSalt), attributes.memLimit!, attributes.opsLimit!, diff --git a/mobile/lib/ui/account/sessions_page.dart b/mobile/lib/ui/account/sessions_page.dart index 4cac67d671..ed3c48407b 100644 --- a/mobile/lib/ui/account/sessions_page.dart +++ b/mobile/lib/ui/account/sessions_page.dart @@ -3,16 +3,16 @@ import 'package:logging/logging.dart'; import 'package:photos/core/configuration.dart'; import 'package:photos/ente_theme_data.dart'; import "package:photos/generated/l10n.dart"; -import 'package:photos/models/sessions.dart'; -import 'package:photos/services/user_service.dart'; +import 'package:photos/models/api/user/sessions.dart'; +import 'package:photos/services/account/user_service.dart'; import "package:photos/theme/ente_theme.dart"; import 'package:photos/ui/common/loading_widget.dart'; -import "package:photos/utils/date_time_util.dart"; +import 'package:photos/ui/notification/toast.dart'; import 'package:photos/utils/dialog_util.dart'; -import 'package:photos/utils/toast_util.dart'; +import "package:photos/utils/standalone/date_time.dart"; class SessionsPage extends StatefulWidget { - const SessionsPage({Key? key}) : super(key: key); + const SessionsPage({super.key}); @override State createState() => _SessionsPageState(); diff --git a/mobile/lib/ui/account/two_factor_authentication_page.dart b/mobile/lib/ui/account/two_factor_authentication_page.dart index 84ab13a37a..bb9b0d9361 100644 --- a/mobile/lib/ui/account/two_factor_authentication_page.dart +++ b/mobile/lib/ui/account/two_factor_authentication_page.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import "package:photos/generated/l10n.dart"; import "package:photos/models/account/two_factor.dart"; -import 'package:photos/services/user_service.dart'; +import 'package:photos/services/account/user_service.dart'; import 'package:photos/ui/lifecycle_event_handler.dart'; import "package:pinput/pinput.dart"; diff --git a/mobile/lib/ui/account/two_factor_recovery_page.dart b/mobile/lib/ui/account/two_factor_recovery_page.dart index 8c106025b5..8beea1da75 100644 --- a/mobile/lib/ui/account/two_factor_recovery_page.dart +++ b/mobile/lib/ui/account/two_factor_recovery_page.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import "package:photos/generated/l10n.dart"; import "package:photos/models/account/two_factor.dart"; -import 'package:photos/services/user_service.dart'; +import 'package:photos/services/account/user_service.dart'; import "package:photos/theme/ente_theme.dart"; import 'package:photos/utils/dialog_util.dart'; @@ -16,8 +16,8 @@ class TwoFactorRecoveryPage extends StatefulWidget { this.sessionID, this.encryptedSecret, this.secretDecryptionNonce, { - Key? key, - }) : super(key: key); + super.key, + }); @override State createState() => _TwoFactorRecoveryPageState(); diff --git a/mobile/lib/ui/account/two_factor_setup_page.dart b/mobile/lib/ui/account/two_factor_setup_page.dart index ac8c8eba0a..5817b4afed 100644 --- a/mobile/lib/ui/account/two_factor_setup_page.dart +++ b/mobile/lib/ui/account/two_factor_setup_page.dart @@ -1,16 +1,16 @@ import 'dart:async'; +import 'package:ente_crypto/ente_crypto.dart'; import "package:flutter/material.dart"; import 'package:flutter/services.dart'; import 'package:photos/core/configuration.dart'; import 'package:photos/ente_theme_data.dart'; import "package:photos/generated/l10n.dart"; -import 'package:photos/services/user_service.dart'; +import 'package:photos/services/account/user_service.dart'; import 'package:photos/ui/account/recovery_key_page.dart'; import 'package:photos/ui/lifecycle_event_handler.dart'; -import 'package:photos/utils/crypto_util.dart'; +import 'package:photos/ui/notification/toast.dart'; import 'package:photos/utils/navigation_util.dart'; -import 'package:photos/utils/toast_util.dart'; import "package:pinput/pinput.dart"; class TwoFactorSetupPage extends StatefulWidget { @@ -22,8 +22,8 @@ class TwoFactorSetupPage extends StatefulWidget { this.secretCode, this.qrCode, this.completer, { - Key? key, - }) : super(key: key); + super.key, + }); @override State createState() => _TwoFactorSetupPageState(); diff --git a/mobile/lib/ui/account/verify_recovery_page.dart b/mobile/lib/ui/account/verify_recovery_page.dart index 54c8595977..47bf0b150c 100644 --- a/mobile/lib/ui/account/verify_recovery_page.dart +++ b/mobile/lib/ui/account/verify_recovery_page.dart @@ -1,5 +1,6 @@ import 'package:bip39/bip39.dart' as bip39; import 'package:dio/dio.dart'; +import 'package:ente_crypto/ente_crypto.dart'; import 'package:flutter/material.dart'; import 'package:logging/logging.dart'; import 'package:photos/core/event_bus.dart'; @@ -7,18 +8,17 @@ import 'package:photos/ente_theme_data.dart'; import 'package:photos/events/notification_event.dart'; import "package:photos/generated/l10n.dart"; import "package:photos/service_locator.dart"; +import 'package:photos/services/account/user_service.dart'; import 'package:photos/services/local_authentication_service.dart'; -import 'package:photos/services/user_service.dart'; import "package:photos/theme/ente_theme.dart"; import 'package:photos/ui/account/recovery_key_page.dart'; import 'package:photos/ui/common/gradient_button.dart'; import 'package:photos/ui/components/buttons/button_widget.dart'; -import 'package:photos/utils/crypto_util.dart'; import 'package:photos/utils/dialog_util.dart'; import 'package:photos/utils/navigation_util.dart'; class VerifyRecoveryPage extends StatefulWidget { - const VerifyRecoveryPage({Key? key}) : super(key: key); + const VerifyRecoveryPage({super.key}); @override State createState() => _VerifyRecoveryPageState(); @@ -40,10 +40,10 @@ class _VerifyRecoveryPageState extends State { final String recoveryKeyWords = bip39.entropyToMnemonic(recoveryKey); if (inputKey == recoveryKey || inputKey == recoveryKeyWords) { try { - await userRemoteFlagService.markRecoveryVerificationAsDone(); + await flagService.setRecoveryKeyVerified(true); } catch (e) { await dialog.hide(); - if (e is DioError && e.type == DioErrorType.other) { + if (e is DioException && e.type == DioExceptionType.connectionError) { await showErrorDialog( context, S.of(context).noInternetConnection, diff --git a/mobile/lib/ui/actions/collection/collection_file_actions.dart b/mobile/lib/ui/actions/collection/collection_file_actions.dart index 81b79825ac..cf28e6687c 100644 --- a/mobile/lib/ui/actions/collection/collection_file_actions.dart +++ b/mobile/lib/ui/actions/collection/collection_file_actions.dart @@ -14,16 +14,16 @@ import "package:photos/services/collections_service.dart"; import 'package:photos/services/favorites_service.dart'; import "package:photos/services/hidden_service.dart"; import "package:photos/services/ignored_files_service.dart"; -import "package:photos/services/remote_sync_service.dart"; +import "package:photos/services/sync/remote_sync_service.dart"; import 'package:photos/ui/actions/collection/collection_sharing_actions.dart'; import 'package:photos/ui/common/progress_dialog.dart'; import 'package:photos/ui/components/action_sheet_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/utils/dialog_util.dart'; import "package:photos/utils/file_uploader.dart"; import "package:photos/utils/share_util.dart"; -import 'package:photos/utils/toast_util.dart'; import "package:receive_sharing_intent/receive_sharing_intent.dart"; extension CollectionFileActions on CollectionActions { @@ -147,7 +147,7 @@ extension CollectionFileActions on CollectionActions { // Newly created collection might not be cached final Collection? c = CollectionsService.instance.getCollectionByID(collectionID); - if (c != null && c.owner!.id != currentUserID) { + if (c != null && c.owner.id != currentUserID) { if (!showProgressDialog) { dialog = createProgressDialog( context, diff --git a/mobile/lib/ui/actions/collection/collection_sharing_actions.dart b/mobile/lib/ui/actions/collection/collection_sharing_actions.dart index c2c354cf77..87b552269d 100644 --- a/mobile/lib/ui/actions/collection/collection_sharing_actions.dart +++ b/mobile/lib/ui/actions/collection/collection_sharing_actions.dart @@ -14,9 +14,9 @@ import 'package:photos/models/file/file.dart'; import 'package:photos/models/files_split.dart'; import "package:photos/models/metadata/collection_magic.dart"; import "package:photos/models/metadata/common_keys.dart"; +import 'package:photos/services/account/user_service.dart'; import 'package:photos/services/collections_service.dart'; import 'package:photos/services/hidden_service.dart'; -import 'package:photos/services/user_service.dart'; import 'package:photos/theme/colors.dart'; import 'package:photos/theme/ente_theme.dart'; import 'package:photos/ui/common/progress_dialog.dart'; @@ -25,12 +25,12 @@ import 'package:photos/ui/components/action_sheet_widget.dart'; 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/payment/subscription.dart'; -import 'package:photos/utils/date_time_util.dart'; import 'package:photos/utils/dialog_util.dart'; import 'package:photos/utils/email_util.dart'; import 'package:photos/utils/share_util.dart'; -import 'package:photos/utils/toast_util.dart'; +import 'package:photos/utils/standalone/date_time.dart'; import "package:styled_text/styled_text.dart"; class CollectionActions { @@ -340,7 +340,7 @@ class CollectionActions { ) async { final textTheme = getEnteTextTheme(bContext); final currentUserID = Configuration.instance.getUserID()!; - if (collection.owner!.id != currentUserID) { + if (collection.owner.id != currentUserID) { throw AssertionError("Can not delete album owned by others"); } if (collection.hasSharees) { @@ -495,7 +495,7 @@ class CollectionActions { bool isHidden = false, }) async { final int currentUserID = Configuration.instance.getUserID()!; - final isCollectionOwner = collection.owner!.id == currentUserID; + final isCollectionOwner = collection.owner.id == currentUserID; final FilesSplit split = FilesSplit.split( files, Configuration.instance.getUserID()!, @@ -631,7 +631,7 @@ class CollectionActions { if (targetCollection == null || (CollectionType.uncategorized == targetCollection.type || targetCollection.type == CollectionType.favorites) || - targetCollection.owner!.id != userID) { + targetCollection.owner.id != userID) { return false; } return true; diff --git a/mobile/lib/ui/actions/file/file_actions.dart b/mobile/lib/ui/actions/file/file_actions.dart index 03f8a2d3ac..60dfeaf739 100644 --- a/mobile/lib/ui/actions/file/file_actions.dart +++ b/mobile/lib/ui/actions/file/file_actions.dart @@ -10,11 +10,11 @@ import "package:photos/theme/ente_theme.dart"; import "package:photos/ui/components/action_sheet_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/viewer/file/file_details_widget.dart'; import "package:photos/utils/delete_file_util.dart"; import "package:photos/utils/dialog_util.dart"; import "package:photos/utils/panorama_util.dart"; -import "package:photos/utils/toast_util.dart"; Future showSingleFileDeleteSheet( BuildContext context, diff --git a/mobile/lib/ui/cast/auto.dart b/mobile/lib/ui/cast/auto.dart index 34c97b34de..bb1fc465b6 100644 --- a/mobile/lib/ui/cast/auto.dart +++ b/mobile/lib/ui/cast/auto.dart @@ -14,8 +14,8 @@ class AutoCastDialog extends StatefulWidget { final void Function(String) onConnect; AutoCastDialog( this.onConnect, { - Key? key, - }) : super(key: key) {} + super.key, + }) {} @override State createState() => _AutoCastDialogState(); diff --git a/mobile/lib/ui/cast/choose.dart b/mobile/lib/ui/cast/choose.dart index e0997f4403..5cf3368dce 100644 --- a/mobile/lib/ui/cast/choose.dart +++ b/mobile/lib/ui/cast/choose.dart @@ -7,8 +7,8 @@ import "package:photos/ui/components/models/button_type.dart"; class CastChooseDialog extends StatefulWidget { const CastChooseDialog({ - Key? key, - }) : super(key: key); + super.key, + }); @override State createState() => _CastChooseDialogState(); diff --git a/mobile/lib/ui/collections/album/horizontal_list.dart b/mobile/lib/ui/collections/album/horizontal_list.dart index 0021d6836a..53ea3232e0 100644 --- a/mobile/lib/ui/collections/album/horizontal_list.dart +++ b/mobile/lib/ui/collections/album/horizontal_list.dart @@ -17,8 +17,8 @@ class AlbumHorizontalList extends StatefulWidget { const AlbumHorizontalList( this.collectionsFuture, { this.hasVerifiedLock, - Key? key, - }) : super(key: key); + super.key, + }); @override State createState() => _AlbumHorizontalListState(); diff --git a/mobile/lib/ui/collections/album/row_item.dart b/mobile/lib/ui/collections/album/row_item.dart index 50b96420ef..5f684d3780 100644 --- a/mobile/lib/ui/collections/album/row_item.dart +++ b/mobile/lib/ui/collections/album/row_item.dart @@ -41,7 +41,7 @@ class AlbumRowItemWidget extends StatelessWidget { final Widget? linkIcon = c.hasLink && isOwner ? Icon( Icons.link, - color: c.publicURLs!.first!.isExpired ? warning500 : strokeBaseDark, + color: c.publicURLs.first.isExpired ? warning500 : strokeBaseDark, ) : null; return GestureDetector( @@ -115,7 +115,7 @@ class AlbumRowItemWidget extends StatelessWidget { bottom: 8.0, ), child: UserAvatarWidget( - c.owner!, + c.owner, thumbnailView: true, ), ), diff --git a/mobile/lib/ui/collections/album/vertical_list.dart b/mobile/lib/ui/collections/album/vertical_list.dart index 17a0308ce0..78bcdc8b2f 100644 --- a/mobile/lib/ui/collections/album/vertical_list.dart +++ b/mobile/lib/ui/collections/album/vertical_list.dart @@ -11,18 +11,18 @@ import 'package:photos/models/collection/collection_items.dart'; import 'package:photos/models/selected_files.dart'; import 'package:photos/services/collections_service.dart'; import "package:photos/services/hidden_service.dart"; -import 'package:photos/services/remote_sync_service.dart'; +import 'package:photos/services/sync/remote_sync_service.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/column_item.dart"; import "package:photos/ui/collections/album/new_list_item.dart"; import 'package:photos/ui/collections/collection_action_sheet.dart'; +import 'package:photos/ui/notification/toast.dart'; import "package:photos/ui/sharing/share_collection_page.dart"; import 'package:photos/ui/viewer/gallery/collection_page.dart'; import "package:photos/ui/viewer/gallery/empty_state.dart"; import 'package:photos/utils/dialog_util.dart'; import 'package:photos/utils/navigation_util.dart'; -import 'package:photos/utils/toast_util.dart'; import 'package:receive_sharing_intent/receive_sharing_intent.dart'; class AlbumVerticalListWidget extends StatelessWidget { @@ -287,8 +287,8 @@ class AlbumVerticalListWidget extends StatelessWidget { CollectionActions(CollectionsService.instance); if (collection.hasLink) { - if (collection.publicURLs!.first!.enableCollect) { - if (Configuration.instance.getUserID() == collection.owner!.id) { + if (collection.publicURLs.first.enableCollect) { + if (Configuration.instance.getUserID() == collection.owner.id) { unawaited( routeToPage( context, @@ -334,7 +334,7 @@ class AlbumVerticalListWidget extends StatelessWidget { context, S.of(context).collaborativeLinkCreatedFor(collection.displayName), ); - if (Configuration.instance.getUserID() == collection.owner!.id) { + if (Configuration.instance.getUserID() == collection.owner.id) { unawaited( routeToPage( context, @@ -353,7 +353,7 @@ class AlbumVerticalListWidget extends StatelessWidget { BuildContext context, Collection collection, ) { - if (Configuration.instance.getUserID() == collection.owner!.id) { + if (Configuration.instance.getUserID() == collection.owner.id) { unawaited( routeToPage( context, diff --git a/mobile/lib/ui/collections/button/archived_button.dart b/mobile/lib/ui/collections/button/archived_button.dart index 16957dfe09..6676d4dcc5 100644 --- a/mobile/lib/ui/collections/button/archived_button.dart +++ b/mobile/lib/ui/collections/button/archived_button.dart @@ -12,8 +12,8 @@ class ArchivedCollectionsButton extends StatelessWidget { const ArchivedCollectionsButton( this.textStyle, { - Key? key, - }) : super(key: key); + super.key, + }); @override Widget build(BuildContext context) { @@ -21,7 +21,7 @@ class ArchivedCollectionsButton extends StatelessWidget { CollectionsService.instance.getHiddenCollectionIds(); return OutlinedButton( style: OutlinedButton.styleFrom( - backgroundColor: Theme.of(context).colorScheme.background, + backgroundColor: Theme.of(context).colorScheme.surface, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), diff --git a/mobile/lib/ui/collections/button/hidden_button.dart b/mobile/lib/ui/collections/button/hidden_button.dart index a721aefd8a..8172d3c6a2 100644 --- a/mobile/lib/ui/collections/button/hidden_button.dart +++ b/mobile/lib/ui/collections/button/hidden_button.dart @@ -16,7 +16,7 @@ class HiddenCollectionsButtonWidget extends StatelessWidget { Widget build(BuildContext context) { return OutlinedButton( style: OutlinedButton.styleFrom( - backgroundColor: Theme.of(context).colorScheme.background, + backgroundColor: Theme.of(context).colorScheme.surface, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), diff --git a/mobile/lib/ui/collections/button/trash_button.dart b/mobile/lib/ui/collections/button/trash_button.dart index 3ba73ac97c..b6e71006fc 100644 --- a/mobile/lib/ui/collections/button/trash_button.dart +++ b/mobile/lib/ui/collections/button/trash_button.dart @@ -45,7 +45,7 @@ class _TrashSectionButtonState extends State { Widget build(BuildContext context) { return OutlinedButton( style: OutlinedButton.styleFrom( - backgroundColor: Theme.of(context).colorScheme.background, + backgroundColor: Theme.of(context).colorScheme.surface, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), diff --git a/mobile/lib/ui/collections/button/uncategorized_button.dart b/mobile/lib/ui/collections/button/uncategorized_button.dart index f474bc6a1d..29800df76e 100644 --- a/mobile/lib/ui/collections/button/uncategorized_button.dart +++ b/mobile/lib/ui/collections/button/uncategorized_button.dart @@ -12,8 +12,8 @@ class UnCategorizedCollections extends StatelessWidget { const UnCategorizedCollections( this.textStyle, { - Key? key, - }) : super(key: key); + super.key, + }); @override Widget build(BuildContext context) { @@ -26,7 +26,7 @@ class UnCategorizedCollections extends StatelessWidget { } return OutlinedButton( style: OutlinedButton.styleFrom( - backgroundColor: Theme.of(context).colorScheme.background, + backgroundColor: Theme.of(context).colorScheme.surface, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), diff --git a/mobile/lib/ui/collections/collection_list_page.dart b/mobile/lib/ui/collections/collection_list_page.dart index f7308e331b..974acdfdbb 100644 --- a/mobile/lib/ui/collections/collection_list_page.dart +++ b/mobile/lib/ui/collections/collection_list_page.dart @@ -27,8 +27,8 @@ class CollectionListPage extends StatefulWidget { this.appTitle, this.initialScrollOffset, this.tag = "", - Key? key, - }) : super(key: key); + super.key, + }); @override State createState() => _CollectionListPageState(); diff --git a/mobile/lib/ui/collections/device/device_folder_item.dart b/mobile/lib/ui/collections/device/device_folder_item.dart index 726d784fe4..64d0eef739 100644 --- a/mobile/lib/ui/collections/device/device_folder_item.dart +++ b/mobile/lib/ui/collections/device/device_folder_item.dart @@ -13,8 +13,8 @@ class DeviceFolderItem extends StatelessWidget { this.deviceCollection, { ///120 is default for the 'on device' scrollview in albums section this.sideOfThumbnail = 120, - Key? key, - }) : super(key: key); + super.key, + }); @override Widget build(BuildContext context) { diff --git a/mobile/lib/ui/collections/device/device_folders_grid_view.dart b/mobile/lib/ui/collections/device/device_folders_grid_view.dart index 6f4ac30f0f..d6c04e0bed 100644 --- a/mobile/lib/ui/collections/device/device_folders_grid_view.dart +++ b/mobile/lib/ui/collections/device/device_folders_grid_view.dart @@ -12,7 +12,7 @@ import 'package:photos/models/device_collection.dart'; import "package:photos/ui/collections/device/device_folder_item.dart"; import 'package:photos/ui/common/loading_widget.dart'; import 'package:photos/ui/viewer/gallery/empty_state.dart'; -import "package:photos/utils/debouncer.dart"; +import "package:photos/utils/standalone/debouncer.dart"; class DeviceFoldersGridView extends StatefulWidget { const DeviceFoldersGridView({ 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 d5ac85e0ea..d42bad8dd5 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 @@ -13,7 +13,7 @@ import 'package:photos/models/device_collection.dart'; import "package:photos/ui/collections/device/device_folder_item.dart"; import 'package:photos/ui/common/loading_widget.dart'; import 'package:photos/ui/viewer/gallery/empty_state.dart'; -import "package:photos/utils/debouncer.dart"; +import "package:photos/utils/standalone/debouncer.dart"; class DeviceFolderVerticalGridView extends StatelessWidget { final Widget? appTitle; @@ -44,9 +44,7 @@ class DeviceFolderVerticalGridView extends StatelessWidget { } class _DeviceFolderVerticalGridViewBody extends StatefulWidget { - const _DeviceFolderVerticalGridViewBody({ - Key? key, - }) : super(key: key); + const _DeviceFolderVerticalGridViewBody(); @override State<_DeviceFolderVerticalGridViewBody> createState() => diff --git a/mobile/lib/ui/collections/flex_grid_view.dart b/mobile/lib/ui/collections/flex_grid_view.dart index a0c3a0e139..4114ff0493 100644 --- a/mobile/lib/ui/collections/flex_grid_view.dart +++ b/mobile/lib/ui/collections/flex_grid_view.dart @@ -28,8 +28,8 @@ class CollectionsFlexiGridViewWidget extends StatelessWidget { this.displayLimitCount = 10, this.shrinkWrap = false, this.tag = "", - Key? key, - }) : super(key: key); + super.key, + }); @override Widget build(BuildContext context) { diff --git a/mobile/lib/ui/collections/new_album_icon.dart b/mobile/lib/ui/collections/new_album_icon.dart index 61b7355a9f..4e18123b66 100644 --- a/mobile/lib/ui/collections/new_album_icon.dart +++ b/mobile/lib/ui/collections/new_album_icon.dart @@ -17,8 +17,8 @@ class NewAlbumIcon extends StatelessWidget { required this.icon, required this.iconButtonType, this.color, - Key? key, - }) : super(key: key); + super.key, + }); @override Widget build(BuildContext context) { diff --git a/mobile/lib/ui/common/bottom_shadow.dart b/mobile/lib/ui/common/bottom_shadow.dart index f6e8bee046..2de7e944e2 100644 --- a/mobile/lib/ui/common/bottom_shadow.dart +++ b/mobile/lib/ui/common/bottom_shadow.dart @@ -13,7 +13,7 @@ class BottomShadowWidget extends StatelessWidget { color: Colors.transparent, boxShadow: [ BoxShadow( - color: shadowColor ?? Theme.of(context).colorScheme.background, + color: shadowColor ?? Theme.of(context).colorScheme.surface, spreadRadius: 42, blurRadius: 42, offset: Offset(0, offsetDy), // changes position of shadow diff --git a/mobile/lib/ui/common/dynamic_fab.dart b/mobile/lib/ui/common/dynamic_fab.dart index d1a7cf6479..b19fcb654a 100644 --- a/mobile/lib/ui/common/dynamic_fab.dart +++ b/mobile/lib/ui/common/dynamic_fab.dart @@ -10,12 +10,12 @@ class DynamicFAB extends StatelessWidget { final Function? onPressedFunction; const DynamicFAB({ - Key? key, + super.key, this.isKeypadOpen, this.buttonText, this.isFormValid, this.onPressedFunction, - }) : super(key: key); + }); @override Widget build(BuildContext context) { @@ -24,7 +24,7 @@ class DynamicFAB extends StatelessWidget { decoration: BoxDecoration( boxShadow: [ BoxShadow( - color: Theme.of(context).colorScheme.background, + color: Theme.of(context).colorScheme.surface, spreadRadius: 200, blurRadius: 100, offset: const Offset(0, 230), diff --git a/mobile/lib/ui/common/fast_scroll_physics.dart b/mobile/lib/ui/common/fast_scroll_physics.dart index 97771af242..139bdca9ad 100644 --- a/mobile/lib/ui/common/fast_scroll_physics.dart +++ b/mobile/lib/ui/common/fast_scroll_physics.dart @@ -3,8 +3,7 @@ import 'package:flutter/material.dart'; class FastScrollPhysics extends PageScrollPhysics { final double speedFactor; - const FastScrollPhysics({this.speedFactor = 2.0, ScrollPhysics? parent}) - : super(parent: parent); + const FastScrollPhysics({this.speedFactor = 2.0, super.parent}); @override FastScrollPhysics applyTo(ScrollPhysics? ancestor) { diff --git a/mobile/lib/ui/common/gradient_button.dart b/mobile/lib/ui/common/gradient_button.dart index af9ca07c70..e96a460ef6 100644 --- a/mobile/lib/ui/common/gradient_button.dart +++ b/mobile/lib/ui/common/gradient_button.dart @@ -15,7 +15,7 @@ class GradientButton extends StatelessWidget { final double paddingValue; const GradientButton({ - Key? key, + super.key, this.linearGradientColors = const [ Color(0xFF2CD267), Color(0xFF1DB954), @@ -24,7 +24,7 @@ class GradientButton extends StatelessWidget { this.text = '', this.iconData, this.paddingValue = 0.0, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/mobile/lib/ui/common/linear_progress_dialog.dart b/mobile/lib/ui/common/linear_progress_dialog.dart index 375eebe48c..714ad73155 100644 --- a/mobile/lib/ui/common/linear_progress_dialog.dart +++ b/mobile/lib/ui/common/linear_progress_dialog.dart @@ -4,7 +4,7 @@ import 'package:photos/ente_theme_data.dart'; class LinearProgressDialog extends StatefulWidget { final String message; - const LinearProgressDialog(this.message, {Key? key}) : super(key: key); + const LinearProgressDialog(this.message, {super.key}); @override LinearProgressDialogState createState() => LinearProgressDialogState(); diff --git a/mobile/lib/ui/common/web_page.dart b/mobile/lib/ui/common/web_page.dart index 1566f6cba0..ee14b1e3e3 100644 --- a/mobile/lib/ui/common/web_page.dart +++ b/mobile/lib/ui/common/web_page.dart @@ -14,9 +14,9 @@ class WebPage extends StatefulWidget { const WebPage( this.title, this.url, { - Key? key, + super.key, this.canOpenInBrowser = false, - }) : super(key: key); + }); @override State createState() => _WebPageState(); diff --git a/mobile/lib/ui/components/buttons/button_widget.dart b/mobile/lib/ui/components/buttons/button_widget.dart index 7590735b10..84012da5b8 100644 --- a/mobile/lib/ui/components/buttons/button_widget.dart +++ b/mobile/lib/ui/components/buttons/button_widget.dart @@ -9,8 +9,8 @@ import 'package:photos/theme/text_style.dart'; import 'package:photos/ui/common/loading_widget.dart'; import 'package:photos/ui/components/models/button_type.dart'; import 'package:photos/ui/components/models/custom_button_style.dart'; -import 'package:photos/utils/debouncer.dart'; import "package:photos/utils/dialog_util.dart"; +import 'package:photos/utils/standalone/debouncer.dart'; enum ButtonSize { small, diff --git a/mobile/lib/ui/components/captioned_text_widget.dart b/mobile/lib/ui/components/captioned_text_widget.dart index abec18fb84..d10810eaa0 100644 --- a/mobile/lib/ui/components/captioned_text_widget.dart +++ b/mobile/lib/ui/components/captioned_text_widget.dart @@ -15,8 +15,8 @@ class CaptionedTextWidget extends StatelessWidget { this.makeTextBold = false, this.textColor, this.subTitleColor, - Key? key, - }) : super(key: key); + super.key, + }); @override Widget build(BuildContext context) { diff --git a/mobile/lib/ui/components/dialog_widget.dart b/mobile/lib/ui/components/dialog_widget.dart index 0dbf491177..1bab11669d 100644 --- a/mobile/lib/ui/components/dialog_widget.dart +++ b/mobile/lib/ui/components/dialog_widget.dart @@ -78,17 +78,19 @@ class DialogWidget extends StatelessWidget { ), child: Padding( padding: const EdgeInsets.all(16), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - ContentContainer( - title: title, - body: body, - icon: icon, - ), - const SizedBox(height: 36), - Actions(buttons), - ], + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ContentContainer( + title: title, + body: body, + icon: icon, + ), + const SizedBox(height: 36), + Actions(buttons), + ], + ), ), ), ); diff --git a/mobile/lib/ui/components/home_header_widget.dart b/mobile/lib/ui/components/home_header_widget.dart index 88471e251a..7b65586554 100644 --- a/mobile/lib/ui/components/home_header_widget.dart +++ b/mobile/lib/ui/components/home_header_widget.dart @@ -5,7 +5,7 @@ import 'package:flutter/material.dart'; import "package:logging/logging.dart"; import "package:photo_manager/photo_manager.dart"; import "package:photos/generated/l10n.dart"; -import "package:photos/services/local_sync_service.dart"; +import "package:photos/services/sync/local_sync_service.dart"; import 'package:photos/ui/components/buttons/icon_button_widget.dart'; import "package:photos/ui/settings/backup/backup_folder_selection_page.dart"; import "package:photos/utils/dialog_util.dart"; @@ -14,8 +14,7 @@ import "package:photos/utils/photo_manager_util.dart"; class HomeHeaderWidget extends StatefulWidget { final Widget centerWidget; - const HomeHeaderWidget({required this.centerWidget, Key? key}) - : super(key: key); + const HomeHeaderWidget({required this.centerWidget, super.key}); @override State createState() => _HomeHeaderWidgetState(); diff --git a/mobile/lib/ui/components/menu_item_widget/menu_item_widget.dart b/mobile/lib/ui/components/menu_item_widget/menu_item_widget.dart index d1e87e26a5..dfd2479057 100644 --- a/mobile/lib/ui/components/menu_item_widget/menu_item_widget.dart +++ b/mobile/lib/ui/components/menu_item_widget/menu_item_widget.dart @@ -4,7 +4,7 @@ import 'package:photos/models/execution_states.dart'; import 'package:photos/models/typedefs.dart'; import 'package:photos/theme/ente_theme.dart'; import 'package:photos/ui/components/menu_item_widget/menu_item_child_widgets.dart'; -import 'package:photos/utils/debouncer.dart'; +import 'package:photos/utils/standalone/debouncer.dart'; class MenuItemWidget extends StatefulWidget { final Widget captionedTextWidget; @@ -88,8 +88,8 @@ class MenuItemWidget extends StatefulWidget { this.showOnlyLoadingState = false, this.surfaceExecutionStates = true, this.alwaysShowSuccessState = false, - Key? key, - }) : super(key: key); + super.key, + }); @override State createState() => _MenuItemWidgetState(); diff --git a/mobile/lib/ui/components/text_input_widget.dart b/mobile/lib/ui/components/text_input_widget.dart index 965de67c48..4a59bbe7ee 100644 --- a/mobile/lib/ui/components/text_input_widget.dart +++ b/mobile/lib/ui/components/text_input_widget.dart @@ -5,8 +5,8 @@ import 'package:photos/models/execution_states.dart'; import 'package:photos/models/typedefs.dart'; import 'package:photos/theme/ente_theme.dart'; import 'package:photos/ui/common/loading_widget.dart'; -import 'package:photos/utils/debouncer.dart'; import 'package:photos/utils/separators_util.dart'; +import 'package:photos/utils/standalone/debouncer.dart'; ///To show wrong password state, throw an exception with the message ///"Incorrect password" in onSubmit. diff --git a/mobile/lib/ui/components/toggle_switch_widget.dart b/mobile/lib/ui/components/toggle_switch_widget.dart index fb132cedf0..ec631f6fa8 100644 --- a/mobile/lib/ui/components/toggle_switch_widget.dart +++ b/mobile/lib/ui/components/toggle_switch_widget.dart @@ -6,7 +6,7 @@ import 'package:photos/ente_theme_data.dart'; import 'package:photos/models/execution_states.dart'; import 'package:photos/models/typedefs.dart'; import 'package:photos/ui/common/loading_widget.dart'; -import 'package:photos/utils/debouncer.dart'; +import 'package:photos/utils/standalone/debouncer.dart'; class ToggleSwitchWidget extends StatefulWidget { final BoolCallBack value; @@ -14,8 +14,8 @@ class ToggleSwitchWidget extends StatefulWidget { const ToggleSwitchWidget({ required this.value, required this.onChanged, - Key? key, - }) : super(key: key); + super.key, + }); @override State createState() => _ToggleSwitchWidgetState(); @@ -58,7 +58,7 @@ class _ToggleSwitchWidgetState extends State { activeTrackColor: enteColorScheme.primary500, activeColor: Colors.white, inactiveThumbColor: enteColorScheme.primary500, - trackOutlineColor: MaterialStateColor.resolveWith( + trackOutlineColor: WidgetStateColor.resolveWith( (states) => enteColorScheme.primary500, ), materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, diff --git a/mobile/lib/ui/extents_page_view.dart b/mobile/lib/ui/extents_page_view.dart index dbf1b2f3fc..1ed3572c9e 100644 --- a/mobile/lib/ui/extents_page_view.dart +++ b/mobile/lib/ui/extents_page_view.dart @@ -42,7 +42,7 @@ class ExtentsPageView extends StatefulWidget { /// child that could possibly be displayed in the page view, instead of just /// those children that are actually visible. ExtentsPageView({ - Key? key, + super.key, this.scrollDirection = Axis.horizontal, this.reverse = false, required this.controller, @@ -53,8 +53,7 @@ class ExtentsPageView extends StatefulWidget { this.dragStartBehavior = DragStartBehavior.start, this.openDrawer, }) : childrenDelegate = SliverChildListDelegate(children), - extents = children.length, - super(key: key); + extents = children.length; /// Creates a scrollable list that works page by page using widgets that are /// created on demand. @@ -73,7 +72,7 @@ class ExtentsPageView extends StatefulWidget { /// you are planning to change child order at a later time, consider using /// [PageView] or [PageView.custom]. ExtentsPageView.builder({ - Key? key, + super.key, this.scrollDirection = Axis.horizontal, this.reverse = false, required this.controller, @@ -86,11 +85,10 @@ class ExtentsPageView extends StatefulWidget { this.openDrawer, }) : childrenDelegate = SliverChildBuilderDelegate(itemBuilder, childCount: itemCount), - extents = 0, - super(key: key); + extents = 0; ExtentsPageView.extents({ - Key? key, + super.key, this.extents = 1, this.scrollDirection = Axis.horizontal, this.reverse = false, @@ -107,8 +105,7 @@ class ExtentsPageView extends StatefulWidget { childCount: itemCount, addAutomaticKeepAlives: false, addRepaintBoundaries: false, - ), - super(key: key); + ); /// Creates a scrollable list that works page by page with a custom child /// model. @@ -191,7 +188,7 @@ class ExtentsPageView extends StatefulWidget { /// ``` /// {@end-tool} const ExtentsPageView.custom({ - Key? key, + super.key, this.scrollDirection = Axis.horizontal, this.reverse = false, required this.controller, @@ -201,8 +198,7 @@ class ExtentsPageView extends StatefulWidget { required this.childrenDelegate, this.dragStartBehavior = DragStartBehavior.start, this.openDrawer, - }) : extents = 0, - super(key: key); + }) : extents = 0; /// The number of pages to build off screen. /// diff --git a/mobile/lib/ui/growth/referral_code_widget.dart b/mobile/lib/ui/growth/referral_code_widget.dart index cb139c7263..7620aff22d 100644 --- a/mobile/lib/ui/growth/referral_code_widget.dart +++ b/mobile/lib/ui/growth/referral_code_widget.dart @@ -108,7 +108,7 @@ class ReferralCodeWidget extends StatelessWidget { notifyParent?.call(); } catch (e, s) { Logger("ReferralCodeWidget").severe("Failed to update code", e, s); - if (e is DioError) { + if (e is DioException) { if (e.response?.statusCode == 400) { await showInfoDialog( context, diff --git a/mobile/lib/ui/growth/referral_screen.dart b/mobile/lib/ui/growth/referral_screen.dart index 6a913415bd..0d82c54edb 100644 --- a/mobile/lib/ui/growth/referral_screen.dart +++ b/mobile/lib/ui/growth/referral_screen.dart @@ -3,7 +3,7 @@ import "package:photos/generated/l10n.dart"; import "package:photos/models/api/storage_bonus/storage_bonus.dart"; import "package:photos/models/user_details.dart"; import "package:photos/service_locator.dart"; -import "package:photos/services/user_service.dart"; +import "package:photos/services/account/user_service.dart"; import "package:photos/theme/ente_theme.dart"; import "package:photos/ui/common/loading_widget.dart"; import "package:photos/ui/common/web_page.dart"; @@ -16,9 +16,9 @@ import "package:photos/ui/components/title_bar_widget.dart"; import "package:photos/ui/growth/apply_code_screen.dart"; import "package:photos/ui/growth/referral_code_widget.dart"; import "package:photos/ui/growth/storage_details_screen.dart"; -import "package:photos/utils/data_util.dart"; import "package:photos/utils/navigation_util.dart"; import "package:photos/utils/share_util.dart"; +import "package:photos/utils/standalone/data.dart"; import "package:tuple/tuple.dart"; class ReferralScreen extends StatefulWidget { diff --git a/mobile/lib/ui/growth/storage_details_screen.dart b/mobile/lib/ui/growth/storage_details_screen.dart index 286772593c..c1c9bcf1a0 100644 --- a/mobile/lib/ui/growth/storage_details_screen.dart +++ b/mobile/lib/ui/growth/storage_details_screen.dart @@ -10,7 +10,7 @@ import "package:photos/ui/common/loading_widget.dart"; import 'package:photos/ui/components/buttons/icon_button_widget.dart'; import "package:photos/ui/components/title_bar_title_widget.dart"; import "package:photos/ui/components/title_bar_widget.dart"; -import "package:photos/utils/data_util.dart"; +import "package:photos/utils/standalone/data.dart"; class StorageDetailsScreen extends StatefulWidget { final ReferralView referralView; diff --git a/mobile/lib/ui/home/grant_permissions_widget.dart b/mobile/lib/ui/home/grant_permissions_widget.dart index 5a4238b1e5..9935b3c332 100644 --- a/mobile/lib/ui/home/grant_permissions_widget.dart +++ b/mobile/lib/ui/home/grant_permissions_widget.dart @@ -6,7 +6,7 @@ import "package:logging/logging.dart"; import 'package:photo_manager/photo_manager.dart'; import "package:photos/generated/l10n.dart"; import "package:photos/l10n/l10n.dart"; -import 'package:photos/services/sync_service.dart'; +import 'package:photos/services/sync/sync_service.dart'; import "package:photos/theme/ente_theme.dart"; import "package:photos/utils/dialog_util.dart"; import "package:photos/utils/photo_manager_util.dart"; diff --git a/mobile/lib/ui/home/header_error_widget.dart b/mobile/lib/ui/home/header_error_widget.dart index 3faf2304a7..381d1c1a95 100644 --- a/mobile/lib/ui/home/header_error_widget.dart +++ b/mobile/lib/ui/home/header_error_widget.dart @@ -9,9 +9,8 @@ import "package:photos/utils/navigation_util.dart"; class HeaderErrorWidget extends StatelessWidget { final Error? _error; - const HeaderErrorWidget({Key? key, required Error? error}) - : _error = error, - super(key: key); + const HeaderErrorWidget({super.key, required Error? error}) + : _error = error; @override Widget build(BuildContext context) { diff --git a/mobile/lib/ui/home/home_bottom_nav_bar.dart b/mobile/lib/ui/home/home_bottom_nav_bar.dart index aa1e0e1d9b..67a0d98cf9 100644 --- a/mobile/lib/ui/home/home_bottom_nav_bar.dart +++ b/mobile/lib/ui/home/home_bottom_nav_bar.dart @@ -12,8 +12,8 @@ class HomeBottomNavigationBar extends StatefulWidget { const HomeBottomNavigationBar( this.selectedFiles, { required this.selectedTabIndex, - Key? key, - }) : super(key: key); + super.key, + }); final SelectedFiles selectedFiles; final int selectedTabIndex; diff --git a/mobile/lib/ui/home/home_gallery_widget.dart b/mobile/lib/ui/home/home_gallery_widget.dart index 0095b13fbb..35bc5eb326 100644 --- a/mobile/lib/ui/home/home_gallery_widget.dart +++ b/mobile/lib/ui/home/home_gallery_widget.dart @@ -19,7 +19,7 @@ import 'package:photos/ui/viewer/actions/file_selection_overlay_bar.dart'; import 'package:photos/ui/viewer/gallery/gallery.dart'; import "package:photos/ui/viewer/gallery/state/gallery_files_inherited_widget.dart"; import "package:photos/ui/viewer/gallery/state/selection_state.dart"; -import "package:photos/utils/debouncer.dart"; +import "package:photos/utils/standalone/debouncer.dart"; class HomeGalleryWidget extends StatefulWidget { final Widget? header; diff --git a/mobile/lib/ui/home/landing_page_widget.dart b/mobile/lib/ui/home/landing_page_widget.dart index 29d0c97758..bdc1e8a03e 100644 --- a/mobile/lib/ui/home/landing_page_widget.dart +++ b/mobile/lib/ui/home/landing_page_widget.dart @@ -26,7 +26,7 @@ import "package:photos/utils/dialog_util.dart"; import "package:photos/utils/navigation_util.dart"; class LandingPageWidget extends StatefulWidget { - const LandingPageWidget({Key? key}) : super(key: key); + const LandingPageWidget({super.key}); @override State createState() => _LandingPageWidgetState(); @@ -317,8 +317,8 @@ class FeatureItemWidget extends StatelessWidget { this.featureTitleFirstLine, this.featureTitleSecondLine, this.subText, { - Key? key, - }) : super(key: key); + super.key, + }); @override Widget build(BuildContext context) { diff --git a/mobile/lib/ui/home/loading_photos_widget.dart b/mobile/lib/ui/home/loading_photos_widget.dart index 7ae293131f..2215cf41e2 100644 --- a/mobile/lib/ui/home/loading_photos_widget.dart +++ b/mobile/lib/ui/home/loading_photos_widget.dart @@ -7,7 +7,7 @@ import 'package:photos/ente_theme_data.dart'; import 'package:photos/events/local_import_progress.dart'; import 'package:photos/events/sync_status_update_event.dart'; import "package:photos/generated/l10n.dart"; -import 'package:photos/services/local_sync_service.dart'; +import 'package:photos/services/sync/local_sync_service.dart'; import 'package:photos/ui/common/bottom_shadow.dart'; import "package:photos/ui/components/buttons/button_widget.dart"; import "package:photos/ui/components/dialog_widget.dart"; diff --git a/mobile/lib/ui/home/memories/full_screen_memory.dart b/mobile/lib/ui/home/memories/full_screen_memory.dart index c8e69b58d8..bb92278911 100644 --- a/mobile/lib/ui/home/memories/full_screen_memory.dart +++ b/mobile/lib/ui/home/memories/full_screen_memory.dart @@ -5,7 +5,7 @@ import "package:flutter/cupertino.dart"; import "package:flutter/material.dart"; import "package:intl/intl.dart"; import "package:photos/core/configuration.dart"; -import "package:photos/models/memory.dart"; +import "package:photos/models/memories/memory.dart"; import "package:photos/services/memories_service.dart"; import "package:photos/theme/text_style.dart"; import "package:photos/ui/actions/file/file_actions.dart"; @@ -96,9 +96,9 @@ class FullScreenMemoryData extends InheritedWidget { required this.memories, required this.indexNotifier, required this.removeCurrentMemory, - required Widget child, - Key? key, - }) : super(child: child, key: key); + required super.child, + super.key, + }); static FullScreenMemoryData? of(BuildContext context) { return context.dependOnInheritedWidgetOfExactType(); diff --git a/mobile/lib/ui/home/memories/memories_widget.dart b/mobile/lib/ui/home/memories/memories_widget.dart index fbeb648ab6..ade6fcaaf5 100644 --- a/mobile/lib/ui/home/memories/memories_widget.dart +++ b/mobile/lib/ui/home/memories/memories_widget.dart @@ -4,7 +4,9 @@ import 'package:flutter/material.dart'; import "package:flutter_animate/flutter_animate.dart"; import "package:photos/core/event_bus.dart"; import "package:photos/events/memories_setting_changed.dart"; -import 'package:photos/models/memory.dart'; +import 'package:photos/models/memories/memory.dart'; +import "package:photos/models/memories/smart_memory.dart"; +import "package:photos/service_locator.dart"; import 'package:photos/services/memories_service.dart'; import "package:photos/ui/common/loading_widget.dart"; import 'package:photos/ui/home/memories/memory_cover_widget.dart'; @@ -55,6 +57,46 @@ class _MemoriesWidgetState extends State { if (!MemoriesService.instance.showMemories) { return const SizedBox.shrink(); } + if (memoriesCacheService.enableSmartMemories) { + return _smartMemories(); + } + return _oldMemories(); + } + + Widget _smartMemories() { + return FutureBuilder>( + future: memoriesCacheService.getMemories( + null, + ), + builder: (context, snapshot) { + if (snapshot.hasData && snapshot.data!.isEmpty) { + return _oldMemories(); + } + if (snapshot.hasError || !snapshot.hasData) { + return SizedBox( + height: _maxHeight + 12 + 10, + child: const EnteLoadingWidget(), + ); + } else { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox( + height: 12, + ), + _buildSmartMemories(snapshot.data!), + const SizedBox(height: 10), + ], + ).animate().fadeIn( + duration: const Duration(milliseconds: 250), + curve: Curves.easeInOutCirc, + ); + } + }, + ); + } + + Widget _oldMemories() { return FutureBuilder>( future: MemoriesService.instance.getMemories(), builder: (context, snapshot) { @@ -85,6 +127,38 @@ class _MemoriesWidgetState extends State { ); } + Widget _buildSmartMemories(List memories) { + final collatedMemories = + memories.map((e) => (e.memories, e.title)).toList(); + + return SizedBox( + height: _maxHeight + MemoryCoverWidget.outerStrokeWidth * 2, + child: ListView.builder( + physics: const AlwaysScrollableScrollPhysics( + parent: BouncingScrollPhysics(), + ), + scrollDirection: Axis.horizontal, + 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, + ); + }, + ), + ); + } + Widget _buildMemories(List memories) { final collatedMemories = _collateMemories(memories); diff --git a/mobile/lib/ui/home/memories/memory_cover_widget.dart b/mobile/lib/ui/home/memories/memory_cover_widget.dart index 0dfc9adf84..7d59112bb9 100644 --- a/mobile/lib/ui/home/memories/memory_cover_widget.dart +++ b/mobile/lib/ui/home/memories/memory_cover_widget.dart @@ -1,7 +1,7 @@ import "package:flutter/material.dart"; import "package:flutter/scheduler.dart"; import "package:photos/generated/l10n.dart"; -import "package:photos/models/memory.dart"; +import "package:photos/models/memories/memory.dart"; import "package:photos/theme/colors.dart"; import "package:photos/theme/effects.dart"; import "package:photos/theme/ente_theme.dart"; @@ -19,6 +19,7 @@ class MemoryCoverWidget extends StatefulWidget { static const aspectRatio = 0.68; static const horizontalPadding = 2.5; final double maxScaleOffsetX; + final String? title; const MemoryCoverWidget({ required this.memories, @@ -27,6 +28,7 @@ class MemoryCoverWidget extends StatefulWidget { required this.maxHeight, required this.maxWidth, required this.maxScaleOffsetX, + this.title, super.key, }); @@ -45,7 +47,12 @@ class _MemoryCoverWidgetState extends State { final widthOfScreen = MediaQuery.sizeOf(context).width; final index = _getNextMemoryIndex(); - final title = _getTitle(widget.memories[index]); + // TODO: lau: remove (I) from name when opening up the feature flag + final title = widget.title != null + ? widget.title! == "filler" + ? _getTitle(widget.memories[index]) + "(I)" + : widget.title! + "(I)" + : _getTitle(widget.memories[index]); final memory = widget.memories[index]; final isSeen = memory.isSeen(); final brightness = diff --git a/mobile/lib/ui/home/start_backup_hook_widget.dart b/mobile/lib/ui/home/start_backup_hook_widget.dart index 64dfa1fa21..5ef2db1e5e 100644 --- a/mobile/lib/ui/home/start_backup_hook_widget.dart +++ b/mobile/lib/ui/home/start_backup_hook_widget.dart @@ -3,7 +3,7 @@ import "dart:async"; import 'package:flutter/material.dart'; import 'package:photo_manager/photo_manager.dart'; import 'package:photos/generated/l10n.dart'; -import 'package:photos/services/local_sync_service.dart'; +import 'package:photos/services/sync/local_sync_service.dart'; import 'package:photos/ui/common/gradient_button.dart'; import 'package:photos/ui/settings/backup/backup_folder_selection_page.dart'; import 'package:photos/utils/navigation_util.dart'; diff --git a/mobile/lib/ui/home/status_bar_widget.dart b/mobile/lib/ui/home/status_bar_widget.dart index e967d7b0b5..15ba5b7141 100644 --- a/mobile/lib/ui/home/status_bar_widget.dart +++ b/mobile/lib/ui/home/status_bar_widget.dart @@ -12,8 +12,7 @@ import "package:photos/generated/l10n.dart"; import "package:photos/models/preview/preview_item_status.dart"; import "package:photos/service_locator.dart"; import "package:photos/services/preview_video_store.dart"; -import 'package:photos/services/sync_service.dart'; -import "package:photos/services/user_remote_flag_service.dart"; +import 'package:photos/services/sync/sync_service.dart'; import "package:photos/theme/ente_theme.dart"; import 'package:photos/theme/text_style.dart'; import 'package:photos/ui/account/verify_recovery_page.dart'; @@ -43,8 +42,7 @@ class _StatusBarWidgetState extends State { bool _showStatus = false; bool _showErrorBanner = false; - bool _showMlBanner = !userRemoteFlagService - .getCachedBoolValue(UserRemoteFlagService.mlEnabled) && + bool _showMlBanner = !flagService.hasGrantedMLConsent && !localSettings.hasSeenMLEnablingBanner; Error? _syncError; @@ -81,8 +79,7 @@ class _StatusBarWidgetState extends State { _notificationSubscription = Bus.instance.on().listen((event) { if (mounted) { - _showMlBanner = !userRemoteFlagService - .getCachedBoolValue(UserRemoteFlagService.mlEnabled) && + _showMlBanner = !flagService.hasGrantedMLConsent && !localSettings.hasSeenMLEnablingBanner; setState(() {}); } @@ -115,7 +112,6 @@ class _StatusBarWidgetState extends State { _subscription.cancel(); _notificationSubscription.cancel(); _previewSubscription.cancel(); - super.dispose(); } @@ -179,9 +175,7 @@ class _StatusBarWidgetState extends State { ), ) : const SizedBox.shrink(), - userRemoteFlagService.shouldShowRecoveryVerification() && - !_showErrorBanner && - !_showMlBanner + _showVerificationBanner() ? Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12), @@ -203,6 +197,17 @@ class _StatusBarWidgetState extends State { ], ); } + + // _showVerificationBanner after 3 days of installation + bool _showVerificationBanner() { + if (_showErrorBanner || + _showErrorBanner || + flagService.recoveryKeyVerified) { + return false; + } + final DateTime installTime = localSettings.getInstallDateTime(); + return DateTime.now().difference(installTime).inDays >= 3; + } } class SyncStatusWidget extends StatefulWidget { diff --git a/mobile/lib/ui/huge_listview/draggable_scrollbar.dart b/mobile/lib/ui/huge_listview/draggable_scrollbar.dart index f3d215c695..0c54170f54 100644 --- a/mobile/lib/ui/huge_listview/draggable_scrollbar.dart +++ b/mobile/lib/ui/huge_listview/draggable_scrollbar.dart @@ -19,7 +19,7 @@ class DraggableScrollbar extends StatefulWidget { final bool isEnabled; const DraggableScrollbar({ - Key? key, + super.key, required this.child, this.backgroundColor = Colors.white, this.drawColor = Colors.grey, @@ -32,7 +32,7 @@ class DraggableScrollbar extends StatefulWidget { required this.labelTextBuilder, this.onChange, this.isEnabled = true, - }) : super(key: key); + }); @override DraggableScrollbarState createState() => DraggableScrollbarState(); diff --git a/mobile/lib/ui/huge_listview/huge_listview.dart b/mobile/lib/ui/huge_listview/huge_listview.dart index a2dd3119c3..c4a8f343ee 100644 --- a/mobile/lib/ui/huge_listview/huge_listview.dart +++ b/mobile/lib/ui/huge_listview/huge_listview.dart @@ -65,7 +65,7 @@ class HugeListView extends StatefulWidget { final bool isScrollablePositionedList; const HugeListView({ - Key? key, + super.key, this.controller, required this.startIndex, required this.totalCount, @@ -83,7 +83,7 @@ class HugeListView extends StatefulWidget { this.thumbPadding, this.disableScroll = false, this.isScrollablePositionedList = true, - }) : super(key: key); + }); @override HugeListViewState createState() => HugeListViewState(); diff --git a/mobile/lib/ui/huge_listview/scroll_bar_thumb.dart b/mobile/lib/ui/huge_listview/scroll_bar_thumb.dart index a18ab86f94..11714f1f6a 100644 --- a/mobile/lib/ui/huge_listview/scroll_bar_thumb.dart +++ b/mobile/lib/ui/huge_listview/scroll_bar_thumb.dart @@ -21,8 +21,8 @@ class ScrollBarThumb extends StatelessWidget { this.onDragStart, this.onDragUpdate, this.onDragEnd, { - Key? key, - }) : super(key: key); + super.key, + }); @override Widget build(BuildContext context) { @@ -133,10 +133,10 @@ class SlideFadeTransition extends StatelessWidget { final Widget child; const SlideFadeTransition({ - Key? key, + super.key, required this.animation, required this.child, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/mobile/lib/ui/map/enable_map.dart b/mobile/lib/ui/map/enable_map.dart index 335b362ae1..c49353884a 100644 --- a/mobile/lib/ui/map/enable_map.dart +++ b/mobile/lib/ui/map/enable_map.dart @@ -2,15 +2,13 @@ import "package:flutter/cupertino.dart"; import "package:photos/generated/l10n.dart"; import 'package:photos/models/button_result.dart'; import "package:photos/service_locator.dart"; -import "package:photos/services/user_remote_flag_service.dart"; 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/utils/toast_util.dart"; +import "package:photos/ui/notification/toast.dart"; Future requestForMapEnable(BuildContext context) async { - const String flagName = UserRemoteFlagService.mapEnabled; - if (userRemoteFlagService.getCachedBoolValue(flagName)) { + if (flagService.mapEnabled) { return true; } @@ -26,10 +24,7 @@ Future requestForMapEnable(BuildContext context) async { labelText: S.of(context).enableMaps, isInAlert: true, onTap: () async { - await userRemoteFlagService.setBoolValue( - flagName, - true, - ); + await flagService.setMapEnabled(true); }, ), ButtonWidget( @@ -52,5 +47,5 @@ Future requestForMapEnable(BuildContext context) async { //For debugging. void disableMap() { - userRemoteFlagService.setBoolValue(UserRemoteFlagService.mapEnabled, false); + flagService.setMapEnabled(false); } diff --git a/mobile/lib/ui/map/map_pull_up_gallery.dart b/mobile/lib/ui/map/map_pull_up_gallery.dart index b547d984dd..6cb6ea04b0 100644 --- a/mobile/lib/ui/map/map_pull_up_gallery.dart +++ b/mobile/lib/ui/map/map_pull_up_gallery.dart @@ -200,10 +200,10 @@ class _MapPullUpGalleryState extends State { class DraggableHeader extends StatelessWidget { const DraggableHeader({ - Key? key, + super.key, required this.scrollController, required this.bottomSheetDraggableAreaHeight, - }) : super(key: key); + }); static const indicatorHeight = 4.0; final ScrollController scrollController; final double bottomSheetDraggableAreaHeight; diff --git a/mobile/lib/ui/map/map_screen.dart b/mobile/lib/ui/map/map_screen.dart index 78120bd839..b12ce3e80c 100644 --- a/mobile/lib/ui/map/map_screen.dart +++ b/mobile/lib/ui/map/map_screen.dart @@ -17,7 +17,7 @@ import "package:photos/ui/map/image_marker.dart"; import "package:photos/ui/map/map_isolate.dart"; import "package:photos/ui/map/map_pull_up_gallery.dart"; import "package:photos/ui/map/map_view.dart"; -import "package:photos/utils/toast_util.dart"; +import "package:photos/ui/notification/toast.dart"; class MapScreen extends StatefulWidget { // Add a function parameter where the function returns a Future> diff --git a/mobile/lib/ui/map/map_view.dart b/mobile/lib/ui/map/map_view.dart index 3629da7e96..e25319b133 100644 --- a/mobile/lib/ui/map/map_view.dart +++ b/mobile/lib/ui/map/map_view.dart @@ -9,7 +9,7 @@ import "package:photos/ui/map/map_gallery_tile.dart"; import "package:photos/ui/map/map_gallery_tile_badge.dart"; import "package:photos/ui/map/map_marker.dart"; import "package:photos/ui/map/tile/layers.dart"; -import "package:photos/utils/debouncer.dart"; +import "package:photos/utils/standalone/debouncer.dart"; class MapView extends StatefulWidget { final List imageMarkers; diff --git a/mobile/lib/ui/map/tile/attribution/map_attribution.dart b/mobile/lib/ui/map/tile/attribution/map_attribution.dart index 7ae8c6bc86..8740806914 100644 --- a/mobile/lib/ui/map/tile/attribution/map_attribution.dart +++ b/mobile/lib/ui/map/tile/attribution/map_attribution.dart @@ -221,7 +221,7 @@ class MapAttributionWidgetState extends State { child: Container( decoration: BoxDecoration( color: widget.popupBackgroundColor ?? - Theme.of(context).colorScheme.background, + Theme.of(context).colorScheme.surface, border: Border.all(width: 0, style: BorderStyle.none), borderRadius: widget.popupBorderRadius ?? BorderRadius.only( diff --git a/mobile/lib/ui/map/tile/layers.dart b/mobile/lib/ui/map/tile/layers.dart index 2597dddea2..7c1a5a1846 100644 --- a/mobile/lib/ui/map/tile/layers.dart +++ b/mobile/lib/ui/map/tile/layers.dart @@ -102,6 +102,7 @@ class MapBoxTilesLayer extends StatelessWidget { fallbackUrl: 'https://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png', subdomains: const ['a', 'b', 'c'], backgroundColor: Colors.transparent, + userAgentPackageName: _userAgent, tileProvider: CachedNetworkTileProvider(), additionalOptions: const { diff --git a/mobile/lib/utils/toast_util.dart b/mobile/lib/ui/notification/toast.dart similarity index 89% rename from mobile/lib/utils/toast_util.dart rename to mobile/lib/ui/notification/toast.dart index 2e876222a7..5e653631bc 100644 --- a/mobile/lib/utils/toast_util.dart +++ b/mobile/lib/ui/notification/toast.dart @@ -11,6 +11,8 @@ void showToast( String message, { toastLength = Toast.LENGTH_LONG, int iosLongToastLengthInSec = 2, + ToastGravity gravity = ToastGravity.BOTTOM, + EasyLoadingToastPosition position = EasyLoadingToastPosition.bottom, }) async { if (Platform.isAndroid) { await Fluttertoast.cancel(); @@ -18,7 +20,7 @@ void showToast( Fluttertoast.showToast( msg: message, toastLength: toastLength, - gravity: ToastGravity.BOTTOM, + gravity: gravity, timeInSecForIosWeb: 1, backgroundColor: Theme.of(context).colorScheme.toastBackgroundColor, textColor: Theme.of(context).colorScheme.toastTextColor, @@ -39,7 +41,7 @@ void showToast( seconds: (toastLength == Toast.LENGTH_LONG ? iosLongToastLengthInSec : 1), ), - toastPosition: EasyLoadingToastPosition.bottom, + toastPosition: position, dismissOnTap: false, ), ); diff --git a/mobile/lib/ui/payment/add_on_page.dart b/mobile/lib/ui/payment/add_on_page.dart index fa7b3aba76..5701ea8cd0 100644 --- a/mobile/lib/ui/payment/add_on_page.dart +++ b/mobile/lib/ui/payment/add_on_page.dart @@ -6,7 +6,7 @@ import "package:photos/theme/ente_theme.dart"; import 'package:photos/ui/components/buttons/icon_button_widget.dart'; import "package:photos/ui/components/title_bar_title_widget.dart"; import "package:photos/ui/components/title_bar_widget.dart"; -import "package:photos/utils/data_util.dart"; +import "package:photos/utils/standalone/data.dart"; class AddOnPage extends StatelessWidget { final BonusData bonusData; diff --git a/mobile/lib/ui/payment/billing_questions_widget.dart b/mobile/lib/ui/payment/billing_questions_widget.dart index 3cadc8acb8..8c4dd63c98 100644 --- a/mobile/lib/ui/payment/billing_questions_widget.dart +++ b/mobile/lib/ui/payment/billing_questions_widget.dart @@ -8,8 +8,8 @@ import 'package:photos/ui/common/loading_widget.dart'; class BillingQuestionsWidget extends StatelessWidget { const BillingQuestionsWidget({ - Key? key, - }) : super(key: key); + super.key, + }); @override Widget build(BuildContext context) { @@ -62,9 +62,9 @@ class BillingQuestionsWidget extends StatelessWidget { class FaqWidget extends StatelessWidget { const FaqWidget({ - Key? key, + super.key, required this.faq, - }) : super(key: key); + }); final FaqItem? faq; diff --git a/mobile/lib/ui/payment/child_subscription_widget.dart b/mobile/lib/ui/payment/child_subscription_widget.dart index 2b61d3cf95..a4c95be321 100644 --- a/mobile/lib/ui/payment/child_subscription_widget.dart +++ b/mobile/lib/ui/payment/child_subscription_widget.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:logging/logging.dart'; import "package:photos/generated/l10n.dart"; import 'package:photos/models/user_details.dart'; -import 'package:photos/services/user_service.dart'; +import 'package:photos/services/account/user_service.dart'; import "package:photos/theme/ente_theme.dart"; import 'package:photos/ui/components/buttons/button_widget.dart'; import 'package:photos/utils/dialog_util.dart'; @@ -10,9 +10,9 @@ import "package:styled_text/styled_text.dart"; class ChildSubscriptionWidget extends StatelessWidget { const ChildSubscriptionWidget({ - Key? key, + super.key, required this.userDetails, - }) : super(key: key); + }); final UserDetails userDetails; diff --git a/mobile/lib/ui/payment/payment_web_page.dart b/mobile/lib/ui/payment/payment_web_page.dart index 162aa99f6f..a19536a59d 100644 --- a/mobile/lib/ui/payment/payment_web_page.dart +++ b/mobile/lib/ui/payment/payment_web_page.dart @@ -8,10 +8,10 @@ import 'package:logging/logging.dart'; import "package:photos/core/constants.dart"; import 'package:photos/ente_theme_data.dart'; import "package:photos/generated/l10n.dart"; -import 'package:photos/models/subscription.dart'; +import 'package:photos/models/api/billing/subscription.dart'; import "package:photos/service_locator.dart"; -import 'package:photos/services/billing_service.dart'; -import 'package:photos/services/user_service.dart'; +import 'package:photos/services/account/billing_service.dart'; +import 'package:photos/services/account/user_service.dart'; import 'package:photos/ui/common/loading_widget.dart'; import 'package:photos/utils/dialog_util.dart'; import "package:photos/utils/email_util.dart"; @@ -20,8 +20,7 @@ class PaymentWebPage extends StatefulWidget { final String? planId; final String? actionType; - const PaymentWebPage({Key? key, this.planId, this.actionType}) - : super(key: key); + const PaymentWebPage({super.key, this.planId, this.actionType}); @override State createState() => _PaymentWebPageState(); diff --git a/mobile/lib/ui/payment/stripe_subscription_page.dart b/mobile/lib/ui/payment/stripe_subscription_page.dart index d37c6c32d8..e88c2db0bb 100644 --- a/mobile/lib/ui/payment/stripe_subscription_page.dart +++ b/mobile/lib/ui/payment/stripe_subscription_page.dart @@ -6,11 +6,11 @@ import "package:photos/core/event_bus.dart"; import 'package:photos/ente_theme_data.dart'; import "package:photos/events/subscription_purchased_event.dart"; import "package:photos/generated/l10n.dart"; -import 'package:photos/models/billing_plan.dart'; -import 'package:photos/models/subscription.dart'; +import 'package:photos/models/api/billing/billing_plan.dart'; +import 'package:photos/models/api/billing/subscription.dart'; import 'package:photos/models/user_details.dart'; import "package:photos/service_locator.dart"; -import 'package:photos/services/user_service.dart'; +import 'package:photos/services/account/user_service.dart'; import "package:photos/theme/colors.dart"; import 'package:photos/theme/ente_theme.dart'; import 'package:photos/ui/common/loading_widget.dart'; @@ -21,15 +21,15 @@ import "package:photos/ui/components/captioned_text_widget.dart"; import "package:photos/ui/components/divider_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/notification/toast.dart'; import 'package:photos/ui/payment/child_subscription_widget.dart'; import 'package:photos/ui/payment/payment_web_page.dart'; import 'package:photos/ui/payment/subscription_common_widgets.dart'; import 'package:photos/ui/payment/subscription_plan_widget.dart'; import "package:photos/ui/payment/view_add_on_widget.dart"; import "package:photos/ui/tabs/home_widget.dart"; -import "package:photos/utils/data_util.dart"; import 'package:photos/utils/dialog_util.dart'; -import 'package:photos/utils/toast_util.dart'; +import "package:photos/utils/standalone/data.dart"; import 'package:step_progress_indicator/step_progress_indicator.dart'; import 'package:url_launcher/url_launcher_string.dart'; @@ -164,8 +164,9 @@ class _StripeSubscriptionPageState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ TitleBarTitleWidget( - title: - widget.isOnboarding ? S.of(context).selectYourPlan : S.of(context).subscription, + title: widget.isOnboarding + ? S.of(context).selectYourPlan + : S.of(context).subscription, ), _isFreePlanUser() || !_hasLoadedData ? const SizedBox.shrink() diff --git a/mobile/lib/ui/payment/subscription_common_widgets.dart b/mobile/lib/ui/payment/subscription_common_widgets.dart index c708e88afa..faa4ebd1ac 100644 --- a/mobile/lib/ui/payment/subscription_common_widgets.dart +++ b/mobile/lib/ui/payment/subscription_common_widgets.dart @@ -2,13 +2,13 @@ import 'package:flutter/material.dart'; import "package:intl/intl.dart"; import 'package:photos/ente_theme_data.dart'; import "package:photos/generated/l10n.dart"; +import 'package:photos/models/api/billing/subscription.dart'; import "package:photos/models/api/storage_bonus/bonus.dart"; -import 'package:photos/models/subscription.dart'; import "package:photos/theme/ente_theme.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/payment/billing_questions_widget.dart'; -import 'package:photos/utils/data_util.dart'; +import 'package:photos/utils/standalone/data.dart'; class SubscriptionHeaderWidget extends StatefulWidget { final bool? isOnboarding; @@ -150,7 +150,7 @@ class AddOnBonusValidity extends StatelessWidget { class SubFaqWidget extends StatelessWidget { final bool isOnboarding; - const SubFaqWidget({Key? key, this.isOnboarding = false}) : super(key: key); + const SubFaqWidget({super.key, this.isOnboarding = false}); @override Widget build(BuildContext context) { @@ -281,7 +281,9 @@ class _SubscriptionToggleState extends State { switchOutCurve: Curves.easeInOutExpo, child: Text( key: ValueKey(_isYearly), - _isYearly ? S.of(context).yearly : S.of(context).monthly, + _isYearly + ? S.of(context).yearly + : S.of(context).monthly, style: textTheme.body, ), ), diff --git a/mobile/lib/ui/payment/subscription_plan_widget.dart b/mobile/lib/ui/payment/subscription_plan_widget.dart index d23d06dab5..3a269ec2b5 100644 --- a/mobile/lib/ui/payment/subscription_plan_widget.dart +++ b/mobile/lib/ui/payment/subscription_plan_widget.dart @@ -6,7 +6,7 @@ import "package:photos/generated/l10n.dart"; import "package:photos/service_locator.dart"; import "package:photos/theme/colors.dart"; import "package:photos/theme/ente_theme.dart"; -import 'package:photos/utils/data_util.dart'; +import 'package:photos/utils/standalone/data.dart'; class SubscriptionPlanWidget extends StatefulWidget { const SubscriptionPlanWidget({ @@ -176,7 +176,10 @@ class _Price extends StatelessWidget { children: [ if (isPlayStore) Text( - currencySymbol + pricePerMonthString + ' / ' + S.of(context).month, + currencySymbol + + pricePerMonthString + + ' / ' + + S.of(context).month, style: textTheme.largeBold.copyWith(color: textBaseLight), ), if (isPlayStore) @@ -186,7 +189,10 @@ class _Price extends StatelessWidget { ), if (!isPlayStore) Text( - currencySymbol + pricePerMonthString + ' / ' + S.of(context).month, + currencySymbol + + pricePerMonthString + + ' / ' + + S.of(context).month, style: textTheme.largeBold.copyWith(color: textBaseLight), ), if (!isPlayStore) diff --git a/mobile/lib/ui/settings/about_section_widget.dart b/mobile/lib/ui/settings/about_section_widget.dart index 24f98e5a7e..7ca9397274 100644 --- a/mobile/lib/ui/settings/about_section_widget.dart +++ b/mobile/lib/ui/settings/about_section_widget.dart @@ -6,14 +6,14 @@ import "package:photos/ui/common/web_page.dart"; import 'package:photos/ui/components/captioned_text_widget.dart'; import 'package:photos/ui/components/expandable_menu_item_widget.dart'; import 'package:photos/ui/components/menu_item_widget/menu_item_widget.dart'; +import 'package:photos/ui/notification/toast.dart'; import 'package:photos/ui/settings/app_update_dialog.dart'; import 'package:photos/ui/settings/common_settings.dart'; import 'package:photos/utils/dialog_util.dart'; -import 'package:photos/utils/toast_util.dart'; import 'package:url_launcher/url_launcher.dart'; class AboutSectionWidget extends StatelessWidget { - const AboutSectionWidget({Key? key}) : super(key: key); + const AboutSectionWidget({super.key}); @override Widget build(BuildContext context) { @@ -104,8 +104,8 @@ class AboutMenuItemWidget extends StatelessWidget { required this.title, required this.url, this.webPageTitle, - Key? key, - }) : super(key: key); + super.key, + }); @override Widget build(BuildContext context) { diff --git a/mobile/lib/ui/settings/account_section_widget.dart b/mobile/lib/ui/settings/account_section_widget.dart index 2c67514268..a137349639 100644 --- a/mobile/lib/ui/settings/account_section_widget.dart +++ b/mobile/lib/ui/settings/account_section_widget.dart @@ -1,11 +1,12 @@ import 'dart:async'; +import 'package:ente_crypto/ente_crypto.dart'; import "package:flutter/foundation.dart"; import 'package:flutter/material.dart'; import "package:photos/emergency/emergency_page.dart"; import "package:photos/generated/l10n.dart"; +import 'package:photos/services/account/user_service.dart'; import 'package:photos/services/local_authentication_service.dart'; -import 'package:photos/services/user_service.dart'; import 'package:photos/theme/ente_theme.dart'; import 'package:photos/ui/account/change_email_dialog.dart'; import 'package:photos/ui/account/delete_account_page.dart'; @@ -16,13 +17,12 @@ import 'package:photos/ui/components/expandable_menu_item_widget.dart'; import 'package:photos/ui/components/menu_item_widget/menu_item_widget.dart'; import "package:photos/ui/payment/subscription.dart"; import 'package:photos/ui/settings/common_settings.dart'; -import "package:photos/utils/crypto_util.dart"; import 'package:photos/utils/dialog_util.dart'; import "package:photos/utils/navigation_util.dart"; import "package:url_launcher/url_launcher_string.dart"; class AccountSectionWidget extends StatelessWidget { - const AccountSectionWidget({Key? key}) : super(key: key); + const AccountSectionWidget({super.key}); @override Widget build(BuildContext context) { diff --git a/mobile/lib/ui/settings/advanced_settings_screen.dart b/mobile/lib/ui/settings/advanced_settings_screen.dart index 0da7aab7ca..461131a503 100644 --- a/mobile/lib/ui/settings/advanced_settings_screen.dart +++ b/mobile/lib/ui/settings/advanced_settings_screen.dart @@ -3,7 +3,6 @@ import "package:photos/core/error-reporting/super_logging.dart"; import "package:photos/generated/l10n.dart"; import "package:photos/service_locator.dart"; import "package:photos/services/preview_video_store.dart"; -import "package:photos/services/user_remote_flag_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'; @@ -101,19 +100,10 @@ class AdvancedSettingsScreen extends StatelessWidget { singleBorderRadius: 8, alignCaptionedTextToLeft: true, trailingWidget: ToggleSwitchWidget( - value: () => userRemoteFlagService.getCachedBoolValue( - UserRemoteFlagService.mapEnabled, - ), + value: () => flagService.mapEnabled, onChanged: () async { - final isEnabled = - userRemoteFlagService.getCachedBoolValue( - UserRemoteFlagService.mapEnabled, - ); - - await userRemoteFlagService.setBoolValue( - UserRemoteFlagService.mapEnabled, - !isEnabled, - ); + final isEnabled = flagService.mapEnabled; + await flagService.setMapEnabled(!isEnabled); }, ), ), diff --git a/mobile/lib/ui/settings/app_update_dialog.dart b/mobile/lib/ui/settings/app_update_dialog.dart index c4c6608db8..adb305b820 100644 --- a/mobile/lib/ui/settings/app_update_dialog.dart +++ b/mobile/lib/ui/settings/app_update_dialog.dart @@ -10,7 +10,7 @@ import 'package:url_launcher/url_launcher_string.dart'; class AppUpdateDialog extends StatefulWidget { final LatestVersionInfo? latestVersionInfo; - const AppUpdateDialog(this.latestVersionInfo, {Key? key}) : super(key: key); + const AppUpdateDialog(this.latestVersionInfo, {super.key}); @override State createState() => _AppUpdateDialogState(); diff --git a/mobile/lib/ui/settings/app_version_widget.dart b/mobile/lib/ui/settings/app_version_widget.dart index 01dfb7d1f9..c0e5b72d55 100644 --- a/mobile/lib/ui/settings/app_version_widget.dart +++ b/mobile/lib/ui/settings/app_version_widget.dart @@ -4,8 +4,8 @@ import "package:photos/generated/l10n.dart"; class AppVersionWidget extends StatefulWidget { const AppVersionWidget({ - Key? key, - }) : super(key: key); + super.key, + }); @override State createState() => _AppVersionWidgetState(); diff --git a/mobile/lib/ui/settings/backup/backup_folder_selection_page.dart b/mobile/lib/ui/settings/backup/backup_folder_selection_page.dart index d657c60950..3eea0c2e7a 100644 --- a/mobile/lib/ui/settings/backup/backup_folder_selection_page.dart +++ b/mobile/lib/ui/settings/backup/backup_folder_selection_page.dart @@ -12,7 +12,7 @@ import 'package:photos/ente_theme_data.dart'; import 'package:photos/generated/l10n.dart'; import 'package:photos/models/device_collection.dart'; import 'package:photos/models/file/file.dart'; -import 'package:photos/services/remote_sync_service.dart'; +import 'package:photos/services/sync/remote_sync_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'; diff --git a/mobile/lib/ui/settings/backup/backup_item_card.dart b/mobile/lib/ui/settings/backup/backup_item_card.dart index da00498ca3..84d9272853 100644 --- a/mobile/lib/ui/settings/backup/backup_item_card.dart +++ b/mobile/lib/ui/settings/backup/backup_item_card.dart @@ -144,6 +144,7 @@ class _BackupItemCardState extends State { ), onPressed: () { String errorMessage = ""; + bool isPreview = false; if (widget.item.error is Exception) { final Exception ex = widget.item.error as Exception; errorMessage = "Error: " + @@ -154,10 +155,11 @@ class _BackupItemCardState extends State { errorMessage = widget.item.error.toString(); } else if (widget.preview?.error != null) { errorMessage = widget.preview!.error!.toString(); + isPreview = true; } showErrorDialog( context, - 'Upload failed', + isPreview ? "Preview upload ailed" : 'Upload failed', errorMessage, ); }, diff --git a/mobile/lib/ui/settings/backup/backup_section_widget.dart b/mobile/lib/ui/settings/backup/backup_section_widget.dart index 6fbdca287e..674ac057c6 100644 --- a/mobile/lib/ui/settings/backup/backup_section_widget.dart +++ b/mobile/lib/ui/settings/backup/backup_section_widget.dart @@ -12,7 +12,7 @@ import 'package:photos/ui/settings/common_settings.dart'; import 'package:photos/utils/navigation_util.dart'; class BackupSectionWidget extends StatefulWidget { - const BackupSectionWidget({Key? key}) : super(key: key); + const BackupSectionWidget({super.key}); @override BackupSectionWidgetState createState() => BackupSectionWidgetState(); diff --git a/mobile/lib/ui/settings/backup/free_space_options.dart b/mobile/lib/ui/settings/backup/free_space_options.dart index 09879f4ebf..6d42e2040c 100644 --- a/mobile/lib/ui/settings/backup/free_space_options.dart +++ b/mobile/lib/ui/settings/backup/free_space_options.dart @@ -7,7 +7,7 @@ import "package:photos/models/backup_status.dart"; import "package:photos/models/duplicate_files.dart"; import "package:photos/service_locator.dart"; import "package:photos/services/deduplication_service.dart"; -import "package:photos/services/sync_service.dart"; +import "package:photos/services/files_service.dart"; import 'package:photos/theme/ente_theme.dart'; import 'package:photos/ui/components/buttons/button_widget.dart'; import 'package:photos/ui/components/buttons/icon_button_widget.dart'; @@ -18,14 +18,14 @@ import "package:photos/ui/components/menu_section_description_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/notification/toast.dart"; import "package:photos/ui/tools/debug/app_storage_viewer.dart"; import "package:photos/ui/tools/deduplicate_page.dart"; import "package:photos/ui/tools/free_space_page.dart"; import "package:photos/ui/viewer/gallery/large_files_page.dart"; -import "package:photos/utils/data_util.dart"; import "package:photos/utils/dialog_util.dart"; import 'package:photos/utils/navigation_util.dart'; -import "package:photos/utils/toast_util.dart"; +import "package:photos/utils/standalone/data.dart"; class FreeUpSpaceOptionsScreen extends StatefulWidget { const FreeUpSpaceOptionsScreen({super.key}); @@ -93,7 +93,7 @@ class _FreeUpSpaceOptionsScreenState extends State { onTap: () async { BackupStatus status; try { - status = await SyncService.instance + status = await FilesService.instance .getBackupStatus(); } catch (e) { await showGenericErrorDialog( diff --git a/mobile/lib/ui/settings/debug/debug_section_widget.dart b/mobile/lib/ui/settings/debug/debug_section_widget.dart index 55b0ef4f10..007a5b8242 100644 --- a/mobile/lib/ui/settings/debug/debug_section_widget.dart +++ b/mobile/lib/ui/settings/debug/debug_section_widget.dart @@ -1,19 +1,19 @@ +import 'package:ente_crypto/ente_crypto.dart'; import 'package:flutter/material.dart'; import 'package:photos/core/configuration.dart'; import "package:photos/service_locator.dart"; import 'package:photos/services/ignored_files_service.dart'; -import 'package:photos/services/local_sync_service.dart'; -import 'package:photos/services/sync_service.dart'; +import 'package:photos/services/sync/local_sync_service.dart'; +import 'package:photos/services/sync/sync_service.dart'; import 'package:photos/theme/ente_theme.dart'; import 'package:photos/ui/components/captioned_text_widget.dart'; import 'package:photos/ui/components/expandable_menu_item_widget.dart'; import 'package:photos/ui/components/menu_item_widget/menu_item_widget.dart'; +import 'package:photos/ui/notification/toast.dart'; import 'package:photos/ui/settings/common_settings.dart'; -import 'package:photos/utils/crypto_util.dart'; -import 'package:photos/utils/toast_util.dart'; class DebugSectionWidget extends StatelessWidget { - const DebugSectionWidget({Key? key}) : super(key: key); + const DebugSectionWidget({super.key}); @override Widget build(BuildContext context) { 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 d89ee548a1..0c719a2299 100644 --- a/mobile/lib/ui/settings/debug/ml_debug_section_widget.dart +++ b/mobile/lib/ui/settings/debug/ml_debug_section_widget.dart @@ -11,16 +11,15 @@ import "package:photos/services/machine_learning/face_ml/person/person_service.d 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/user_remote_flag_service.dart"; import 'package:photos/theme/ente_theme.dart'; import 'package:photos/ui/components/captioned_text_widget.dart'; import 'package:photos/ui/components/expandable_menu_item_widget.dart'; import 'package:photos/ui/components/menu_item_widget/menu_item_widget.dart'; import "package:photos/ui/components/toggle_switch_widget.dart"; +import 'package:photos/ui/notification/toast.dart'; import 'package:photos/ui/settings/common_settings.dart'; import "package:photos/utils/dialog_util.dart"; import "package:photos/utils/ml_util.dart"; -import 'package:photos/utils/toast_util.dart'; class MLDebugSectionWidget extends StatefulWidget { const MLDebugSectionWidget({super.key}); @@ -81,17 +80,12 @@ class _MLDebugSectionWidgetState extends State { }, ), trailingWidget: ToggleSwitchWidget( - value: () => userRemoteFlagService - .getCachedBoolValue(UserRemoteFlagService.mlEnabled), + value: () => flagService.hasGrantedMLConsent, onChanged: () async { try { - final oldMlConsent = userRemoteFlagService - .getCachedBoolValue(UserRemoteFlagService.mlEnabled); + final oldMlConsent = flagService.hasGrantedMLConsent; final mlConsent = !oldMlConsent; - await userRemoteFlagService.setBoolValue( - UserRemoteFlagService.mlEnabled, - mlConsent, - ); + await flagService.setMLConsent(mlConsent); logger.info('ML consent turned ${mlConsent ? 'on' : 'off'}'); if (!mlConsent) { MLService.instance.pauseIndexingAndClustering(); @@ -285,6 +279,23 @@ class _MLDebugSectionWidgetState extends State { }, ), sectionOptionSpacing, + MenuItemWidget( + captionedTextWidget: const CaptionedTextWidget( + title: "Update memories", + ), + pressedColor: getEnteColorScheme(context).fillFaint, + trailingIcon: Icons.chevron_right_outlined, + trailingIconIsMuted: true, + onTap: () async { + try { + await memoriesCacheService.updateCache(forced: true); + } catch (e, s) { + logger.warning('Update memories failed', e, s); + await showGenericErrorDialog(context: context, error: e); + } + }, + ), + sectionOptionSpacing, MenuItemWidget( captionedTextWidget: const CaptionedTextWidget( title: "Sync person mappings ", diff --git a/mobile/lib/ui/settings/developer_settings_page.dart b/mobile/lib/ui/settings/developer_settings_page.dart index c9f6357f29..27267bf516 100644 --- a/mobile/lib/ui/settings/developer_settings_page.dart +++ b/mobile/lib/ui/settings/developer_settings_page.dart @@ -4,8 +4,8 @@ import "package:photos/core/configuration.dart"; import "package:photos/core/network/network.dart"; import "package:photos/generated/l10n.dart"; import "package:photos/ui/common/gradient_button.dart"; +import "package:photos/ui/notification/toast.dart"; import "package:photos/utils/dialog_util.dart"; -import "package:photos/utils/toast_util.dart"; class DeveloperSettingsPage extends StatefulWidget { const DeveloperSettingsPage({super.key}); diff --git a/mobile/lib/ui/settings/general_section_widget.dart b/mobile/lib/ui/settings/general_section_widget.dart index 3532292551..b5f7e35ad3 100644 --- a/mobile/lib/ui/settings/general_section_widget.dart +++ b/mobile/lib/ui/settings/general_section_widget.dart @@ -5,7 +5,7 @@ import "package:photos/app.dart"; import "package:photos/generated/l10n.dart"; import "package:photos/l10n/l10n.dart"; import "package:photos/service_locator.dart"; -import 'package:photos/services/user_service.dart'; +import 'package:photos/services/account/user_service.dart'; import 'package:photos/theme/ente_theme.dart'; import "package:photos/ui/components/captioned_text_widget.dart"; import "package:photos/ui/components/expandable_menu_item_widget.dart"; @@ -18,7 +18,7 @@ import "package:photos/ui/settings/notification_settings_screen.dart"; import 'package:photos/utils/navigation_util.dart'; class GeneralSectionWidget extends StatelessWidget { - const GeneralSectionWidget({Key? key}) : super(key: key); + const GeneralSectionWidget({super.key}); @override Widget build(BuildContext context) { diff --git a/mobile/lib/ui/settings/inherited_settings_state.dart b/mobile/lib/ui/settings/inherited_settings_state.dart index dee7d317d4..ce749a0adb 100644 --- a/mobile/lib/ui/settings/inherited_settings_state.dart +++ b/mobile/lib/ui/settings/inherited_settings_state.dart @@ -3,9 +3,9 @@ import 'package:flutter/widgets.dart'; /// StatefulWidget that wraps InheritedSettingsState class SettingsStateContainer extends StatefulWidget { const SettingsStateContainer({ - Key? key, + super.key, required this.child, - }) : super(key: key); + }); final Widget child; @override @@ -54,12 +54,12 @@ class InheritedSettingsState extends InheritedWidget { final void Function() decrement; const InheritedSettingsState({ - Key? key, + super.key, required this.expandedSectionCount, required this.increment, required this.decrement, - required Widget child, - }) : super(key: key, child: child); + required super.child, + }); bool get isAnySectionExpanded => expandedSectionCount > 0; diff --git a/mobile/lib/ui/settings/language_picker.dart b/mobile/lib/ui/settings/language_picker.dart index 98efa54f47..9cd313e549 100644 --- a/mobile/lib/ui/settings/language_picker.dart +++ b/mobile/lib/ui/settings/language_picker.dart @@ -18,8 +18,8 @@ class LanguageSelectorPage extends StatelessWidget { this.supportedLocales, this.onLocaleChanged, this.currentLocale, { - Key? key, - }) : super(key: key); + super.key, + }); @override Widget build(BuildContext context) { @@ -78,8 +78,8 @@ class ItemsWidget extends StatefulWidget { this.supportedLocales, this.onLocaleChanged, this.currentLocale, { - Key? key, - }) : super(key: key); + super.key, + }); @override State createState() => _ItemsWidgetState(); diff --git a/mobile/lib/ui/settings/lock_screen/lock_screen_password.dart b/mobile/lib/ui/settings/lock_screen/lock_screen_password.dart index 5341f21b66..3c8c2cc330 100644 --- a/mobile/lib/ui/settings/lock_screen/lock_screen_password.dart +++ b/mobile/lib/ui/settings/lock_screen/lock_screen_password.dart @@ -1,5 +1,6 @@ import "dart:convert"; +import 'package:ente_crypto/ente_crypto.dart'; import "package:flutter/material.dart"; import "package:flutter/services.dart"; import "package:flutter_sodium/flutter_sodium.dart"; @@ -10,7 +11,6 @@ import "package:photos/ui/components/buttons/icon_button_widget.dart"; import "package:photos/ui/components/text_input_widget.dart"; import "package:photos/ui/settings/lock_screen/lock_screen_confirm_password.dart"; import "package:photos/ui/settings/lock_screen/lock_screen_options.dart"; -import "package:photos/utils/crypto_util.dart"; import "package:photos/utils/lock_screen_settings.dart"; class LockScreenPassword extends StatefulWidget { diff --git a/mobile/lib/ui/settings/lock_screen/lock_screen_pin.dart b/mobile/lib/ui/settings/lock_screen/lock_screen_pin.dart index 2f526751f2..039b9df242 100644 --- a/mobile/lib/ui/settings/lock_screen/lock_screen_pin.dart +++ b/mobile/lib/ui/settings/lock_screen/lock_screen_pin.dart @@ -1,5 +1,6 @@ import "dart:convert"; +import 'package:ente_crypto/ente_crypto.dart'; import "package:flutter/material.dart"; import "package:flutter/services.dart"; import "package:flutter_sodium/flutter_sodium.dart"; @@ -11,7 +12,6 @@ import "package:photos/ui/components/buttons/icon_button_widget.dart"; import "package:photos/ui/settings/lock_screen/custom_pin_keypad.dart"; import "package:photos/ui/settings/lock_screen/lock_screen_confirm_pin.dart"; import "package:photos/ui/settings/lock_screen/lock_screen_options.dart"; -import "package:photos/utils/crypto_util.dart"; import "package:photos/utils/lock_screen_settings.dart"; import 'package:pinput/pinput.dart'; diff --git a/mobile/lib/ui/settings/ml/enable_ml_consent.dart b/mobile/lib/ui/settings/ml/enable_ml_consent.dart index 972858c3d7..c828e9d36b 100644 --- a/mobile/lib/ui/settings/ml/enable_ml_consent.dart +++ b/mobile/lib/ui/settings/ml/enable_ml_consent.dart @@ -3,7 +3,6 @@ import "package:photos/core/event_bus.dart"; import "package:photos/events/notification_event.dart"; import "package:photos/generated/l10n.dart"; import "package:photos/service_locator.dart"; -import "package:photos/services/user_remote_flag_service.dart"; import "package:photos/theme/ente_theme.dart"; import "package:photos/ui/common/web_page.dart"; import "package:photos/ui/components/buttons/button_widget.dart"; @@ -153,10 +152,7 @@ class _EnableMachineLearningConsentState Future enableMlConsent(BuildContext context) async { try { - await userRemoteFlagService.setBoolValue( - UserRemoteFlagService.mlEnabled, - true, - ); + await flagService.setMLConsent(true); Bus.instance.fire(NotificationEvent()); Navigator.of(context).pop(true); } catch (e) { diff --git a/mobile/lib/ui/settings/ml/machine_learning_settings_page.dart b/mobile/lib/ui/settings/ml/machine_learning_settings_page.dart index 0ebe3656ba..ad4f87821a 100644 --- a/mobile/lib/ui/settings/ml/machine_learning_settings_page.dart +++ b/mobile/lib/ui/settings/ml/machine_learning_settings_page.dart @@ -12,7 +12,6 @@ import "package:photos/services/machine_learning/semantic_search/clip/clip_image import "package:photos/services/machine_learning/semantic_search/clip/clip_text_encoder.dart"; import 'package:photos/services/machine_learning/semantic_search/semantic_search_service.dart'; import "package:photos/services/remote_assets_service.dart"; -import "package:photos/services/user_remote_flag_service.dart"; import "package:photos/theme/ente_theme.dart"; import "package:photos/ui/common/loading_widget.dart"; import "package:photos/ui/common/web_page.dart"; @@ -77,8 +76,7 @@ class _MachineLearningSettingsPageState @override Widget build(BuildContext context) { - final hasEnabled = userRemoteFlagService - .getCachedBoolValue(UserRemoteFlagService.mlEnabled); + final hasEnabled = flagService.hasGrantedMLConsent; return Scaffold( body: CustomScrollView( primary: false, @@ -211,8 +209,7 @@ class _MachineLearningSettingsPageState } Future toggleMlConsent() async { - final oldMlConsent = userRemoteFlagService - .getCachedBoolValue(UserRemoteFlagService.mlEnabled); + final oldMlConsent = flagService.hasGrantedMLConsent; // Go to consent page first if not enabled if (!oldMlConsent) { final result = await Navigator.push( @@ -228,10 +225,7 @@ class _MachineLearningSettingsPageState } } final mlConsent = !oldMlConsent; - await userRemoteFlagService.setBoolValue( - UserRemoteFlagService.mlEnabled, - mlConsent, - ); + await flagService.setMLConsent(mlConsent); if (!mlConsent) { MLService.instance.pauseIndexingAndClustering(); unawaited( @@ -248,8 +242,7 @@ class _MachineLearningSettingsPageState } Widget _getMlSettings(BuildContext context) { - final hasEnabled = userRemoteFlagService - .getCachedBoolValue(UserRemoteFlagService.mlEnabled); + final hasEnabled = flagService.hasGrantedMLConsent; if (!hasEnabled) { return const SizedBox.shrink(); } diff --git a/mobile/lib/ui/settings/ml/ml_user_dev_screen.dart b/mobile/lib/ui/settings/ml/ml_user_dev_screen.dart index f295d4b202..32fdaaaedc 100644 --- a/mobile/lib/ui/settings/ml/ml_user_dev_screen.dart +++ b/mobile/lib/ui/settings/ml/ml_user_dev_screen.dart @@ -5,6 +5,8 @@ import "package:photos/db/ml/base.dart"; import "package:photos/db/ml/db.dart"; import "package:photos/events/people_changed_event.dart"; import "package:photos/service_locator.dart"; +import "package:photos/services/machine_learning/ml_indexing_isolate.dart"; +import "package:photos/services/machine_learning/ml_models_overview.dart"; import "package:photos/services/machine_learning/semantic_search/semantic_search_service.dart"; import "package:photos/theme/ente_theme.dart"; import "package:photos/ui/components/buttons/button_widget.dart"; @@ -14,8 +16,8 @@ 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/components/toggle_switch_widget.dart"; +import "package:photos/ui/notification/toast.dart"; import "package:photos/utils/dialog_util.dart"; -import "package:photos/utils/toast_util.dart"; final Logger _logger = Logger("MLUserDeveloperOptions"); @@ -104,6 +106,90 @@ class _MLUserDeveloperOptionsState extends State { isBottomBorderRadiusRemoved: true, isGestureDetectorDisabled: true, ), + const SizedBox(height: 24), + ButtonWidget( + buttonType: ButtonType.neutral, + labelText: "Load face detection model", + onTap: () async { + try { + await MLIndexingIsolate.instance + .debugLoadSingleModel(MLModels.faceDetection); + } catch (e, s) { + _logger.severe( + "Could not load face detection model", + e, + s, + ); + await showGenericErrorDialog( + context: context, + error: e, + ); + } + }, + ), + const SizedBox(height: 24), + ButtonWidget( + buttonType: ButtonType.neutral, + labelText: "Load face recognition model", + onTap: () async { + try { + await MLIndexingIsolate.instance + .debugLoadSingleModel(MLModels.faceEmbedding); + } catch (e, s) { + _logger.severe( + "Could not load face detection model", + e, + s, + ); + await showGenericErrorDialog( + context: context, + error: e, + ); + } + }, + ), + const SizedBox(height: 24), + ButtonWidget( + buttonType: ButtonType.neutral, + labelText: "Load clip image model", + onTap: () async { + try { + await MLIndexingIsolate.instance + .debugLoadSingleModel(MLModels.clipImageEncoder); + } catch (e, s) { + _logger.severe( + "Could not load face detection model", + e, + s, + ); + await showGenericErrorDialog( + context: context, + error: e, + ); + } + }, + ), + const SizedBox(height: 24), + ButtonWidget( + buttonType: ButtonType.neutral, + labelText: "Load clip text model", + onTap: () async { + try { + await MLIndexingIsolate.instance + .debugLoadSingleModel(MLModels.clipTextEncoder); + } catch (e, s) { + _logger.severe( + "Could not load face detection model", + e, + s, + ); + await showGenericErrorDialog( + context: context, + error: e, + ); + } + }, + ), const SafeArea( child: SizedBox( height: 12, diff --git a/mobile/lib/ui/settings/security_section_widget.dart b/mobile/lib/ui/settings/security_section_widget.dart index 403a5506d4..c2c305a7ad 100644 --- a/mobile/lib/ui/settings/security_section_widget.dart +++ b/mobile/lib/ui/settings/security_section_widget.dart @@ -1,6 +1,7 @@ import 'dart:async'; import "dart:typed_data"; +import 'package:ente_crypto/ente_crypto.dart'; import 'package:flutter/material.dart'; import "package:local_auth/local_auth.dart"; import "package:logging/logging.dart"; @@ -11,9 +12,9 @@ import 'package:photos/events/two_factor_status_change_event.dart'; import "package:photos/generated/l10n.dart"; import "package:photos/l10n/l10n.dart"; import "package:photos/models/user_details.dart"; +import "package:photos/services/account/passkey_service.dart"; +import 'package:photos/services/account/user_service.dart'; import 'package:photos/services/local_authentication_service.dart'; -import "package:photos/services/passkey_service.dart"; -import 'package:photos/services/user_service.dart'; import 'package:photos/theme/ente_theme.dart'; import "package:photos/ui/account/request_pwd_verification_page.dart"; import 'package:photos/ui/account/sessions_page.dart'; @@ -21,16 +22,15 @@ import 'package:photos/ui/components/captioned_text_widget.dart'; import 'package:photos/ui/components/expandable_menu_item_widget.dart'; import 'package:photos/ui/components/menu_item_widget/menu_item_widget.dart'; import 'package:photos/ui/components/toggle_switch_widget.dart'; +import "package:photos/ui/notification/toast.dart"; import 'package:photos/ui/settings/common_settings.dart'; import "package:photos/ui/settings/lock_screen/lock_screen_options.dart"; import "package:photos/utils/auth_util.dart"; -import "package:photos/utils/crypto_util.dart"; import "package:photos/utils/dialog_util.dart"; -import "package:photos/utils/navigation_util.dart"; -import "package:photos/utils/toast_util.dart"; +import 'package:photos/utils/navigation_util.dart'; class SecuritySectionWidget extends StatefulWidget { - const SecuritySectionWidget({Key? key}) : super(key: key); + const SecuritySectionWidget({super.key}); @override State createState() => _SecuritySectionWidgetState(); diff --git a/mobile/lib/ui/settings/settings_title_bar_widget.dart b/mobile/lib/ui/settings/settings_title_bar_widget.dart index b15db62763..5fa625b49b 100644 --- a/mobile/lib/ui/settings/settings_title_bar_widget.dart +++ b/mobile/lib/ui/settings/settings_title_bar_widget.dart @@ -7,7 +7,7 @@ import 'package:photos/theme/ente_theme.dart'; import 'package:photos/ui/common/loading_widget.dart'; class SettingsTitleBarWidget extends StatelessWidget { - const SettingsTitleBarWidget({Key? key}) : super(key: key); + const SettingsTitleBarWidget({super.key}); @override Widget build(BuildContext context) { diff --git a/mobile/lib/ui/settings/social_section_widget.dart b/mobile/lib/ui/settings/social_section_widget.dart index b9329eec7f..c97b16b30a 100644 --- a/mobile/lib/ui/settings/social_section_widget.dart +++ b/mobile/lib/ui/settings/social_section_widget.dart @@ -11,7 +11,7 @@ import 'package:photos/ui/settings/common_settings.dart'; import 'package:url_launcher/url_launcher_string.dart'; class SocialSectionWidget extends StatelessWidget { - const SocialSectionWidget({Key? key}) : super(key: key); + const SocialSectionWidget({super.key}); @override Widget build(BuildContext context) { @@ -85,9 +85,9 @@ class SocialsMenuItemWidget extends StatelessWidget { const SocialsMenuItemWidget( this.text, this.url, { - Key? key, + super.key, this.launchInExternalApp = true, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/mobile/lib/ui/settings/storage_card_widget.dart b/mobile/lib/ui/settings/storage_card_widget.dart index 3554c6fa47..8388e87038 100644 --- a/mobile/lib/ui/settings/storage_card_widget.dart +++ b/mobile/lib/ui/settings/storage_card_widget.dart @@ -11,10 +11,10 @@ import 'package:photos/theme/ente_theme.dart'; import "package:photos/ui/common/loading_widget.dart"; import 'package:photos/ui/payment/subscription.dart'; import 'package:photos/ui/settings/storage_progress_widget.dart'; -import 'package:photos/utils/data_util.dart'; +import 'package:photos/utils/standalone/data.dart'; class StorageCardWidget extends StatefulWidget { - const StorageCardWidget({Key? key}) : super(key: key); + const StorageCardWidget({super.key}); @override State createState() => _StorageCardWidgetState(); @@ -24,6 +24,8 @@ class _StorageCardWidgetState extends State { late Image _background; final _logger = Logger((_StorageCardWidgetState).toString()); final ValueNotifier _isStorageCardPressed = ValueNotifier(false); + int? familyMemberStorageLimit; + bool showFamilyBreakup = false; @override void initState() { @@ -119,9 +121,18 @@ class _StorageCardWidgetState extends State { Widget _userDetails(UserDetails userDetails) { const hundredMBinBytes = 107374182; const oneTBinBytes = 1073741824000; + showFamilyBreakup = userDetails.isPartOfFamily(); + if (showFamilyBreakup) { + familyMemberStorageLimit = userDetails.familyMemberStorageLimit(); + showFamilyBreakup = familyMemberStorageLimit == null; + } - final usedStorageInBytes = userDetails.getFamilyOrPersonalUsage(); - final totalStorageInBytes = userDetails.getTotalStorage(); + final usedStorageInBytes = familyMemberStorageLimit != null + ? userDetails.usage + : userDetails.getFamilyOrPersonalUsage(); + final totalStorageInBytes = familyMemberStorageLimit != null + ? familyMemberStorageLimit! + : userDetails.getTotalStorage(); final freeStorageInBytes = totalStorageInBytes - usedStorageInBytes; final isMobileScreenSmall = @@ -212,20 +223,19 @@ class _StorageCardWidgetState extends State { Color.fromRGBO(255, 255, 255, 0.2), //hardcoded in figma fractionOfStorage: 1, ), - userDetails.isPartOfFamily() + showFamilyBreakup ? StorageProgressWidget( color: strokeBaseDark, fractionOfStorage: - ((userDetails.getFamilyOrPersonalUsage()) / - userDetails.getTotalStorage()), + ((usedStorageInBytes) / totalStorageInBytes), ) : const SizedBox.shrink(), StorageProgressWidget( - color: userDetails.isPartOfFamily() + color: showFamilyBreakup ? getEnteColorScheme(context).primary300 : strokeBaseDark, fractionOfStorage: - (userDetails.usage / userDetails.getTotalStorage()), + (userDetails.usage / totalStorageInBytes), ), ], ), @@ -234,7 +244,7 @@ class _StorageCardWidgetState extends State { mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.start, children: [ - userDetails.isPartOfFamily() + showFamilyBreakup ? Row( children: [ Container( @@ -272,7 +282,9 @@ class _StorageCardWidgetState extends State { ) : const SizedBox.shrink(), Text( - S.of(context).availableStorageSpace(freeSpace, freeSpaceUnit), + S + .of(context) + .availableStorageSpace(freeSpace, freeSpaceUnit), style: getEnteTextTheme(context) .mini .copyWith(color: textFaintDark), diff --git a/mobile/lib/ui/settings/storage_error_widget.dart b/mobile/lib/ui/settings/storage_error_widget.dart deleted file mode 100644 index 8ccf6de0f5..0000000000 --- a/mobile/lib/ui/settings/storage_error_widget.dart +++ /dev/null @@ -1,32 +0,0 @@ -import 'package:flutter/material.dart'; -import "package:photos/generated/l10n.dart"; -import 'package:photos/theme/colors.dart'; -import 'package:photos/theme/ente_theme.dart'; - -class StorageErrorWidget extends StatelessWidget { - const StorageErrorWidget({super.key}); - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.all(12), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Icon( - Icons.error_outline_outlined, - color: strokeBaseDark, - ), - const SizedBox(height: 8), - Text( - S.of(context).yourStorageDetailsCouldNotBeFetched, - style: getEnteTextTheme(context).small.copyWith( - color: textMutedDark, - ), - ), - ], - ), - ); - } -} diff --git a/mobile/lib/ui/settings/support_section_widget.dart b/mobile/lib/ui/settings/support_section_widget.dart index fa730f703a..49ef874c71 100644 --- a/mobile/lib/ui/settings/support_section_widget.dart +++ b/mobile/lib/ui/settings/support_section_widget.dart @@ -13,7 +13,7 @@ import 'package:photos/utils/email_util.dart'; import "package:url_launcher/url_launcher_string.dart"; class SupportSectionWidget extends StatelessWidget { - const SupportSectionWidget({Key? key}) : super(key: key); + const SupportSectionWidget({super.key}); @override Widget build(BuildContext context) { diff --git a/mobile/lib/ui/settings/theme_switch_widget.dart b/mobile/lib/ui/settings/theme_switch_widget.dart index 545ef92b75..1fcd6802ee 100644 --- a/mobile/lib/ui/settings/theme_switch_widget.dart +++ b/mobile/lib/ui/settings/theme_switch_widget.dart @@ -9,7 +9,7 @@ import 'package:photos/ui/components/menu_item_widget/menu_item_widget.dart'; import 'package:photos/ui/settings/common_settings.dart'; class ThemeSwitchWidget extends StatefulWidget { - const ThemeSwitchWidget({Key? key}) : super(key: key); + const ThemeSwitchWidget({super.key}); @override State createState() => _ThemeSwitchWidgetState(); diff --git a/mobile/lib/ui/settings_page.dart b/mobile/lib/ui/settings_page.dart index 274e2c785f..32401fd30f 100644 --- a/mobile/lib/ui/settings_page.dart +++ b/mobile/lib/ui/settings_page.dart @@ -33,7 +33,7 @@ import "package:photos/utils/navigation_util.dart"; class SettingsPage extends StatelessWidget { final ValueNotifier emailNotifier; - const SettingsPage({Key? key, required this.emailNotifier}) : super(key: key); + const SettingsPage({super.key, required this.emailNotifier}); @override Widget build(BuildContext context) { diff --git a/mobile/lib/ui/sharing/add_participant_page.dart b/mobile/lib/ui/sharing/add_participant_page.dart index cfa48d037a..6c2ffbef0d 100644 --- a/mobile/lib/ui/sharing/add_participant_page.dart +++ b/mobile/lib/ui/sharing/add_participant_page.dart @@ -4,8 +4,8 @@ import "package:photos/extensions/user_extension.dart"; import "package:photos/generated/l10n.dart"; import "package:photos/models/api/collection/user.dart"; import 'package:photos/models/collection/collection.dart'; +import "package:photos/services/account/user_service.dart"; import 'package:photos/services/collections_service.dart'; -import "package:photos/services/user_service.dart"; import 'package:photos/theme/ente_theme.dart'; import 'package:photos/ui/actions/collection/collection_sharing_actions.dart'; import 'package:photos/ui/components/buttons/button_widget.dart'; @@ -15,9 +15,9 @@ import 'package:photos/ui/components/menu_item_widget/menu_item_widget.dart'; import 'package:photos/ui/components/menu_section_description_widget.dart'; import 'package:photos/ui/components/menu_section_title.dart'; 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/toast_util.dart"; class AddParticipantPage extends StatefulWidget { final Collection collection; @@ -363,8 +363,8 @@ class _AddParticipantPage extends State { List _getSuggestedUser() { final Set existingEmails = {}; - for (final User? u in widget.collection.sharees ?? []) { - if (u != null && u.id != null && u.email.isNotEmpty) { + for (final User u in widget.collection.sharees) { + if (u.id != null && u.email.isNotEmpty) { existingEmails.add(u.email); } } diff --git a/mobile/lib/ui/sharing/album_participants_page.dart b/mobile/lib/ui/sharing/album_participants_page.dart index 656f9a0076..e27fe92478 100644 --- a/mobile/lib/ui/sharing/album_participants_page.dart +++ b/mobile/lib/ui/sharing/album_participants_page.dart @@ -64,11 +64,11 @@ class _AlbumParticipantsPageState extends State { @override Widget build(BuildContext context) { final isOwner = - widget.collection.owner?.id == Configuration.instance.getUserID(); + widget.collection.owner.id == Configuration.instance.getUserID(); final colorScheme = getEnteColorScheme(context); final currentUserID = Configuration.instance.getUserID()!; final int participants = 1 + widget.collection.getSharees().length; - final User owner = widget.collection.owner!; + final User owner = widget.collection.owner; if (owner.id == currentUserID && owner.email == "") { owner.email = Configuration.instance.getEmail()!; } @@ -107,11 +107,9 @@ class _AlbumParticipantsPageState extends State { captionedTextWidget: CaptionedTextWidget( title: isOwner ? S.of(context).you - : widget.collection.owner != null - ? _nameIfAvailableElseEmail( - widget.collection.owner!, - ) - : '', + : _nameIfAvailableElseEmail( + widget.collection.owner, + ), makeTextBold: isOwner, ), leadingIconWidget: UserAvatarWidget( diff --git a/mobile/lib/ui/sharing/manage_links_widget.dart b/mobile/lib/ui/sharing/manage_links_widget.dart index 5b389fab82..dfea026297 100644 --- a/mobile/lib/ui/sharing/manage_links_widget.dart +++ b/mobile/lib/ui/sharing/manage_links_widget.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'package:collection/collection.dart'; +import 'package:ente_crypto/ente_crypto.dart'; import "package:fast_base58/fast_base58.dart"; import 'package:flutter/material.dart'; import "package:flutter/services.dart"; @@ -16,14 +17,13 @@ import 'package:photos/ui/components/divider_widget.dart'; import 'package:photos/ui/components/menu_item_widget/menu_item_widget.dart'; import 'package:photos/ui/components/menu_section_description_widget.dart'; import "package:photos/ui/components/toggle_switch_widget.dart"; +import 'package:photos/ui/notification/toast.dart'; import 'package:photos/ui/sharing/pickers/device_limit_picker_page.dart'; import 'package:photos/ui/sharing/pickers/link_expiry_picker_page.dart'; -import 'package:photos/utils/crypto_util.dart'; -import 'package:photos/utils/date_time_util.dart'; import 'package:photos/utils/dialog_util.dart'; import 'package:photos/utils/navigation_util.dart'; import "package:photos/utils/share_util.dart"; -import 'package:photos/utils/toast_util.dart'; +import 'package:photos/utils/standalone/date_time.dart'; class ManageSharedLinkWidget extends StatefulWidget { final Collection? collection; @@ -47,13 +47,13 @@ class _ManageSharedLinkWidgetState extends State { @override Widget build(BuildContext context) { final isCollectEnabled = - widget.collection!.publicURLs?.firstOrNull?.enableCollect ?? false; + widget.collection!.publicURLs.firstOrNull?.enableCollect ?? false; final isDownloadEnabled = - widget.collection!.publicURLs?.firstOrNull?.enableDownload ?? true; + widget.collection!.publicURLs.firstOrNull?.enableDownload ?? true; final isPasswordEnabled = - widget.collection!.publicURLs?.firstOrNull?.passwordEnabled ?? false; + widget.collection!.publicURLs.firstOrNull?.passwordEnabled ?? false; final enteColorScheme = getEnteColorScheme(context); - final PublicURL url = widget.collection!.publicURLs!.firstOrNull!; + final PublicURL url = widget.collection!.publicURLs.firstOrNull!; final String collectionKey = Base58Encode( CollectionsService.instance.getCollectionKey(widget.collection!.id), ); @@ -328,7 +328,7 @@ class _ManageSharedLinkWidgetState extends State { Future> _getEncryptedPassword(String pass) async { final kekSalt = CryptoUtil.getSaltToDeriveKey(); final result = await CryptoUtil.deriveInteractiveKey( - utf8.encode(pass) as Uint8List, + utf8.encode(pass), kekSalt, ); return { diff --git a/mobile/lib/ui/sharing/pickers/device_limit_picker_page.dart b/mobile/lib/ui/sharing/pickers/device_limit_picker_page.dart index 66c08bde74..3d83b4d8af 100644 --- a/mobile/lib/ui/sharing/pickers/device_limit_picker_page.dart +++ b/mobile/lib/ui/sharing/pickers/device_limit_picker_page.dart @@ -72,7 +72,7 @@ class _ItemsWidgetState extends State { bool isCustomLimit = false; @override void initState() { - currentDeviceLimit = widget.collection.publicURLs!.first!.deviceLimit; + currentDeviceLimit = widget.collection.publicURLs.first.deviceLimit; initialDeviceLimit = currentDeviceLimit; if (!publicLinkDeviceLimits.contains(currentDeviceLimit)) { isCustomLimit = true; diff --git a/mobile/lib/ui/sharing/share_collection_page.dart b/mobile/lib/ui/sharing/share_collection_page.dart index 42ba9f417c..64090528c8 100644 --- a/mobile/lib/ui/sharing/share_collection_page.dart +++ b/mobile/lib/ui/sharing/share_collection_page.dart @@ -14,6 +14,7 @@ import 'package:photos/ui/components/divider_widget.dart'; import 'package:photos/ui/components/menu_item_widget/menu_item_widget.dart'; import 'package:photos/ui/components/menu_section_description_widget.dart'; import 'package:photos/ui/components/menu_section_title.dart'; +import 'package:photos/ui/notification/toast.dart'; import "package:photos/ui/sharing/add_participant_page.dart"; import 'package:photos/ui/sharing/album_participants_page.dart'; import "package:photos/ui/sharing/album_share_info_widget.dart"; @@ -22,7 +23,6 @@ import 'package:photos/ui/sharing/manage_links_widget.dart'; import 'package:photos/ui/sharing/user_avator_widget.dart'; import 'package:photos/utils/navigation_util.dart'; import 'package:photos/utils/share_util.dart'; -import 'package:photos/utils/toast_util.dart'; class ShareCollectionPage extends StatefulWidget { final Collection collection; @@ -61,7 +61,7 @@ class _ShareCollectionPageState extends State { @override Widget build(BuildContext context) { - _sharees = widget.collection.sharees ?? []; + _sharees = widget.collection.sharees; final bool hasUrl = widget.collection.hasLink; final children = []; children.add( @@ -136,7 +136,7 @@ class _ShareCollectionPageState extends State { } final bool hasExpired = - widget.collection.publicURLs?.firstOrNull?.isExpired ?? false; + widget.collection.publicURLs.firstOrNull?.isExpired ?? false; children.addAll([ const SizedBox( height: 24, @@ -166,7 +166,7 @@ class _ShareCollectionPageState extends State { CollectionsService.instance.getCollectionKey(widget.collection.id), ); final String url = - "${widget.collection.publicURLs!.first!.url}#$collectionKey"; + "${widget.collection.publicURLs.first.url}#$collectionKey"; children.addAll( [ MenuItemWidget( diff --git a/mobile/lib/ui/sharing/user_avator_widget.dart b/mobile/lib/ui/sharing/user_avator_widget.dart index a69430a953..7acd869f87 100644 --- a/mobile/lib/ui/sharing/user_avator_widget.dart +++ b/mobile/lib/ui/sharing/user_avator_widget.dart @@ -13,7 +13,7 @@ import "package:photos/services/machine_learning/face_ml/person/person_service.d 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/utils/debouncer.dart"; +import "package:photos/utils/standalone/debouncer.dart"; import 'package:tuple/tuple.dart'; enum AvatarType { small, mini, tiny, extra } @@ -80,7 +80,7 @@ class _UserAvatarWidgetState extends State { ); if (person != null) { _faceThumbnail = - await PersonService.instance.getRecentFileOfPerson(person); + await PersonService.instance.getThumbnailFileOfPerson(person); } return person?.remoteID; }); @@ -117,18 +117,25 @@ class _UserAvatarWidgetState extends State { if (snapshot.hasData) { final personID = snapshot.data as String; return ClipOval( - child: PersonFaceWidget( - _faceThumbnail!, - personId: personID, - onErrorCallback: () { - if (mounted) { - setState(() { - _personID = null; - _faceThumbnail = null; - }); - } - }, - ), + child: _faceThumbnail == null + ? _FirstLetterCircularAvatar( + user: widget.user, + currentUserID: widget.currentUserID, + thumbnailView: widget.thumbnailView, + type: widget.type, + ) + : PersonFaceWidget( + _faceThumbnail!, + personId: personID, + onErrorCallback: () { + if (mounted) { + setState(() { + _personID = null; + _faceThumbnail = null; + }); + } + }, + ), ); } else if (snapshot.hasError) { _logger.severe("Error loading personID", snapshot.error); diff --git a/mobile/lib/ui/sharing/verify_identity_dialog.dart b/mobile/lib/ui/sharing/verify_identity_dialog.dart index eaa725635e..88374b58e3 100644 --- a/mobile/lib/ui/sharing/verify_identity_dialog.dart +++ b/mobile/lib/ui/sharing/verify_identity_dialog.dart @@ -8,7 +8,7 @@ import "package:flutter/services.dart"; import "package:logging/logging.dart"; import "package:photos/core/configuration.dart"; import "package:photos/generated/l10n.dart"; -import "package:photos/services/user_service.dart"; +import "package:photos/services/account/user_service.dart"; import "package:photos/theme/ente_theme.dart"; import "package:photos/ui/common/loading_widget.dart"; import 'package:photos/ui/components/buttons/button_widget.dart'; @@ -24,10 +24,10 @@ class VerifyIdentifyDialog extends StatefulWidget { final bool self; VerifyIdentifyDialog({ - Key? key, + super.key, required this.self, this.email = '', - }) : super(key: key) { + }) { if (!self && email.isEmpty) { throw ArgumentError("email cannot be empty when self is false"); } diff --git a/mobile/lib/ui/tabs/home_widget.dart b/mobile/lib/ui/tabs/home_widget.dart index 0528c0d167..565db9daad 100644 --- a/mobile/lib/ui/tabs/home_widget.dart +++ b/mobile/lib/ui/tabs/home_widget.dart @@ -2,6 +2,7 @@ import 'dart:async'; import "dart:convert"; import "dart:io"; +import "package:ente_crypto/ente_crypto.dart"; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; @@ -33,13 +34,14 @@ import 'package:photos/models/collection/collection_items.dart'; import "package:photos/models/file/file.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/app_lifecycle_service.dart'; import 'package:photos/services/collections_service.dart'; -import 'package:photos/services/local_sync_service.dart'; import "package:photos/services/machine_learning/face_ml/person/person_service.dart"; import "package:photos/services/notification_service.dart"; -import "package:photos/services/remote_sync_service.dart"; -import 'package:photos/services/user_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"; import 'package:photos/states/user_details_state.dart'; import 'package:photos/theme/colors.dart'; import "package:photos/theme/effects.dart"; @@ -66,9 +68,7 @@ import "package:photos/ui/viewer/gallery/shared_public_collection_page.dart"; import "package:photos/ui/viewer/search/search_widget.dart"; import 'package:photos/ui/viewer/search_tab/search_tab.dart'; import "package:photos/utils/collection_util.dart"; -import "package:photos/utils/crypto_util.dart"; import 'package:photos/utils/dialog_util.dart'; -import "package:photos/utils/diff_fetcher.dart"; import "package:photos/utils/navigation_util.dart"; import 'package:receive_sharing_intent/receive_sharing_intent.dart'; import 'package:uni_links/uni_links.dart'; @@ -275,7 +275,7 @@ class _HomeWidgetState extends State { final existingCollection = CollectionsService.instance.getCollectionByID(collection.id); - if (collection.owner!.id! == Configuration.instance.getUserID() || + if (collection.isOwner(Configuration.instance.getUserID() ?? -1) || (existingCollection != null && !existingCollection.isDeleted)) { await routeToPage( context, @@ -286,8 +286,8 @@ class _HomeWidgetState extends State { return; } final dialog = createProgressDialog(context, "Loading..."); - final publicUrl = collection.publicURLs![0]; - if (!publicUrl!.enableDownload) { + final publicUrl = collection.publicURLs[0]; + if (!publicUrl.enableDownload) { await showErrorDialog( context, context.l10n.canNotOpenTitle, diff --git a/mobile/lib/ui/tabs/nav_bar.dart b/mobile/lib/ui/tabs/nav_bar.dart index be2d313f8e..e5f0d22c2d 100644 --- a/mobile/lib/ui/tabs/nav_bar.dart +++ b/mobile/lib/ui/tabs/nav_bar.dart @@ -6,7 +6,7 @@ import "package:photos/theme/effects.dart"; class GNav extends StatefulWidget { const GNav({ - Key? key, + super.key, this.tabs, this.selectedIndex = 0, this.onTabChange, @@ -31,7 +31,7 @@ class GNav extends StatefulWidget { this.haptic, this.tabBackgroundGradient, this.mainAxisAlignment = MainAxisAlignment.spaceBetween, - }) : super(key: key); + }); final List? tabs; final int selectedIndex; @@ -159,7 +159,7 @@ class GButton extends StatefulWidget { final String? semanticLabel; const GButton({ - Key? key, + super.key, this.active, this.haptic, this.backgroundColor, @@ -186,7 +186,7 @@ class GButton extends StatefulWidget { this.activeBorder, this.shadow, this.semanticLabel, - }) : super(key: key); + }); @override State createState() => _GButtonState(); @@ -229,7 +229,7 @@ class _GButtonState extends State { class Button extends StatefulWidget { const Button({ - Key? key, + super.key, this.icon, this.iconSize, this.leading, @@ -252,7 +252,7 @@ class Button extends StatefulWidget { this.border, this.activeBorder, this.shadow, - }) : super(key: key); + }); final IconData? icon; final double? iconSize; diff --git a/mobile/lib/ui/tabs/section_title.dart b/mobile/lib/ui/tabs/section_title.dart index 5229082014..1f1be877f2 100644 --- a/mobile/lib/ui/tabs/section_title.dart +++ b/mobile/lib/ui/tabs/section_title.dart @@ -14,9 +14,9 @@ class SectionTitle extends StatelessWidget { this.title, this.titleWithBrand, this.mutedTitle = false, - Key? key, + super.key, this.padding, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/mobile/lib/ui/tabs/shared/empty_state.dart b/mobile/lib/ui/tabs/shared/empty_state.dart index b69d64b96d..53b1584a64 100644 --- a/mobile/lib/ui/tabs/shared/empty_state.dart +++ b/mobile/lib/ui/tabs/shared/empty_state.dart @@ -7,9 +7,9 @@ import 'package:photos/ui/collections/collection_action_sheet.dart'; import 'package:photos/ui/components/buttons/button_widget.dart'; import "package:photos/ui/components/empty_state_item_widget.dart"; import "package:photos/ui/components/models/button_type.dart"; +import "package:photos/ui/notification/toast.dart"; import "package:photos/utils/collection_util.dart"; import "package:photos/utils/share_util.dart"; -import "package:photos/utils/toast_util.dart"; class SharedEmptyStateWidget extends StatelessWidget { const SharedEmptyStateWidget({super.key}); 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 5c6b7c4c5f..db84df9083 100644 --- a/mobile/lib/ui/tabs/shared/quick_link_album_item.dart +++ b/mobile/lib/ui/tabs/shared/quick_link_album_item.dart @@ -109,7 +109,7 @@ class QuickLinkAlbumItem extends StatelessWidget { style: textTheme.smallMuted, ), c.hasLink - ? (c.publicURLs!.first!.isExpired + ? (c.publicURLs.first.isExpired ? Icon( Icons.link_outlined, color: colorScheme.warning500, diff --git a/mobile/lib/ui/tabs/shared_collections_tab.dart b/mobile/lib/ui/tabs/shared_collections_tab.dart index a6e7f3c679..1f0fc3d727 100644 --- a/mobile/lib/ui/tabs/shared_collections_tab.dart +++ b/mobile/lib/ui/tabs/shared_collections_tab.dart @@ -20,8 +20,8 @@ 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/utils/debouncer.dart"; import "package:photos/utils/navigation_util.dart"; +import "package:photos/utils/standalone/debouncer.dart"; class SharedCollectionsTab extends StatefulWidget { const SharedCollectionsTab({super.key}); diff --git a/mobile/lib/ui/tabs/user_collections_tab.dart b/mobile/lib/ui/tabs/user_collections_tab.dart index 5e8431a5a9..afc6178c24 100644 --- a/mobile/lib/ui/tabs/user_collections_tab.dart +++ b/mobile/lib/ui/tabs/user_collections_tab.dart @@ -27,12 +27,12 @@ import 'package:photos/ui/components/buttons/icon_button_widget.dart'; import "package:photos/ui/tabs/section_title.dart"; import "package:photos/ui/viewer/actions/delete_empty_albums.dart"; import "package:photos/ui/viewer/gallery/empty_state.dart"; -import "package:photos/utils/debouncer.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({Key? key}) : super(key: key); + const UserCollectionsTab({super.key}); @override State createState() => _UserCollectionsTabState(); diff --git a/mobile/lib/ui/tools/app_lock.dart b/mobile/lib/ui/tools/app_lock.dart index 19a718b7a0..784b2eaa14 100644 --- a/mobile/lib/ui/tools/app_lock.dart +++ b/mobile/lib/ui/tools/app_lock.dart @@ -38,7 +38,7 @@ class AppLock extends StatefulWidget { final Locale? locale; const AppLock({ - Key? key, + super.key, required this.builder, required this.lockScreen, required this.savedThemeMode, @@ -46,7 +46,7 @@ class AppLock extends StatefulWidget { this.locale, this.darkTheme, this.lightTheme, - }) : super(key: key); + }); static _AppLockState? of(BuildContext context) => context.findAncestorStateOfType<_AppLockState>(); diff --git a/mobile/lib/ui/tools/collage/collage_save_button.dart b/mobile/lib/ui/tools/collage/collage_save_button.dart index cd2645403b..c7f77a47e0 100644 --- a/mobile/lib/ui/tools/collage/collage_save_button.dart +++ b/mobile/lib/ui/tools/collage/collage_save_button.dart @@ -4,12 +4,12 @@ import "package:logging/logging.dart"; import "package:photo_manager/photo_manager.dart"; import "package:photos/generated/l10n.dart"; import 'package:photos/models/file/file.dart'; -import "package:photos/services/sync_service.dart"; +import "package:photos/services/sync/sync_service.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/viewer/file/detail_page.dart"; import "package:photos/utils/navigation_util.dart"; -import "package:photos/utils/toast_util.dart"; import "package:widgets_to_image/widgets_to_image.dart"; class SaveCollageButton extends StatelessWidget { diff --git a/mobile/lib/ui/tools/debug/app_storage_viewer.dart b/mobile/lib/ui/tools/debug/app_storage_viewer.dart index 5ac484684d..bb7abf3e51 100644 --- a/mobile/lib/ui/tools/debug/app_storage_viewer.dart +++ b/mobile/lib/ui/tools/debug/app_storage_viewer.dart @@ -16,10 +16,10 @@ import 'package:photos/ui/components/menu_section_title.dart'; import 'package:photos/ui/components/title_bar_title_widget.dart'; import 'package:photos/ui/components/title_bar_widget.dart'; import 'package:photos/ui/tools/debug/path_storage_viewer.dart'; -import 'package:photos/utils/directory_content.dart'; +import 'package:photos/utils/standalone/directory_content.dart'; class AppStorageViewer extends StatefulWidget { - const AppStorageViewer({Key? key}) : super(key: key); + const AppStorageViewer({super.key}); @override State createState() => _AppStorageViewerState(); diff --git a/mobile/lib/ui/tools/debug/log_file_viewer.dart b/mobile/lib/ui/tools/debug/log_file_viewer.dart index e41561c0e8..2b538ef160 100644 --- a/mobile/lib/ui/tools/debug/log_file_viewer.dart +++ b/mobile/lib/ui/tools/debug/log_file_viewer.dart @@ -1,5 +1,4 @@ import 'dart:io'; -import 'dart:ui'; import 'package:flutter/material.dart'; import "package:photos/generated/l10n.dart"; @@ -7,7 +6,7 @@ import 'package:photos/ui/common/loading_widget.dart'; class LogFileViewer extends StatefulWidget { final File file; - const LogFileViewer(this.file, {Key? key}) : super(key: key); + const LogFileViewer(this.file, {super.key}); @override State createState() => _LogFileViewerState(); diff --git a/mobile/lib/ui/tools/debug/path_storage_viewer.dart b/mobile/lib/ui/tools/debug/path_storage_viewer.dart index 6d169ef48f..1859234175 100644 --- a/mobile/lib/ui/tools/debug/path_storage_viewer.dart +++ b/mobile/lib/ui/tools/debug/path_storage_viewer.dart @@ -7,8 +7,8 @@ import 'package:logging/logging.dart'; import 'package:photos/theme/ente_theme.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/utils/data_util.dart'; -import 'package:photos/utils/directory_content.dart'; +import 'package:photos/utils/standalone/data.dart'; +import 'package:photos/utils/standalone/directory_content.dart'; class PathStorageItem { final String path; @@ -33,8 +33,8 @@ class PathStorageViewer extends StatefulWidget { this.removeTopRadius = false, this.removeBottomRadius = false, this.enableDoubleTapClear = false, - Key? key, - }) : super(key: key); + super.key, + }); @override State createState() => _PathStorageViewerState(); diff --git a/mobile/lib/ui/tools/deduplicate_page.dart b/mobile/lib/ui/tools/deduplicate_page.dart index 8b8efd8dd8..579b458e0d 100644 --- a/mobile/lib/ui/tools/deduplicate_page.dart +++ b/mobile/lib/ui/tools/deduplicate_page.dart @@ -14,10 +14,10 @@ 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'; import 'package:photos/ui/viewer/gallery/empty_state.dart'; -import 'package:photos/utils/data_util.dart'; import 'package:photos/utils/delete_file_util.dart'; import "package:photos/utils/dialog_util.dart"; import 'package:photos/utils/navigation_util.dart'; +import 'package:photos/utils/standalone/data.dart'; class DeduplicatePage extends StatefulWidget { final List duplicates; diff --git a/mobile/lib/ui/tools/editor/filtered_image.dart b/mobile/lib/ui/tools/editor/filtered_image.dart index 92fcc976e4..31c74590ac 100644 --- a/mobile/lib/ui/tools/editor/filtered_image.dart +++ b/mobile/lib/ui/tools/editor/filtered_image.dart @@ -9,8 +9,8 @@ class FilteredImage extends StatelessWidget { this.brightness, this.saturation, this.hue, - Key? key, - }) : super(key: key); + super.key, + }); final double? brightness, saturation, hue; final Widget child; diff --git a/mobile/lib/ui/tools/editor/image_editor_page.dart b/mobile/lib/ui/tools/editor/image_editor_page.dart index 2ef93601f2..686044d21c 100644 --- a/mobile/lib/ui/tools/editor/image_editor_page.dart +++ b/mobile/lib/ui/tools/editor/image_editor_page.dart @@ -6,7 +6,6 @@ import 'dart:ui' as ui show Image; import 'package:extended_image/extended_image.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/painting.dart' show decodeImageFromList; import "package:flutter_image_compress/flutter_image_compress.dart"; import 'package:image_editor/image_editor.dart'; import 'package:logging/logging.dart'; @@ -18,16 +17,16 @@ import 'package:photos/events/local_photos_updated_event.dart'; import "package:photos/generated/l10n.dart"; import 'package:photos/models/file/file.dart' as ente; import 'package:photos/models/location/location.dart'; -import 'package:photos/services/sync_service.dart'; +import 'package:photos/services/sync/sync_service.dart'; import 'package:photos/ui/common/loading_widget.dart'; import 'package:photos/ui/components/action_sheet_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/tools/editor/filtered_image.dart'; import 'package:photos/ui/viewer/file/detail_page.dart'; import 'package:photos/utils/dialog_util.dart'; import 'package:photos/utils/navigation_util.dart'; -import 'package:photos/utils/toast_util.dart'; import 'package:syncfusion_flutter_core/theme.dart'; import 'package:syncfusion_flutter_sliders/sliders.dart'; diff --git a/mobile/lib/ui/tools/editor/video_editor_page.dart b/mobile/lib/ui/tools/editor/video_editor_page.dart index d96f0008a7..5abf738ad9 100644 --- a/mobile/lib/ui/tools/editor/video_editor_page.dart +++ b/mobile/lib/ui/tools/editor/video_editor_page.dart @@ -12,7 +12,8 @@ import "package:photos/events/local_photos_updated_event.dart"; import "package:photos/generated/l10n.dart"; import "package:photos/models/file/file.dart"; import "package:photos/models/location/location.dart"; -import "package:photos/services/sync_service.dart"; +import "package:photos/services/sync/sync_service.dart"; +import "package:photos/ui/notification/toast.dart"; import "package:photos/ui/tools/editor/export_video_service.dart"; import 'package:photos/ui/tools/editor/video_crop_page.dart'; import "package:photos/ui/tools/editor/video_editor/video_editor_bottom_action.dart"; @@ -24,7 +25,6 @@ import "package:photos/ui/tools/editor/video_trim_page.dart"; import "package:photos/ui/viewer/file/detail_page.dart"; import "package:photos/utils/dialog_util.dart"; import "package:photos/utils/navigation_util.dart"; -import "package:photos/utils/toast_util.dart"; import "package:video_editor/video_editor.dart"; class VideoEditorPage extends StatefulWidget { @@ -209,15 +209,14 @@ class _VideoEditorPageState extends State { final config = VideoFFmpegVideoEditorConfig( _controller!, - format: VideoExportFormat( - path.extension(widget.ioFile.path).substring(1), - ), - // commandBuilder: (config, videoPath, outputPath) { - // final List filters = config.getExportFilters(); - // filters.add('hflip'); // add horizontal flip + format: VideoExportFormat.mp4, + commandBuilder: (config, videoPath, outputPath) { + final List filters = config.getExportFilters(); - // return '-i $videoPath ${config.filtersCmd(filters)} -preset ultrafast $outputPath'; - // }, + final String startTrimCmd = "-ss ${_controller!.startTrim}"; + final String toTrimCmd = "-t ${_controller!.trimmedDuration}"; + return '$startTrimCmd -i $videoPath $toTrimCmd ${config.filtersCmd(filters)} -c:v libx264 -c:a aac $outputPath'; + }, ); try { @@ -241,12 +240,12 @@ class _VideoEditorPageState extends State { await PhotoManager.stopChangeNotify(); try { - final AssetEntity? newAsset = + final AssetEntity newAsset = await (PhotoManager.editor.saveVideo(result, title: fileName)); result.deleteSync(); final newFile = await EnteFile.fromAsset( widget.file.deviceFolder ?? '', - newAsset!, + newAsset, ); newFile.creationTime = widget.file.creationTime; diff --git a/mobile/lib/ui/tools/free_space_page.dart b/mobile/lib/ui/tools/free_space_page.dart index 44c8e72449..664bf49c3e 100644 --- a/mobile/lib/ui/tools/free_space_page.dart +++ b/mobile/lib/ui/tools/free_space_page.dart @@ -1,11 +1,14 @@ +import "dart:io"; + 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/backup_status.dart'; import 'package:photos/ui/common/gradient_button.dart'; -import 'package:photos/utils/data_util.dart'; +import "package:photos/ui/notification/toast.dart"; import 'package:photos/utils/delete_file_util.dart'; +import 'package:photos/utils/standalone/data.dart'; class FreeSpacePage extends StatefulWidget { final BackupStatus status; @@ -13,9 +16,9 @@ class FreeSpacePage extends StatefulWidget { const FreeSpacePage( this.status, { - Key? key, + super.key, this.clearSpaceForFolder = false, - }) : super(key: key); + }); @override State createState() => _FreeSpacePageState(); @@ -163,9 +166,24 @@ class _FreeSpacePageState extends State { } Future _freeStorage(BackupStatus status) async { - final result = await deleteLocalFiles(context, status.localIDs); - if (result) { + bool isSuccess = await deleteLocalFiles(context, status.localIDs); + + if (isSuccess == false) { + isSuccess = await deleteLocalFilesAfterRemovingAlreadyDeletedIDs( + context, + status.localIDs, + ); + } + + if (isSuccess == false && Platform.isAndroid) { + isSuccess = + await retryFreeUpSpaceAfterRemovingAssetsNonExistingInDisk(context); + } + + if (isSuccess) { Navigator.of(context).pop(true); + } else { + showToast(context, S.of(context).couldNotFreeUpSpace); } } } diff --git a/mobile/lib/ui/tools/lock_screen.dart b/mobile/lib/ui/tools/lock_screen.dart index 516ac92319..e81cc2b8bc 100644 --- a/mobile/lib/ui/tools/lock_screen.dart +++ b/mobile/lib/ui/tools/lock_screen.dart @@ -10,7 +10,7 @@ import "package:photos/core/configuration.dart"; import "package:photos/generated/l10n.dart"; import "package:photos/l10n/l10n.dart"; import "package:photos/service_locator.dart"; -import "package:photos/services/user_service.dart"; +import "package:photos/services/account/user_service.dart"; import "package:photos/theme/ente_theme.dart"; import "package:photos/ui/components/buttons/icon_button_widget.dart"; import 'package:photos/ui/tools/app_lock.dart'; @@ -19,7 +19,7 @@ import "package:photos/utils/dialog_util.dart"; import "package:photos/utils/lock_screen_settings.dart"; class LockScreen extends StatefulWidget { - const LockScreen({Key? key}) : super(key: key); + const LockScreen({super.key}); @override State createState() => _LockScreenState(); diff --git a/mobile/lib/ui/viewer/actions/delete_empty_albums.dart b/mobile/lib/ui/viewer/actions/delete_empty_albums.dart index 95ee49da49..18b817f067 100644 --- a/mobile/lib/ui/viewer/actions/delete_empty_albums.dart +++ b/mobile/lib/ui/viewer/actions/delete_empty_albums.dart @@ -6,14 +6,14 @@ import "package:photos/generated/l10n.dart"; import 'package:photos/models/collection/collection.dart'; import 'package:photos/models/file/file.dart'; import 'package:photos/services/collections_service.dart'; -import "package:photos/services/remote_sync_service.dart"; +import "package:photos/services/sync/remote_sync_service.dart"; import 'package:photos/ui/components/action_sheet_widget.dart'; import 'package:photos/ui/components/buttons/button_widget.dart'; import 'package:photos/ui/components/models/button_type.dart'; class DeleteEmptyAlbums extends StatefulWidget { final List collections; - const DeleteEmptyAlbums(this.collections, {Key? key}) : super(key: key); + const DeleteEmptyAlbums(this.collections, {super.key}); @override State createState() => _DeleteEmptyAlbumsState(); 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 ab6cbf315a..cab693d278 100644 --- a/mobile/lib/ui/viewer/actions/file_selection_actions_widget.dart +++ b/mobile/lib/ui/viewer/actions/file_selection_actions_widget.dart @@ -26,6 +26,7 @@ import 'package:photos/services/collections_service.dart'; import 'package:photos/services/hidden_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/video_memory_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'; @@ -35,6 +36,7 @@ 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/show_images_prevew.dart"; import "package:photos/ui/tools/collage/collage_creator_page.dart"; import "package:photos/ui/viewer/file/detail_page.dart"; @@ -45,7 +47,6 @@ 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/toast_util.dart'; import "package:screenshot/screenshot.dart"; class FileSelectionActionsWidget extends StatefulWidget { @@ -86,6 +87,8 @@ class _FileSelectionActionsWidgetState Collection? _cachedCollectionForSharedLink; final GlobalKey shareButtonKey = GlobalKey(); final GlobalKey sendLinkButtonKey = GlobalKey(); + final StreamController _progressController = + StreamController(); @override void initState() { @@ -102,6 +105,7 @@ class _FileSelectionActionsWidgetState @override void dispose() { + _progressController.close(); widget.selectedFiles.removeListener(_selectFileChangeListener); super.dispose(); } @@ -316,6 +320,16 @@ class _FileSelectionActionsWidgetState ), ); } + if (flagService.internalUser && + widget.type != GalleryType.sharedPublicCollection) { + items.add( + SelectionActionButton( + icon: Icons.movie_creation_sharp, + labelText: "(i) Video Memory", + onTap: _onCreateVideoMemoryClicked, + ), + ); + } if (widget.type.showHideOption()) { items.add( @@ -649,6 +663,12 @@ class _FileSelectionActionsWidgetState } } + Future _onCreateVideoMemoryClicked() async { + final List selectedFiles = widget.selectedFiles.files.toList(); + await createSlideshow(context, selectedFiles); + widget.selectedFiles.clearAll(); + } + Future _createPlaceholder( List ownedSelectedFiles, ) async { @@ -804,7 +824,7 @@ class _FileSelectionActionsWidgetState .getCollectionKey(_cachedCollectionForSharedLink!.id), ); final String url = - "${_cachedCollectionForSharedLink!.publicURLs?.first?.url}#$collectionKey"; + "${_cachedCollectionForSharedLink!.publicURLs.first.url}#$collectionKey"; unawaited(Clipboard.setData(ClipboardData(text: url))); await shareImageAndUrl( placeholderBytes, diff --git a/mobile/lib/ui/viewer/file/custom_app_bar.dart b/mobile/lib/ui/viewer/file/custom_app_bar.dart deleted file mode 100644 index 1003e56af9..0000000000 --- a/mobile/lib/ui/viewer/file/custom_app_bar.dart +++ /dev/null @@ -1,25 +0,0 @@ -import 'package:flutter/material.dart'; - -class CustomAppBar extends PreferredSize { - @override - final Widget child; - @override - final Size preferredSize; - final double height; - - const CustomAppBar( - this.child, - this.preferredSize, { - Key? key, - this.height = kToolbarHeight, - }) : super(key: key, child: child, preferredSize: preferredSize); - - @override - Widget build(BuildContext context) { - return Container( - height: preferredSize.height, - alignment: Alignment.center, - child: child, - ); - } -} diff --git a/mobile/lib/ui/viewer/file/detail_page.dart b/mobile/lib/ui/viewer/file/detail_page.dart index 7e8795c978..0dab65b96d 100644 --- a/mobile/lib/ui/viewer/file/detail_page.dart +++ b/mobile/lib/ui/viewer/file/detail_page.dart @@ -17,6 +17,7 @@ import "package:photos/models/file/file_type.dart"; import "package:photos/service_locator.dart"; import "package:photos/services/local_authentication_service.dart"; import "package:photos/ui/common/fast_scroll_physics.dart"; +import 'package:photos/ui/notification/toast.dart'; import 'package:photos/ui/tools/editor/image_editor_page.dart'; import "package:photos/ui/tools/editor/video_editor_page.dart"; import "package:photos/ui/viewer/file/file_app_bar.dart"; @@ -28,7 +29,6 @@ import 'package:photos/utils/dialog_util.dart'; import 'package:photos/utils/file_util.dart'; import 'package:photos/utils/navigation_util.dart'; import "package:photos/utils/thumbnail_util.dart"; -import 'package:photos/utils/toast_util.dart'; enum DetailPageMode { minimalistic, @@ -65,7 +65,7 @@ class DetailPageConfiguration { class DetailPage extends StatefulWidget { final DetailPageConfiguration config; - const DetailPage(this.config, {key}) : super(key: key); + const DetailPage(this.config, {super.key}); @override State createState() => _DetailPageState(); @@ -156,7 +156,6 @@ class _DetailPageState extends State { return FileAppBar( _files![selectedIndex], _onFileRemoved, - 100, widget.config.mode == DetailPageMode.full, enableFullScreenNotifier: _enableFullScreenNotifier, ); diff --git a/mobile/lib/ui/viewer/file/exif_info_dialog.dart b/mobile/lib/ui/viewer/file/exif_info_dialog.dart index e3344aa48b..2b286b5601 100644 --- a/mobile/lib/ui/viewer/file/exif_info_dialog.dart +++ b/mobile/lib/ui/viewer/file/exif_info_dialog.dart @@ -1,4 +1,3 @@ -import 'dart:ui'; import 'package:flutter/material.dart'; import "package:photos/generated/l10n.dart"; @@ -9,7 +8,7 @@ import 'package:photos/utils/exif_util.dart'; class ExifInfoDialog extends StatelessWidget { final EnteFile file; - const ExifInfoDialog(this.file, {Key? key}) : super(key: key); + const ExifInfoDialog(this.file, {super.key}); @override Widget build(BuildContext context) { diff --git a/mobile/lib/ui/viewer/file/file_app_bar.dart b/mobile/lib/ui/viewer/file/file_app_bar.dart index e763a174cc..bf7e7cb996 100644 --- a/mobile/lib/ui/viewer/file/file_app_bar.dart +++ b/mobile/lib/ui/viewer/file/file_app_bar.dart @@ -23,26 +23,23 @@ import "package:photos/services/local_authentication_service.dart"; import "package:photos/services/preview_video_store.dart"; import "package:photos/theme/ente_theme.dart"; import 'package:photos/ui/collections/collection_action_sheet.dart'; -import 'package:photos/ui/viewer/file/custom_app_bar.dart'; +import 'package:photos/ui/notification/toast.dart'; import "package:photos/ui/viewer/file_details/favorite_widget.dart"; import "package:photos/ui/viewer/file_details/upload_icon_widget.dart"; import 'package:photos/utils/dialog_util.dart'; import "package:photos/utils/file_download_util.dart"; import 'package:photos/utils/file_util.dart'; import "package:photos/utils/magic_util.dart"; -import 'package:photos/utils/toast_util.dart'; class FileAppBar extends StatefulWidget { final EnteFile file; final Function(EnteFile) onFileRemoved; - final double height; final bool shouldShowActions; final ValueNotifier enableFullScreenNotifier; const FileAppBar( this.file, this.onFileRemoved, - this.height, this.shouldShowActions, { required this.enableFullScreenNotifier, super.key, @@ -98,8 +95,9 @@ class FileAppBarState extends State { final isTrashedFile = widget.file is TrashFile; final shouldShowActions = widget.shouldShowActions && !isTrashedFile; - return CustomAppBar( - ValueListenableBuilder( + return PreferredSize( + preferredSize: const Size.fromHeight(kToolbarHeight), + child: ValueListenableBuilder( valueListenable: widget.enableFullScreenNotifier, builder: (context, bool isFullScreen, child) { return IgnorePointer( @@ -124,32 +122,33 @@ class FileAppBarState extends State { stops: const [0, 0.2, 1], ), ), - child: AnimatedSwitcher( - duration: const Duration(milliseconds: 250), - switchInCurve: Curves.easeInOut, - switchOutCurve: Curves.easeInOut, - child: AppBar( - clipBehavior: Clip.none, - key: ValueKey(isGuestView), - iconTheme: const IconThemeData( - color: Colors.white, - ), //same for both themes - leading: IconButton( - icon: const Icon(Icons.arrow_back), - onPressed: () { - isGuestView - ? _requestAuthentication() - : Navigator.of(context).pop(); - }, + child: SafeArea( + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 250), + switchInCurve: Curves.easeInOut, + switchOutCurve: Curves.easeInOut, + child: AppBar( + clipBehavior: Clip.none, + key: ValueKey(isGuestView), + iconTheme: const IconThemeData( + color: Colors.white, + ), //same for both themes + leading: IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () { + isGuestView + ? _requestAuthentication() + : Navigator.of(context).pop(); + }, + ), + actions: shouldShowActions && !isGuestView ? _actions : [], + elevation: 0, + backgroundColor: const Color(0x00000000), ), - actions: shouldShowActions && !isGuestView ? _actions : [], - elevation: 0, - backgroundColor: const Color(0x00000000), ), ), ), ), - Size.fromHeight(Platform.isAndroid ? 84 : 96), ); } @@ -179,10 +178,7 @@ class FileAppBarState extends State { } if (!isFileHidden && isFileUploaded) { _actions.add( - Padding( - padding: const EdgeInsets.all(8), - child: FavoriteWidget(widget.file), - ), + Center(child: FavoriteWidget(widget.file)), ); } if (!isFileUploaded) { diff --git a/mobile/lib/ui/viewer/file/file_details_widget.dart b/mobile/lib/ui/viewer/file/file_details_widget.dart index 1138908860..12f722b357 100644 --- a/mobile/lib/ui/viewer/file/file_details_widget.dart +++ b/mobile/lib/ui/viewer/file/file_details_widget.dart @@ -19,7 +19,6 @@ 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/services/user_remote_flag_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"; @@ -298,8 +297,7 @@ class _FileDetailsWidgetState extends State { ]); } - if (userRemoteFlagService - .getCachedBoolValue(UserRemoteFlagService.mlEnabled)) { + if (flagService.hasGrantedMLConsent) { fileDetailsTiles.addAll([ FacesItemWidget(file), const FileDetailsDivider(), diff --git a/mobile/lib/ui/viewer/file/file_icons_widget.dart b/mobile/lib/ui/viewer/file/file_icons_widget.dart index 9cc2554035..b7e675ff95 100644 --- a/mobile/lib/ui/viewer/file/file_icons_widget.dart +++ b/mobile/lib/ui/viewer/file/file_icons_widget.dart @@ -10,7 +10,7 @@ import 'package:photos/models/file/trash_file.dart'; import 'package:photos/theme/colors.dart'; import "package:photos/theme/ente_theme.dart"; import 'package:photos/ui/sharing/user_avator_widget.dart'; -import "package:photos/utils/data_util.dart"; +import "package:photos/utils/standalone/data.dart"; class ThumbnailPlaceHolder extends StatelessWidget { const ThumbnailPlaceHolder({super.key}); diff --git a/mobile/lib/ui/viewer/file/native_video_player_controls/play_pause_button.dart b/mobile/lib/ui/viewer/file/native_video_player_controls/play_pause_button.dart index 06a1d40708..0b979402ae 100644 --- a/mobile/lib/ui/viewer/file/native_video_player_controls/play_pause_button.dart +++ b/mobile/lib/ui/viewer/file/native_video_player_controls/play_pause_button.dart @@ -1,3 +1,5 @@ +import "dart:async"; + import "package:flutter/material.dart"; import "package:native_video_player/native_video_player.dart"; import "package:photos/theme/colors.dart"; @@ -13,20 +15,29 @@ class PlayPauseButton extends StatefulWidget { class _PlayPauseButtonState extends State { bool _isPlaying = true; + StreamSubscription? subscription; + @override void initState() { super.initState(); - widget.controller?.onPlaybackStatusChanged - .addListener(_onPlaybackStatusChanged); + subscription = widget.controller?.events.listen(listen); } @override void dispose() { - widget.controller?.onPlaybackStatusChanged - .removeListener(_onPlaybackStatusChanged); + subscription?.cancel(); super.dispose(); } + void listen(PlaybackEvent event) { + switch (event) { + case PlaybackStatusChangedEvent(): + _onPlaybackStatusChanged(); + break; + default: + } + } + @override Widget build(BuildContext context) { return GestureDetector( @@ -74,8 +85,7 @@ class _PlayPauseButtonState extends State { ); } - PlaybackStatus? get _playbackStatus => - widget.controller?.playbackInfo?.status; + PlaybackStatus? get _playbackStatus => widget.controller?.playbackStatus; void _onPlaybackStatusChanged() { if (_playbackStatus == PlaybackStatus.playing) { 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 dafd812db4..0348dae0e9 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 @@ -2,10 +2,11 @@ import "dart:async"; import "package:flutter/material.dart"; import "package:native_video_player/native_video_player.dart"; -import "package:photos/service_locator.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/debouncer.dart"; +import "package:photos/utils/standalone/debouncer.dart"; class SeekBar extends StatefulWidget { final NativeVideoPlayerController controller; @@ -19,11 +20,13 @@ class SeekBar extends StatefulWidget { class _SeekBarState extends State with SingleTickerProviderStateMixin { late final AnimationController _animationController; - double _prevPositionFraction = 0.0; final _debouncer = Debouncer( const Duration(milliseconds: 100), executionInterval: const Duration(milliseconds: 325), ); + StreamSubscription? _eventsSubscription; + StreamSubscription? _seekbarSubscription; + @override void initState() { super.initState(); @@ -33,11 +36,18 @@ class _SeekBarState extends State with SingleTickerProviderStateMixin { value: 0, ); - widget.controller.onPlaybackStatusChanged.addListener( - _onPlaybackStatusChanged, - ); - widget.controller.onPlaybackPositionChanged.addListener( - _onPlaybackPositionChanged, + Future.microtask(() { + _seekbarSubscription = + Bus.instance.on().listen((event) { + if (!mounted || _animationController.value == event.position) return; + + _animationController.value = event.position.toDouble(); + setState(() {}); + }); + }); + + _eventsSubscription = widget.controller.events.listen( + _listen, ); _startMovingSeekbar(); @@ -45,13 +55,9 @@ class _SeekBarState extends State with SingleTickerProviderStateMixin { @override void dispose() { + _seekbarSubscription?.cancel(); + _eventsSubscription?.cancel(); _animationController.dispose(); - widget.controller.onPlaybackStatusChanged.removeListener( - _onPlaybackStatusChanged, - ); - widget.controller.onPlaybackPositionChanged.removeListener( - _onPlaybackPositionChanged, - ); _debouncer.cancelDebounceTimer(); super.dispose(); } @@ -65,6 +71,7 @@ class _SeekBarState extends State with SingleTickerProviderStateMixin { return SliderTheme( data: SliderTheme.of(context).copyWith( trackHeight: 1.0, + tickMarkShape: SliderTickMarkShape.noTickMark, thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 8.0), overlayShape: const RoundSliderOverlayShape(overlayRadius: 14.0), activeTrackColor: colorScheme.primary300, @@ -103,7 +110,8 @@ class _SeekBarState extends State with SingleTickerProviderStateMixin { void _seekTo(double value) { _debouncer.run(() async { unawaited( - widget.controller.seekTo((value * widget.duration!).round()), + widget.controller + .seekTo((Duration(seconds: (value * widget.duration!).round()))), ); }); } @@ -133,29 +141,35 @@ class _SeekBarState extends State with SingleTickerProviderStateMixin { }); } + void _listen(PlaybackEvent playerData) { + switch (playerData) { + case PlaybackStatusChangedEvent(): + // Emitted when playback status changes (playing, paused, or stopped) + _onPlaybackStatusChanged(); + break; + case PlaybackPositionChangedEvent(): + // Emitted when playback position changes + _onPlaybackPositionChanged(); + break; + case PlaybackEndedEvent(): + _animationController.value = 0; + default: + } + } + void _onPlaybackStatusChanged() { - if (widget.controller.playbackInfo?.status == PlaybackStatus.paused) { + if (widget.controller.playbackStatus == PlaybackStatus.paused) { _animationController.stop(); } } void _onPlaybackPositionChanged() async { - if (widget.controller.playbackInfo?.status == PlaybackStatus.paused || - (widget.controller.playbackInfo?.status == PlaybackStatus.stopped && - widget.controller.playbackInfo?.positionFraction != 0)) { + if (widget.controller.playbackStatus == PlaybackStatus.paused || + (widget.controller.playbackStatus == PlaybackStatus.stopped && + widget.controller.playbackPosition.inSeconds != 0)) { return; } - final target = widget.controller.playbackInfo?.positionFraction ?? 0; - - //To immediately set the position to 0 when the video ends - if (_prevPositionFraction == 1.0 && target == 0.0) { - setState(() { - _animationController.value = 0; - }); - if (!localSettings.shouldLoopVideo()) { - return; - } - } + final target = widget.controller.playbackPosition.inMilliseconds; //There is a slight delay (around 350 ms) for the event being listened to //by this listener on the next target (target that comes after 0). Adding @@ -164,22 +178,24 @@ class _SeekBarState extends State with SingleTickerProviderStateMixin { await Future.delayed(const Duration(milliseconds: 450)); } + final duration = widget.controller.videoInfo?.durationInMilliseconds; + final double fractionTarget = + duration == null || duration <= 0 ? 0 : target / duration; + if (widget.duration != null) { unawaited( _animationController.animateTo( - target + (1 / widget.duration!), + fractionTarget + (1 / widget.duration!), duration: const Duration(seconds: 1), ), ); } else { unawaited( _animationController.animateTo( - target, + fractionTarget, duration: const Duration(seconds: 1), ), ); } - - _prevPositionFraction = target; } } diff --git a/mobile/lib/ui/viewer/file/preview_video_widget.dart b/mobile/lib/ui/viewer/file/preview_video_widget.dart deleted file mode 100644 index dbaa409ed4..0000000000 --- a/mobile/lib/ui/viewer/file/preview_video_widget.dart +++ /dev/null @@ -1,264 +0,0 @@ -import 'dart:async'; -import "dart:io"; - -import 'package:chewie/chewie.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -import "package:fluttertoast/fluttertoast.dart"; -import "package:logging/logging.dart"; -import 'package:photos/core/constants.dart'; -import "package:photos/core/event_bus.dart"; -import "package:photos/events/guest_view_event.dart"; -import 'package:photos/models/file/file.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/ui/actions/file/file_actions.dart"; -import 'package:photos/ui/viewer/file/thumbnail_widget.dart'; -import "package:photos/ui/viewer/file/video_control.dart"; -import "package:photos/utils/data_util.dart"; -// import 'package:photos/ui/viewer/file/video_controls.dart'; -import "package:photos/utils/dialog_util.dart"; -import 'package:photos/utils/file_util.dart'; -import 'package:photos/utils/toast_util.dart'; -import "package:photos/utils/wakelock_util.dart"; -import 'package:video_player/video_player.dart'; -import 'package:visibility_detector/visibility_detector.dart'; - -class PreviewVideoWidget extends StatefulWidget { - final EnteFile file; - final bool? autoPlay; - final String? tagPrefix; - final Function(bool)? playbackCallback; - final void Function()? onStreamChange; - - const PreviewVideoWidget( - this.file, { - this.autoPlay = true, - this.tagPrefix, - this.playbackCallback, - this.onStreamChange, - super.key, - }); - - @override - State createState() => _PreviewVideoWidgetState(); -} - -class _PreviewVideoWidgetState extends State { - final _logger = Logger("PreviewVideoWidget"); - VideoPlayerController? _videoPlayerController; - ChewieController? _chewieController; - final _progressNotifier = ValueNotifier(null); - bool _isPlaying = false; - final EnteWakeLock _wakeLock = EnteWakeLock(); - bool _isFileSwipeLocked = false; - late final StreamSubscription _fileSwipeLockEventSubscription; - File? previewFile; - - @override - void initState() { - super.initState(); - - _checkForPreview(); - _fileSwipeLockEventSubscription = - Bus.instance.on().listen((event) { - setState(() { - _isFileSwipeLocked = event.swipeLocked; - }); - }); - } - - @override - void dispose() { - _fileSwipeLockEventSubscription.cancel(); - removeCallBack(widget.file); - _videoPlayerController?.dispose(); - _chewieController?.dispose(); - _progressNotifier.dispose(); - _wakeLock.dispose(); - super.dispose(); - } - - Future _checkForPreview() async { - final data = await PreviewVideoStore.instance - .getPlaylist(widget.file) - .onError((error, stackTrace) { - if (!mounted) return; - _logger.warning("Failed to download preview video", error, stackTrace); - Fluttertoast.showToast(msg: "Failed to download preview!"); - return null; - }); - if (!mounted) return; - if (data != null) { - if (flagService.internalUser) { - final d = - FileDataService.instance.previewIds?[widget.file.uploadedFileID!]; - if (d != null && widget.file.fileSize != null) { - // show toast with human readable size - final size = formatBytes(widget.file.fileSize!); - showToast( - context, - "Preview OG Size ($size), previewSize: ${formatBytes(d.objectSize)}", - ); - } else { - showShortToast(context, "Playing preview"); - } - } - previewFile = data.preview; - _setVideoPlayerController(); - } - } - - void _setVideoPlayerController() { - if (!mounted) { - // Note: Do not initiale video player if widget is not mounted. - // On Android, if multiple instance of ExoPlayer is created, it will start - // resulting in playback errors for videos. See https://github.com/google/ExoPlayer/issues/6168 - return; - } - VideoPlayerController videoPlayerController; - videoPlayerController = VideoPlayerController.file(previewFile!); - - debugPrint("videoPlayerController: $videoPlayerController"); - _videoPlayerController = videoPlayerController - ..initialize().whenComplete(() { - if (mounted) { - setState(() {}); - } - }).onError( - (error, stackTrace) { - if (mounted && flagService.internalUser) { - if (error is Exception) { - showErrorDialogForException( - context: context, - exception: error, - message: "Failed to play video\n ${error.toString()}", - ); - } else { - showToast(context, "Failed to play video"); - } - } - }, - ); - } - - @override - Widget build(BuildContext context) { - final content = _videoPlayerController != null && - _videoPlayerController!.value.isInitialized - ? _getVideoPlayer() - : _getLoadingWidget(); - final contentWithDetector = GestureDetector( - onVerticalDragUpdate: _isFileSwipeLocked - ? null - : (d) => { - if (d.delta.dy > dragSensitivity) - { - Navigator.of(context).pop(), - } - else if (d.delta.dy < (dragSensitivity * -1)) - { - showDetailsSheet(context, widget.file), - }, - }, - child: content, - ); - return VisibilityDetector( - key: Key(widget.file.tag), - onVisibilityChanged: (info) { - if (info.visibleFraction < 1) { - if (mounted && _chewieController != null) { - _chewieController!.pause(); - } - } - }, - child: Hero( - tag: widget.tagPrefix! + widget.file.tag, - child: contentWithDetector, - ), - ); - } - - Widget _getLoadingWidget() { - return Stack( - children: [ - _getThumbnail(), - Container( - color: Colors.black12, - constraints: const BoxConstraints.expand(), - ), - Center( - child: SizedBox.fromSize( - size: const Size.square(20), - child: ValueListenableBuilder( - valueListenable: _progressNotifier, - builder: (BuildContext context, double? progress, _) { - return progress == null || progress == 1 - ? const CupertinoActivityIndicator( - color: Colors.white, - ) - : CircularProgressIndicator( - backgroundColor: Colors.black, - value: progress, - valueColor: const AlwaysStoppedAnimation( - Color.fromRGBO(45, 194, 98, 1.0), - ), - ); - }, - ), - ), - ), - ], - ); - } - - Widget _getThumbnail() { - return Container( - color: Colors.black, - constraints: const BoxConstraints.expand(), - child: ThumbnailWidget( - widget.file, - fit: BoxFit.contain, - ), - ); - } - - Future _keepScreenAliveOnPlaying(bool isPlaying) async { - if (isPlaying) { - _wakeLock.enable(); - } - if (!isPlaying) { - _wakeLock.disable(); - } - } - - Widget _getVideoPlayer() { - _videoPlayerController!.addListener(() { - if (_isPlaying != _videoPlayerController!.value.isPlaying) { - _isPlaying = _videoPlayerController!.value.isPlaying; - if (widget.playbackCallback != null) { - widget.playbackCallback!(_isPlaying); - } - unawaited(_keepScreenAliveOnPlaying(_isPlaying)); - } - }); - _chewieController = ChewieController( - videoPlayerController: _videoPlayerController!, - aspectRatio: _videoPlayerController!.value.aspectRatio, - autoPlay: widget.autoPlay!, - autoInitialize: true, - looping: true, - allowMuting: true, - allowFullScreen: false, - customControls: VideoControls( - file: widget.file, - onStreamChange: widget.onStreamChange, - ), - ); - return Container( - color: Colors.black, - child: Chewie(controller: _chewieController!), - ); - } -} diff --git a/mobile/lib/ui/viewer/file/video_control.dart b/mobile/lib/ui/viewer/file/video_control.dart deleted file mode 100644 index d662819003..0000000000 --- a/mobile/lib/ui/viewer/file/video_control.dart +++ /dev/null @@ -1,518 +0,0 @@ -import 'dart:async'; - -import 'package:chewie/chewie.dart'; -import 'package:flutter/material.dart'; -import "package:photos/models/file/file.dart"; -import "package:photos/theme/colors.dart"; -import "package:photos/theme/ente_theme.dart"; -import "package:photos/ui/common/loading_widget.dart"; -import "package:photos/ui/viewer/file/preview_status_widget.dart"; -import "package:photos/utils/debouncer.dart"; -import 'package:video_player/video_player.dart'; - -class VideoControls extends StatefulWidget { - const VideoControls({ - super.key, - required this.file, - required this.onStreamChange, - }); - final EnteFile file; - final void Function()? onStreamChange; - - @override - State createState() { - return _VideoControlsState(); - } -} - -class _VideoControlsState extends State { - VideoPlayerValue? _latestValue; - bool _hideStuff = true; - Timer? _hideTimer; - Timer? _initTimer; - Timer? _showAfterExpandCollapseTimer; - bool _dragging = false; - bool _displayTapped = false; - Timer? _bufferingDisplayTimer; - bool _displayBufferingIndicator = false; - - final barHeight = 120.0; - final marginSize = 5.0; - - late VideoPlayerController controller; - ChewieController? chewieController; - - void _bufferingTimerTimeout() { - _displayBufferingIndicator = true; - if (mounted) { - setState(() {}); - } - } - - @override - Widget build(BuildContext context) { - if (_latestValue!.hasError) { - return chewieController!.errorBuilder != null - ? chewieController!.errorBuilder!( - context, - chewieController!.videoPlayerController.value.errorDescription!, - ) - : Center( - child: Icon( - Icons.error, - color: Theme.of(context).colorScheme.onSurface, - size: 42, - ), - ); - } - - return MouseRegion( - onHover: (_) { - _cancelAndRestartTimer(); - }, - child: GestureDetector( - onTap: () => _cancelAndRestartTimer(), - child: AbsorbPointer( - absorbing: _hideStuff, - child: Stack( - children: [ - if (_latestValue != null && - !_latestValue!.isPlaying && - _latestValue!.isBuffering || - _displayBufferingIndicator) - const Align( - alignment: Alignment.center, - child: Center( - child: EnteLoadingWidget( - size: 32, - color: fillBaseDark, - padding: 0, - ), - ), - ) - else - Positioned.fill(child: _buildHitArea()), - Align( - alignment: Alignment.bottomCenter, - child: SafeArea( - top: false, - left: false, - right: false, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - PreviewStatusWidget( - showControls: !_hideStuff, - file: widget.file, - isPreviewPlayer: true, - onStreamChange: widget.onStreamChange, - ), - _buildBottomBar(context), - ], - ), - ), - ), - ], - ), - ), - ), - ); - } - - @override - void dispose() { - _dispose(); - super.dispose(); - } - - void _dispose() { - controller.removeListener(_updateState); - _hideTimer?.cancel(); - _initTimer?.cancel(); - _showAfterExpandCollapseTimer?.cancel(); - } - - @override - void didChangeDependencies() { - final oldController = chewieController; - chewieController = ChewieController.of(context); - controller = chewieController!.videoPlayerController; - - if (oldController != chewieController) { - _dispose(); - _initialize(); - } - - super.didChangeDependencies(); - } - - Widget _buildBottomBar( - BuildContext context, - ) { - return Container( - padding: const EdgeInsets.only(bottom: 60), - height: 100, - child: AnimatedOpacity( - opacity: _hideStuff ? 0.0 : 1.0, - duration: const Duration(milliseconds: 300), - child: _SeekBarAndDuration( - controller: controller, - latestValue: _latestValue, - updateDragging: (bool value) { - setState(() { - _dragging = value; - }); - }, - ), - ), - ); - } - - Widget _buildHitArea() { - return GestureDetector( - onTap: () { - if (_latestValue != null) { - if (_displayTapped) { - setState(() { - _hideStuff = !_hideStuff; - }); - } else { - _cancelAndRestartTimer(); - } - } else { - _playPause(); - - setState(() { - _hideStuff = true; - }); - } - }, - behavior: HitTestBehavior.opaque, - child: AnimatedOpacity( - opacity: _latestValue != null && !_hideStuff && !_dragging ? 1.0 : 0.0, - duration: const Duration(milliseconds: 300), - child: Center( - child: _PlayPauseButton( - _playPause, - _latestValue!.isPlaying, - ), - ), - ), - ); - } - - void _cancelAndRestartTimer() { - _hideTimer?.cancel(); - _startHideTimer(); - - setState(() { - _hideStuff = false; - _displayTapped = true; - }); - } - - Future _initialize() async { - controller.addListener(_updateState); - - _updateState(); - - if ((controller.value.isPlaying) || chewieController!.autoPlay) { - _startHideTimer(); - } - - if (chewieController!.showControlsOnInitialize) { - _initTimer = Timer(const Duration(milliseconds: 200), () { - setState(() { - _hideStuff = false; - }); - }); - } - } - - void _playPause() { - final bool isFinished = _latestValue!.position >= _latestValue!.duration; - - setState(() { - if (controller.value.isPlaying) { - _hideStuff = false; - _hideTimer?.cancel(); - controller.pause(); - } else { - _cancelAndRestartTimer(); - - if (!controller.value.isInitialized) { - controller.initialize().then((_) { - controller.play(); - }); - } else { - if (isFinished) { - controller.seekTo(const Duration(seconds: 0)); - } - controller.play(); - } - } - }); - } - - void _startHideTimer() { - _hideTimer = Timer(const Duration(seconds: 2), () { - setState(() { - _hideStuff = true; - }); - }); - } - - void _updateState() { - // display the progress bar indicator only after the buffering delay if it has been set - if (chewieController?.progressIndicatorDelay != null) { - if (controller.value.isBuffering) { - _bufferingDisplayTimer ??= Timer( - chewieController!.progressIndicatorDelay!, - _bufferingTimerTimeout, - ); - } else { - _bufferingDisplayTimer?.cancel(); - _bufferingDisplayTimer = null; - _displayBufferingIndicator = false; - } - } else { - _displayBufferingIndicator = controller.value.isBuffering; - } - setState(() { - _latestValue = controller.value; - }); - } -} - -class _SeekBarAndDuration extends StatelessWidget { - final VideoPlayerController? controller; - final VideoPlayerValue? latestValue; - final Function(bool) updateDragging; - - const _SeekBarAndDuration({ - required this.controller, - required this.latestValue, - required this.updateDragging, - }); - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric( - horizontal: 8, - ), - child: Container( - padding: const EdgeInsets.fromLTRB( - 16, - 4, - 16, - 4, - ), - decoration: BoxDecoration( - color: Colors.black.withOpacity(0.3), - borderRadius: const BorderRadius.all( - Radius.circular(8), - ), - border: Border.all( - color: strokeFaintDark, - width: 1, - ), - ), - child: Row( - children: [ - if (latestValue?.position == null) - Text( - "0:00", - style: getEnteTextTheme( - context, - ).mini.copyWith( - color: textBaseDark, - ), - ) - else - Text( - _secondsToDuration(latestValue!.position.inSeconds), - style: getEnteTextTheme( - context, - ).mini.copyWith( - color: textBaseDark, - ), - ), - Expanded( - child: _SeekBar(controller!, updateDragging), - ), - Text( - _secondsToDuration( - latestValue?.duration.inSeconds ?? 0, - ), - style: getEnteTextTheme( - context, - ).mini.copyWith( - color: textBaseDark, - ), - ), - ], - ), - ), - ); - } - - /// Returns the duration in the format "h:mm:ss" or "m:ss". - String _secondsToDuration(int totalSeconds) { - final hours = totalSeconds ~/ 3600; - final minutes = (totalSeconds % 3600) ~/ 60; - final seconds = totalSeconds % 60; - - if (hours > 0) { - return '${hours.toString().padLeft(1, '0')}:${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}'; - } else { - return '${minutes.toString().padLeft(1, '0')}:${seconds.toString().padLeft(2, '0')}'; - } - } -} - -class _SeekBar extends StatefulWidget { - final VideoPlayerController controller; - final Function(bool) updateDragging; - const _SeekBar( - this.controller, - this.updateDragging, - ); - - @override - State<_SeekBar> createState() => _SeekBarState(); -} - -class _SeekBarState extends State<_SeekBar> { - double _sliderValue = 0.0; - final _debouncer = Debouncer( - const Duration(milliseconds: 300), - executionInterval: const Duration(milliseconds: 300), - ); - bool _controllerWasPlaying = false; - - @override - void initState() { - super.initState(); - widget.controller.addListener(updateSlider); - } - - void updateSlider() { - if (widget.controller.value.isInitialized) { - setState(() { - _sliderValue = widget.controller.value.position.inSeconds.toDouble(); - }); - } - } - - @override - void dispose() { - _debouncer.cancelDebounceTimer(); - widget.controller.removeListener(updateSlider); - super.dispose(); - } - - @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, - inactiveTrackColor: fillMutedDark, - thumbColor: backgroundElevatedLight, - overlayColor: fillMutedDark, - ), - child: Slider( - min: 0.0, - max: widget.controller.value.duration.inSeconds.toDouble(), - value: _sliderValue, - onChangeStart: (value) async { - widget.updateDragging(true); - _controllerWasPlaying = widget.controller.value.isPlaying; - if (_controllerWasPlaying) { - await widget.controller.pause(); - } - }, - onChanged: (value) { - if (mounted) { - setState(() { - _sliderValue = value; - }); - } - - _debouncer.run(() async { - await widget.controller.seekTo(Duration(seconds: value.toInt())); - }); - }, - divisions: 4500, - onChangeEnd: (value) async { - await widget.controller.seekTo(Duration(seconds: value.toInt())); - - if (_controllerWasPlaying) { - await widget.controller.play(); - } - widget.updateDragging(false); - }, - allowedInteraction: SliderInteraction.tapAndSlide, - ), - ); - } -} - -class _PlayPauseButton extends StatefulWidget { - final void Function() playPause; - final bool isPlaying; - const _PlayPauseButton( - this.playPause, - this.isPlaying, - ); - - @override - State<_PlayPauseButton> createState() => _PlayPauseButtonState(); -} - -class _PlayPauseButtonState extends State<_PlayPauseButton> { - @override - Widget build(BuildContext context) { - return Container( - width: 54, - height: 54, - decoration: BoxDecoration( - color: Colors.black.withOpacity(0.3), - shape: BoxShape.circle, - border: Border.all( - color: strokeFaintDark, - width: 1, - ), - ), - child: GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: widget.playPause, - child: AnimatedSwitcher( - duration: const Duration(milliseconds: 250), - transitionBuilder: (Widget child, Animation animation) { - return ScaleTransition(scale: animation, child: child); - }, - switchInCurve: Curves.easeInOutQuart, - switchOutCurve: Curves.easeInOutQuart, - child: widget.isPlaying - ? const Icon( - Icons.pause, - size: 32, - key: ValueKey("pause"), - color: Colors.white, - ) - : const Icon( - Icons.play_arrow, - size: 36, - key: ValueKey("play"), - color: Colors.white, - ), - ), - ), - ); - } -} diff --git a/mobile/lib/ui/viewer/file/video_control/custom_progress_bar.dart b/mobile/lib/ui/viewer/file/video_control/custom_progress_bar.dart new file mode 100644 index 0000000000..563f6d1a07 --- /dev/null +++ b/mobile/lib/ui/viewer/file/video_control/custom_progress_bar.dart @@ -0,0 +1,41 @@ +// ignore_for_file: implementation_imports + +import "package:chewie/src/chewie_progress_colors.dart"; +import "package:chewie/src/progress_bar.dart"; +import "package:flutter/material.dart"; +import "package:flutter/widgets.dart"; +import "package:video_player/video_player.dart"; + +class CustomProgressBar extends StatelessWidget { + CustomProgressBar( + this.controller, { + ChewieProgressColors? colors, + this.onDragEnd, + this.onDragStart, + this.onDragUpdate, + super.key, + this.draggableProgressBar = true, + }) : colors = colors ?? ChewieProgressColors(); + + final VideoPlayerController controller; + final ChewieProgressColors colors; + final Function()? onDragStart; + final Function()? onDragEnd; + final Function()? onDragUpdate; + final bool draggableProgressBar; + + @override + Widget build(BuildContext context) { + return VideoProgressBar( + controller, + barHeight: 1.5, + handleHeight: 8, + drawShadow: true, + colors: colors, + onDragEnd: onDragEnd, + onDragStart: onDragStart, + onDragUpdate: onDragUpdate, + draggableProgressBar: draggableProgressBar, + ); + } +} diff --git a/mobile/lib/ui/viewer/file/video_exif_dialog.dart b/mobile/lib/ui/viewer/file/video_exif_dialog.dart index ba650d469f..51dc0f1f64 100644 --- a/mobile/lib/ui/viewer/file/video_exif_dialog.dart +++ b/mobile/lib/ui/viewer/file/video_exif_dialog.dart @@ -7,7 +7,7 @@ import "package:photos/theme/ente_theme.dart"; class VideoExifDialog extends StatelessWidget { final FFProbeProps props; - const VideoExifDialog({Key? key, required this.props}) : super(key: key); + const VideoExifDialog({super.key, required this.props}); @override Widget build(BuildContext context) { diff --git a/mobile/lib/ui/viewer/file/video_widget.dart b/mobile/lib/ui/viewer/file/video_widget.dart index 237e4cdaca..d468842c56 100644 --- a/mobile/lib/ui/viewer/file/video_widget.dart +++ b/mobile/lib/ui/viewer/file/video_widget.dart @@ -1,15 +1,23 @@ import "dart:async"; +import "dart:io"; import "package:flutter/material.dart"; +import "package:fluttertoast/fluttertoast.dart"; import "package:logging/logging.dart"; import "package:photos/core/event_bus.dart"; +import "package:photos/events/stream_switched_event.dart"; import "package:photos/events/use_media_kit_for_video.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/ui/viewer/file/preview_video_widget.dart"; +import "package:photos/theme/colors.dart"; +import "package:photos/ui/common/loading_widget.dart"; +import "package:photos/ui/notification/toast.dart"; import "package:photos/ui/viewer/file/video_widget_media_kit_new.dart"; import "package:photos/ui/viewer/file/video_widget_native.dart"; +import "package:photos/utils/standalone/data.dart"; class VideoWidget extends StatefulWidget { final EnteFile file; @@ -32,6 +40,11 @@ class _VideoWidgetState extends State { late final StreamSubscription useMediaKitForVideoSubscription; late bool selectPreviewForPlay = widget.file.localID == null; + PlaylistData? playlistData; + final nativePlayerKey = GlobalKey(); + final mediaKitKey = GlobalKey(); + + bool isPreviewLoadable = true; @override void initState() { @@ -43,6 +56,7 @@ class _VideoWidgetState extends State { useNativeVideoPlayer = false; }); }); + _checkForPreview(); } @override @@ -51,49 +65,121 @@ class _VideoWidgetState extends State { super.dispose(); } + Future _checkForPreview() async { + final isPreviewAvailable = FileDataService.instance.previewIds + ?.containsKey(widget.file.uploadedFileID) ?? + false; + if (!PreviewVideoStore.instance.isVideoStreamingEnabled || + !isPreviewAvailable) { + return; + } + widget.playbackCallback?.call(false); + final data = await PreviewVideoStore.instance + .getPlaylist(widget.file) + .onError((error, stackTrace) { + if (!mounted) return; + _logger.warning("Failed to download preview video", error, stackTrace); + Fluttertoast.showToast(msg: "Failed to download preview!"); + return null; + }); + if (!mounted) return; + if (data != null) { + if (flagService.internalUser) { + final d = + FileDataService.instance.previewIds?[widget.file.uploadedFileID!]; + if (d != null && widget.file.fileSize != null) { + // show toast with human readable size + final size = formatBytes(widget.file.fileSize!); + showToast( + context, + gravity: ToastGravity.TOP, + "[i] Preview OG Size ($size), previewSize: ${formatBytes(d.objectSize)}", + ); + } + } + playlistData = data; + } else { + isPreviewLoadable = false; + } + setState(() {}); + } + @override Widget build(BuildContext context) { - final isPreviewVideoPlayable = + final isPreviewVideoPlayable = isPreviewLoadable && PreviewVideoStore.instance.isVideoStreamingEnabled && - widget.file.isUploaded && - (FileDataService.instance.previewIds - ?.containsKey(widget.file.uploadedFileID!) ?? - false); - if (isPreviewVideoPlayable && selectPreviewForPlay) { - return PreviewVideoWidget( - widget.file, - tagPrefix: widget.tagPrefix, - playbackCallback: widget.playbackCallback, - onStreamChange: () { - setState(() { - selectPreviewForPlay = false; - }); - }, + widget.file.isUploaded && + (FileDataService.instance.previewIds + ?.containsKey(widget.file.uploadedFileID!) ?? + false); + final playPreview = isPreviewVideoPlayable && selectPreviewForPlay; + if (playPreview && playlistData == null) { + return Center( + child: Container( + width: 48, + height: 48, + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(24), + color: Colors.black.withOpacity(0.3), + border: Border.all( + color: strokeFaintDark, + width: 1, + ), + ), + child: const EnteLoadingWidget( + size: 32, + color: fillBaseDark, + padding: 0, + ), + ), ); } - if (useNativeVideoPlayer) { + if (useNativeVideoPlayer && !playPreview || + playPreview && Platform.isAndroid) { return VideoWidgetNative( widget.file, + key: nativePlayerKey, tagPrefix: widget.tagPrefix, playbackCallback: widget.playbackCallback, + playlistData: playlistData, + selectedPreview: playPreview, onStreamChange: () { setState(() { - selectPreviewForPlay = true; - }); - }, - ); - } else { - return VideoWidgetMediaKitNew( - widget.file, - tagPrefix: widget.tagPrefix, - playbackCallback: widget.playbackCallback, - onStreamChange: () { - setState(() { - selectPreviewForPlay = true; + selectPreviewForPlay = !selectPreviewForPlay; + Bus.instance.fire( + StreamSwitchedEvent( + selectPreviewForPlay, + Platform.isAndroid && useNativeVideoPlayer + ? PlayerType.nativeVideoPlayer + : PlayerType.mediaKit, + ), + ); }); }, ); } + return VideoWidgetMediaKitNew( + widget.file, + key: mediaKitKey, + tagPrefix: widget.tagPrefix, + playbackCallback: widget.playbackCallback, + preview: playlistData?.preview, + selectedPreview: playPreview, + onStreamChange: () { + setState(() { + selectPreviewForPlay = !selectPreviewForPlay; + Bus.instance.fire( + StreamSwitchedEvent( + selectPreviewForPlay, + Platform.isAndroid + ? PlayerType.nativeVideoPlayer + : PlayerType.mediaKit, + ), + ); + }); + }, + ); } } 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 4ce67981b6..ae1f86dd6b 100644 --- a/mobile/lib/ui/viewer/file/video_widget_media_kit.dart +++ b/mobile/lib/ui/viewer/file/video_widget_media_kit.dart @@ -17,10 +17,10 @@ import "package:photos/services/files_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/notification/toast.dart"; import "package:photos/ui/viewer/file/thumbnail_widget.dart"; import "package:photos/utils/dialog_util.dart"; import "package:photos/utils/file_util.dart"; -import "package:photos/utils/toast_util.dart"; class VideoWidgetMediaKit extends StatefulWidget { final EnteFile file; 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 new file mode 100644 index 0000000000..ba4b730206 --- /dev/null +++ b/mobile/lib/ui/viewer/file/video_widget_media_kit_common.dart @@ -0,0 +1,471 @@ +import "dart:async"; + +import "package:flutter/material.dart"; +import "package:media_kit_video/media_kit_video.dart"; +import "package:photos/models/file/file.dart"; +import "package:photos/theme/colors.dart"; +import "package:photos/theme/ente_theme.dart"; +import "package:photos/ui/common/loading_widget.dart"; +import "package:photos/ui/viewer/file/preview_status_widget.dart"; +import "package:photos/utils/standalone/date_time.dart"; +import "package:photos/utils/standalone/debouncer.dart"; + +class VideoWidget extends StatefulWidget { + final EnteFile file; + final VideoController controller; + final Function(bool)? playbackCallback; + final bool isFromMemories; + final void Function() onStreamChange; + final bool isPreviewPlayer; + + const VideoWidget( + this.file, + this.controller, + this.playbackCallback, { + super.key, + required this.isFromMemories, + // ignore: unused_element + required this.onStreamChange, + required this.isPreviewPlayer, + }); + + @override + State createState() => _VideoWidgetState(); +} + +class _VideoWidgetState extends State { + final showControlsNotifier = ValueNotifier(true); + static const double verticalMargin = 64; + final _hideControlsDebouncer = Debouncer( + const Duration(milliseconds: 2000), + ); + final _isSeekingNotifier = ValueNotifier(false); + late final StreamSubscription _isPlayingStreamSubscription; + + @override + void initState() { + _isPlayingStreamSubscription = + widget.controller.player.stream.playing.listen((isPlaying) { + if (isPlaying && !_isSeekingNotifier.value) { + _hideControlsDebouncer.run(() async { + showControlsNotifier.value = false; + widget.playbackCallback?.call(true); + }); + } + }); + + _isSeekingNotifier.addListener(isSeekingListener); + super.initState(); + } + + @override + void dispose() { + showControlsNotifier.dispose(); + _isPlayingStreamSubscription.cancel(); + _hideControlsDebouncer.cancelDebounceTimer(); + _isSeekingNotifier.removeListener(isSeekingListener); + _isSeekingNotifier.dispose(); + super.dispose(); + } + + void isSeekingListener() { + if (_isSeekingNotifier.value) { + _hideControlsDebouncer.cancelDebounceTimer(); + } else { + if (widget.controller.player.state.playing) { + _hideControlsDebouncer.run(() async { + showControlsNotifier.value = false; + widget.playbackCallback?.call(true); + }); + } + } + } + + @override + Widget build(BuildContext context) { + return Video( + controller: widget.controller, + controls: (state) { + return ValueListenableBuilder( + valueListenable: showControlsNotifier, + builder: (context, value, _) { + return AnimatedOpacity( + duration: const Duration(milliseconds: 200), + opacity: value ? 1 : 0, + curve: Curves.easeInOutQuad, + child: Stack( + alignment: Alignment.center, + children: [ + GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + showControlsNotifier.value = !showControlsNotifier.value; + if (widget.playbackCallback != null) { + widget.playbackCallback!( + !showControlsNotifier.value, + ); + } + }, + 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: [ + widget.file.caption != null + ? Padding( + padding: const EdgeInsets.fromLTRB( + 16, + 12, + 16, + 8, + ), + child: Text( + widget.file.caption!, + overflow: TextOverflow.ellipsis, + maxLines: 1, + style: getEnteTextTheme(context) + .mini + .copyWith( + color: textBaseDark, + ), + textAlign: TextAlign.center, + ), + ) + : const SizedBox.shrink(), + PreviewStatusWidget( + showControls: value, + file: widget.file, + isPreviewPlayer: widget.isPreviewPlayer, + onStreamChange: widget.onStreamChange, + ), + SeekBarAndDuration( + controller: widget.controller, + isSeekingNotifier: _isSeekingNotifier, + ), + ], + ), + ), + ), + ), + ), + ], + ), + ); + }, + ); + }, + ); + } +} + +class PlayPauseButtonMediaKit extends StatefulWidget { + final VideoController? controller; + const PlayPauseButtonMediaKit( + this.controller, { + super.key, + }); + + @override + State createState() => _PlayPauseButtonState(); +} + +class _PlayPauseButtonState extends State { + bool _isPlaying = true; + late final StreamSubscription? isPlayingStreamSubscription; + late StreamSubscription? _bufferStateSubscription; + late var buffering = widget.controller?.player.state.buffering ?? true; + + @override + void initState() { + super.initState(); + + isPlayingStreamSubscription = + widget.controller?.player.stream.playing.listen((isPlaying) { + setState(() { + _isPlaying = isPlaying; + }); + }); + + _bufferStateSubscription = + widget.controller?.player.stream.buffering.listen( + (event) => setState(() => buffering = event), + ); + } + + @override + void dispose() { + isPlayingStreamSubscription?.cancel(); + _bufferStateSubscription?.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + if (buffering) return const EnteLoadingWidget(); + + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + if (widget.controller?.player.state.playing ?? false) { + widget.controller?.player.pause(); + } else { + widget.controller?.player.play(); + } + }, + child: Container( + width: 54, + height: 54, + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.3), + shape: BoxShape.circle, + border: Border.all( + color: strokeFaintDark, + width: 1, + ), + ), + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 250), + transitionBuilder: (Widget child, Animation animation) { + return ScaleTransition(scale: animation, child: child); + }, + switchInCurve: Curves.easeInOutQuart, + switchOutCurve: Curves.easeInOutQuart, + child: _isPlaying + ? const Icon( + Icons.pause, + size: 32, + key: ValueKey("pause"), + color: Colors.white, + ) + : const Icon( + Icons.play_arrow, + size: 36, + key: ValueKey("play"), + color: Colors.white, + ), + ), + ), + ); + } +} + +class SeekBarAndDuration extends StatelessWidget { + final VideoController? controller; + final ValueNotifier isSeekingNotifier; + + const SeekBarAndDuration({ + super.key, + required this.controller, + required this.isSeekingNotifier, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric( + horizontal: 8, + ), + child: Container( + padding: const EdgeInsets.fromLTRB( + 16, + 4, + 16, + 4, + ), + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.3), + borderRadius: const BorderRadius.all( + Radius.circular(8), + ), + border: Border.all( + color: strokeFaintDark, + width: 1, + ), + ), + child: Row( + children: [ + StreamBuilder( + stream: controller?.player.stream.position, + builder: (context, snapshot) { + if (snapshot.data == null) { + return Text( + "0:00", + style: getEnteTextTheme( + context, + ).mini.copyWith( + color: textBaseDark, + ), + ); + } + return Text( + secondsToDuration(snapshot.data!.inSeconds), + style: getEnteTextTheme( + context, + ).mini.copyWith( + color: textBaseDark, + ), + ); + }, + ), + Expanded( + child: SeekBar( + controller!, + isSeekingNotifier, + ), + ), + Text( + _secondsToDuration( + controller!.player.state.duration.inSeconds, + ), + style: getEnteTextTheme( + context, + ).mini.copyWith( + color: textBaseDark, + ), + ), + ], + ), + ), + ); + } + + /// Returns the duration in the format "h:mm:ss" or "m:ss". + String _secondsToDuration(int totalSeconds) { + final hours = totalSeconds ~/ 3600; + final minutes = (totalSeconds % 3600) ~/ 60; + final seconds = totalSeconds % 60; + + if (hours > 0) { + return '${hours.toString().padLeft(1, '0')}:${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}'; + } else { + return '${minutes.toString().padLeft(1, '0')}:${seconds.toString().padLeft(2, '0')}'; + } + } +} + +class SeekBar extends StatefulWidget { + final VideoController controller; + final ValueNotifier isSeekingNotifier; + const SeekBar( + this.controller, + this.isSeekingNotifier, { + super.key, + }); + + @override + State createState() => _SeekBarState(); +} + +class _SeekBarState extends State { + double _sliderValue = 0.0; + late final StreamSubscription _positionStreamSubscription; + final _debouncer = Debouncer( + const Duration(milliseconds: 300), + executionInterval: const Duration(milliseconds: 300), + ); + @override + void initState() { + super.initState(); + _positionStreamSubscription = + widget.controller.player.stream.position.listen((event) { + if (widget.isSeekingNotifier.value) return; + if (mounted) { + setState(() { + _sliderValue = event.inMilliseconds / + widget.controller.player.state.duration.inMilliseconds; + if (_sliderValue.isNaN) { + _sliderValue = 0.0; + } + }); + } + }); + } + + @override + void dispose() { + _positionStreamSubscription.cancel(); + _debouncer.cancelDebounceTimer(); + super.dispose(); + } + + @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, + inactiveTrackColor: fillMutedDark, + thumbColor: backgroundElevatedLight, + overlayColor: fillMutedDark, + ), + child: Slider( + min: 0.0, + max: 1.0, + value: _sliderValue, + onChangeStart: (value) { + if (mounted) { + setState(() { + widget.isSeekingNotifier.value = true; + }); + } + }, + onChanged: (value) { + if (mounted) { + setState(() { + _sliderValue = value; + }); + } + + _debouncer.run(() async { + await widget.controller.player.seek( + Duration( + milliseconds: (value * + widget.controller.player.state.duration.inMilliseconds) + .round(), + ), + ); + }); + }, + divisions: 4500, + onChangeEnd: (value) async { + await widget.controller.player.seek( + Duration( + milliseconds: (value * + widget.controller.player.state.duration.inMilliseconds) + .round(), + ), + ); + if (mounted) { + setState(() { + widget.isSeekingNotifier.value = false; + }); + } + }, + allowedInteraction: SliderInteraction.tapAndSlide, + ), + ); + } +} 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 7d1ba08cfa..a338114326 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 @@ -9,32 +9,38 @@ import "package:photos/core/constants.dart"; import "package:photos/core/event_bus.dart"; import "package:photos/events/guest_view_event.dart"; import "package:photos/events/pause_video_event.dart"; +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/service_locator.dart"; import "package:photos/services/files_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/viewer/file/preview_status_widget.dart"; -import "package:photos/utils/debouncer.dart"; +import "package:photos/ui/common/loading_widget.dart"; +import "package:photos/ui/notification/toast.dart"; +import "package:photos/ui/viewer/file/video_widget_media_kit_common.dart" + as common; import "package:photos/utils/dialog_util.dart"; import "package:photos/utils/file_util.dart"; -import "package:photos/utils/toast_util.dart"; class VideoWidgetMediaKitNew extends StatefulWidget { final EnteFile file; final String? tagPrefix; final Function(bool)? playbackCallback; final bool isFromMemories; - final void Function()? onStreamChange; + final void Function() onStreamChange; + final File? preview; + final bool selectedPreview; const VideoWidgetMediaKitNew( this.file, { this.tagPrefix, this.playbackCallback, this.isFromMemories = false, - this.onStreamChange, + required this.onStreamChange, + this.preview, + required this.selectedPreview, super.key, }); @@ -53,6 +59,7 @@ class _VideoWidgetMediaKitNewState extends State bool isGuestView = false; late final StreamSubscription _guestViewEventSubscription; bool _isGuestView = false; + StreamSubscription? _streamSwitchedSubscription; @override void initState() { @@ -61,6 +68,38 @@ class _VideoWidgetMediaKitNewState extends State ); super.initState(); WidgetsBinding.instance.addObserver(this); + + if (widget.selectedPreview) { + loadPreview(); + } else { + loadOriginal(); + } + + pauseVideoSubscription = Bus.instance.on().listen((event) { + player.pause(); + }); + _guestViewEventSubscription = + Bus.instance.on().listen((event) { + setState(() { + _isGuestView = event.isGuestView; + }); + }); + _streamSwitchedSubscription = + Bus.instance.on().listen((event) { + if (event.type != PlayerType.mediaKit || !mounted) return; + if (event.selectedPreview) { + loadPreview(); + } else { + loadOriginal(); + } + }); + } + + void loadPreview() { + _setVideoController(widget.preview!.path); + } + + void loadOriginal() { if (widget.file.isRemoteFile) { _loadNetworkVideo(); _setFileSizeIfNull(); @@ -88,16 +127,6 @@ class _VideoWidgetMediaKitNewState extends State } }); } - - pauseVideoSubscription = Bus.instance.on().listen((event) { - player.pause(); - }); - _guestViewEventSubscription = - Bus.instance.on().listen((event) { - setState(() { - _isGuestView = event.isGuestView; - }); - }); } @override @@ -111,6 +140,7 @@ class _VideoWidgetMediaKitNewState extends State @override void dispose() { + _streamSwitchedSubscription?.cancel(); _guestViewEventSubscription.cancel(); pauseVideoSubscription.cancel(); removeCallBack(widget.file); @@ -137,44 +167,21 @@ class _VideoWidgetMediaKitNewState extends State }, child: Center( child: controller != null - ? _VideoWidget( + ? common.VideoWidget( widget.file, controller!, widget.playbackCallback, isFromMemories: widget.isFromMemories, + onStreamChange: widget.onStreamChange, + isPreviewPlayer: widget.selectedPreview, ) - // : Stack( - // children: [ - // _getThumbnail(), - // Container( - // color: Colors.black12, - // constraints: const BoxConstraints.expand(), - // ), - // Center( - // child: SizedBox.fromSize( - // size: const Size.square(20), - // child: ValueListenableBuilder( - // valueListenable: _progressNotifier, - // builder: (BuildContext context, double? progress, _) { - // return progress == null || progress == 1 - // ? const CupertinoActivityIndicator( - // color: Colors.white, - // ) - // : CircularProgressIndicator( - // backgroundColor: Colors.black, - // value: progress, - // valueColor: - // const AlwaysStoppedAnimation( - // Color.fromRGBO(45, 194, 98, 1.0), - // ), - // ); - // }, - // ), - // ), - // ), - // ], - // ), - : const SizedBox.shrink(), + : const Center( + child: EnteLoadingWidget( + size: 32, + color: fillBaseDark, + padding: 0, + ), + ), ), ); } @@ -222,458 +229,16 @@ class _VideoWidgetMediaKitNewState extends State void _setVideoController(String url) { if (mounted) { setState(() { - player.setPlaylistMode(PlaylistMode.single); - controller = VideoController(player); + if (controller == null) { + player.setPlaylistMode( + localSettings.shouldLoopVideo() + ? PlaylistMode.single + : PlaylistMode.none, + ); + controller = VideoController(player); + } player.open(Media(url), play: _isAppInFG); }); } } } - -class _VideoWidget extends StatefulWidget { - final EnteFile file; - final VideoController controller; - final Function(bool)? playbackCallback; - final bool isFromMemories; - final void Function()? onStreamChange; - - const _VideoWidget( - this.file, - this.controller, - this.playbackCallback, { - required this.isFromMemories, - this.onStreamChange, - }); - - @override - State<_VideoWidget> createState() => __VideoWidgetState(); -} - -class __VideoWidgetState extends State<_VideoWidget> { - final showControlsNotifier = ValueNotifier(true); - static const verticalMargin = 72.0; - final _hideControlsDebouncer = Debouncer( - const Duration(milliseconds: 2000), - ); - final _isSeekingNotifier = ValueNotifier(false); - late final StreamSubscription _isPlayingStreamSubscription; - - @override - void initState() { - _isPlayingStreamSubscription = - widget.controller.player.stream.playing.listen((isPlaying) { - if (isPlaying && !_isSeekingNotifier.value) { - _hideControlsDebouncer.run(() async { - showControlsNotifier.value = false; - widget.playbackCallback?.call(true); - }); - } - }); - - _isSeekingNotifier.addListener(isSeekingListener); - super.initState(); - } - - @override - void dispose() { - showControlsNotifier.dispose(); - _isPlayingStreamSubscription.cancel(); - _hideControlsDebouncer.cancelDebounceTimer(); - _isSeekingNotifier.removeListener(isSeekingListener); - _isSeekingNotifier.dispose(); - super.dispose(); - } - - void isSeekingListener() { - if (_isSeekingNotifier.value) { - _hideControlsDebouncer.cancelDebounceTimer(); - } else { - if (widget.controller.player.state.playing) { - _hideControlsDebouncer.run(() async { - showControlsNotifier.value = false; - widget.playbackCallback?.call(false); - }); - } - } - } - - @override - Widget build(BuildContext context) { - return Video( - controller: widget.controller, - controls: (state) { - return ValueListenableBuilder( - valueListenable: showControlsNotifier, - builder: (context, value, _) { - return AnimatedOpacity( - duration: const Duration(milliseconds: 200), - opacity: value ? 1 : 0, - curve: Curves.easeInOutQuad, - child: Stack( - alignment: Alignment.center, - children: [ - GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () { - showControlsNotifier.value = !showControlsNotifier.value; - if (widget.playbackCallback != null) { - widget.playbackCallback!( - !showControlsNotifier.value, - ); - } - }, - 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: [ - widget.file.caption != null - ? Padding( - padding: const EdgeInsets.fromLTRB( - 16, - 12, - 16, - 8, - ), - child: Text( - widget.file.caption!, - overflow: TextOverflow.ellipsis, - maxLines: 1, - style: getEnteTextTheme(context) - .mini - .copyWith( - color: textBaseDark, - ), - textAlign: TextAlign.center, - ), - ) - : const SizedBox.shrink(), - ValueListenableBuilder( - valueListenable: showControlsNotifier, - builder: (context, value, _) { - return PreviewStatusWidget( - showControls: value, - file: widget.file, - onStreamChange: widget.onStreamChange, - ); - }, - ), - _SeekBarAndDuration( - controller: widget.controller, - isSeekingNotifier: _isSeekingNotifier, - ), - ], - ), - ), - ), - ), - ), - ], - ), - ); - }, - ); - }, - ); - } -} - -class PlayPauseButtonMediaKit extends StatefulWidget { - final VideoController? controller; - const PlayPauseButtonMediaKit( - this.controller, { - super.key, - }); - - @override - State createState() => _PlayPauseButtonState(); -} - -class _PlayPauseButtonState extends State { - bool _isPlaying = true; - late final StreamSubscription? isPlayingStreamSubscription; - - @override - void initState() { - super.initState(); - - isPlayingStreamSubscription = - widget.controller?.player.stream.playing.listen((isPlaying) { - setState(() { - _isPlaying = isPlaying; - }); - }); - } - - @override - void dispose() { - isPlayingStreamSubscription?.cancel(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () { - if (widget.controller?.player.state.playing ?? false) { - widget.controller?.player.pause(); - } else { - widget.controller?.player.play(); - } - }, - child: Container( - width: 54, - height: 54, - decoration: BoxDecoration( - color: Colors.black.withOpacity(0.3), - shape: BoxShape.circle, - border: Border.all( - color: strokeFaintDark, - width: 1, - ), - ), - child: AnimatedSwitcher( - duration: const Duration(milliseconds: 250), - transitionBuilder: (Widget child, Animation animation) { - return ScaleTransition(scale: animation, child: child); - }, - switchInCurve: Curves.easeInOutQuart, - switchOutCurve: Curves.easeInOutQuart, - child: _isPlaying - ? const Icon( - Icons.pause, - size: 32, - key: ValueKey("pause"), - color: Colors.white, - ) - : const Icon( - Icons.play_arrow, - size: 36, - key: ValueKey("play"), - color: Colors.white, - ), - ), - ), - ); - } -} - -class _SeekBarAndDuration extends StatelessWidget { - final VideoController? controller; - final ValueNotifier isSeekingNotifier; - - const _SeekBarAndDuration({ - required this.controller, - required this.isSeekingNotifier, - }); - - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric( - horizontal: 8, - ), - child: Container( - padding: const EdgeInsets.fromLTRB( - 16, - 4, - 16, - 4, - ), - decoration: BoxDecoration( - color: Colors.black.withOpacity(0.3), - borderRadius: const BorderRadius.all( - Radius.circular(8), - ), - border: Border.all( - color: strokeFaintDark, - width: 1, - ), - ), - child: Row( - children: [ - StreamBuilder( - stream: controller?.player.stream.position, - builder: (context, snapshot) { - if (snapshot.data == null) { - return Text( - "0:00", - style: getEnteTextTheme( - context, - ).mini.copyWith( - color: textBaseDark, - ), - ); - } - return Text( - _secondsToDuration(snapshot.data!.inSeconds), - style: getEnteTextTheme( - context, - ).mini.copyWith( - color: textBaseDark, - ), - ); - }, - ), - Expanded( - child: _SeekBar( - controller!, - isSeekingNotifier, - ), - ), - Text( - _secondsToDuration( - controller!.player.state.duration.inSeconds, - ), - style: getEnteTextTheme( - context, - ).mini.copyWith( - color: textBaseDark, - ), - ), - ], - ), - ), - ); - } - - /// Returns the duration in the format "h:mm:ss" or "m:ss". - String _secondsToDuration(int totalSeconds) { - final hours = totalSeconds ~/ 3600; - final minutes = (totalSeconds % 3600) ~/ 60; - final seconds = totalSeconds % 60; - - if (hours > 0) { - return '${hours.toString().padLeft(1, '0')}:${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}'; - } else { - return '${minutes.toString().padLeft(1, '0')}:${seconds.toString().padLeft(2, '0')}'; - } - } -} - -class _SeekBar extends StatefulWidget { - final VideoController controller; - final ValueNotifier isSeekingNotifier; - const _SeekBar( - this.controller, - this.isSeekingNotifier, - ); - - @override - State<_SeekBar> createState() => _SeekBarState(); -} - -class _SeekBarState extends State<_SeekBar> { - double _sliderValue = 0.0; - late final StreamSubscription _positionStreamSubscription; - final _debouncer = Debouncer( - const Duration(milliseconds: 300), - executionInterval: const Duration(milliseconds: 300), - ); - @override - void initState() { - super.initState(); - _positionStreamSubscription = - widget.controller.player.stream.position.listen((event) { - if (widget.isSeekingNotifier.value) return; - if (mounted) { - setState(() { - _sliderValue = event.inMilliseconds / - widget.controller.player.state.duration.inMilliseconds; - if (_sliderValue.isNaN) { - _sliderValue = 0.0; - } - }); - } - }); - } - - @override - void dispose() { - _positionStreamSubscription.cancel(); - _debouncer.cancelDebounceTimer(); - super.dispose(); - } - - @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, - inactiveTrackColor: fillMutedDark, - thumbColor: backgroundElevatedLight, - overlayColor: fillMutedDark, - ), - child: Slider( - min: 0.0, - max: 1.0, - value: _sliderValue, - onChangeStart: (value) { - if (mounted) { - setState(() { - widget.isSeekingNotifier.value = true; - }); - } - }, - onChanged: (value) { - if (mounted) { - setState(() { - _sliderValue = value; - }); - } - - _debouncer.run(() async { - await widget.controller.player.seek( - Duration( - milliseconds: (value * - widget.controller.player.state.duration.inMilliseconds) - .round(), - ), - ); - }); - }, - divisions: 4500, - onChangeEnd: (value) async { - await widget.controller.player.seek( - Duration( - milliseconds: (value * - widget.controller.player.state.duration.inMilliseconds) - .round(), - ), - ); - if (mounted) { - setState(() { - widget.isSeekingNotifier.value = false; - }); - } - }, - allowedInteraction: SliderInteraction.tapAndSlide, - ), - ); - } -} diff --git a/mobile/lib/ui/viewer/file/video_widget_native.dart b/mobile/lib/ui/viewer/file/video_widget_native.dart index 810bca4dd6..65fb1ecb0f 100644 --- a/mobile/lib/ui/viewer/file/video_widget_native.dart +++ b/mobile/lib/ui/viewer/file/video_widget_native.dart @@ -10,25 +10,29 @@ import "package:photos/core/constants.dart"; import "package:photos/core/event_bus.dart"; import "package:photos/events/guest_view_event.dart"; import "package:photos/events/pause_video_event.dart"; +import "package:photos/events/seekbar_triggered_event.dart"; +import "package:photos/events/stream_switched_event.dart"; import "package:photos/events/use_media_kit_for_video.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/models/preview/playlist_data.dart"; import "package:photos/service_locator.dart"; import "package:photos/services/files_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"; 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/utils/debouncer.dart"; import "package:photos/utils/dialog_util.dart"; import "package:photos/utils/exif_util.dart"; import "package:photos/utils/file_util.dart"; -import "package:photos/utils/toast_util.dart"; +import "package:photos/utils/standalone/date_time.dart"; +import "package:photos/utils/standalone/debouncer.dart"; import "package:visibility_detector/visibility_detector.dart"; class VideoWidgetNative extends StatefulWidget { @@ -37,6 +41,9 @@ class VideoWidgetNative extends StatefulWidget { final Function(bool)? playbackCallback; final bool isFromMemories; final void Function()? onStreamChange; + final PlaylistData? playlistData; + final bool selectedPreview; + const VideoWidgetNative( this.file, { this.tagPrefix, @@ -44,6 +51,8 @@ class VideoWidgetNative extends StatefulWidget { this.isFromMemories = false, required this.onStreamChange, super.key, + this.playlistData, + required this.selectedPreview, }); @override @@ -69,6 +78,9 @@ class _VideoWidgetNativeState extends State final _isSeeking = ValueNotifier(false); final _debouncer = Debouncer(const Duration(milliseconds: 2000)); final _elTooltipController = ElTooltipController(); + StreamSubscription? _subscription; + StreamSubscription? _streamSwitchedSubscription; + int position = 0; @override void initState() { @@ -77,32 +89,11 @@ class _VideoWidgetNativeState extends State ); super.initState(); WidgetsBinding.instance.addObserver(this); - if (widget.file.isRemoteFile) { - _loadNetworkVideo(); - _setFileSizeIfNull(); - } else if (widget.file.isSharedMediaToAppSandbox) { - final localFile = File(getSharedMediaFilePath(widget.file)); - if (localFile.existsSync()) { - _setFilePathForNativePlayer(localFile.path); - } else if (widget.file.uploadedFileID != null) { - _loadNetworkVideo(); - } + + if (widget.selectedPreview) { + loadPreview(); } else { - widget.file.getAsset.then((asset) async { - if (asset == null || !(await asset.exists)) { - if (widget.file.uploadedFileID != null) { - _loadNetworkVideo(); - } - } else { - // ignore: unawaited_futures - getFile(widget.file, isOrigin: true).then((file) { - _setFilePathForNativePlayer(file!.path); - if (Platform.isIOS) { - _shouldClearCache = true; - } - }); - } - }); + loadOriginal(); } pauseVideoSubscription = Bus.instance.on().listen((event) { @@ -114,12 +105,71 @@ class _VideoWidgetNativeState extends State _isGuestView = event.isGuestView; }); }); + _streamSwitchedSubscription = + Bus.instance.on().listen((event) { + if (event.type != PlayerType.nativeVideoPlayer) return; + if (event.selectedPreview) { + loadPreview(update: true); + } else { + loadOriginal(update: true); + } + }); + } + + Future setVideoSource() async { + final videoSource = VideoSource( + path: _filePath!, + type: VideoSourceType.file, + ); + await _controller?.loadVideo(videoSource); + await _controller?.play(); + + Bus.instance.fire(SeekbarTriggeredEvent(position: 0)); + } + + void loadPreview({bool update = false}) async { + _setFilePathForNativePlayer(widget.playlistData!.preview.path, update); + + await setVideoSource(); + } + + void loadOriginal({bool update = false}) async { + if (widget.file.isRemoteFile) { + _loadNetworkVideo(update); + _setFileSizeIfNull(); + } else if (widget.file.isSharedMediaToAppSandbox) { + final localFile = File(getSharedMediaFilePath(widget.file)); + if (localFile.existsSync()) { + _setFilePathForNativePlayer(localFile.path, update); + } else if (widget.file.uploadedFileID != null) { + _loadNetworkVideo(update); + } + } else { + await widget.file.getAsset.then((asset) async { + if (asset == null || !(await asset.exists)) { + if (widget.file.uploadedFileID != null) { + _loadNetworkVideo(update); + } + } else { + // ignore: unawaited_futures + getFile(widget.file, isOrigin: true).then((file) { + _setFilePathForNativePlayer(file!.path, update); + if (Platform.isIOS) { + _shouldClearCache = true; + } + }); + } + }); + } + if (update) { + await setVideoSource(); + } } @override void didChangeAppLifecycleState(AppLifecycleState state) { if (state != AppLifecycleState.resumed) { - if (_controller?.playbackInfo?.status == PlaybackStatus.playing) { + if (_controller?.playbackStatus == PlaybackStatus.playing) { _controller?.pause(); } } @@ -127,6 +177,9 @@ class _VideoWidgetNativeState extends State @override void dispose() { + _subscription?.cancel(); + _controller?.dispose(); + //https://github.com/fluttercandies/flutter_photo_manager/blob/8afba2745ebaac6af8af75de9cbded9157bc2690/README.md#clear-caches if (_shouldClearCache) { _logger.info("Clearing cache"); @@ -142,16 +195,12 @@ class _VideoWidgetNativeState extends State ); } } + _streamSwitchedSubscription?.cancel(); _guestViewEventSubscription.cancel(); pauseVideoSubscription.cancel(); removeCallBack(widget.file); _progressNotifier.dispose(); WidgetsBinding.instance.removeObserver(this); - _controller?.onPlaybackEnded.removeListener(_onPlaybackEnded); - _controller?.onPlaybackReady.removeListener(_onPlaybackReady); - _controller?.onError.removeListener(_onError); - _controller?.onPlaybackStatusChanged - .removeListener(_onPlaybackStatusChanged); _isPlaybackReady.dispose(); _showControls.dispose(); _isSeeking.removeListener(_seekListener); @@ -210,10 +259,10 @@ class _VideoWidgetNativeState extends State behavior: HitTestBehavior.opaque, onTap: () { _showControls.value = !_showControls.value; - _elTooltipController.hide(); if (widget.playbackCallback != null) { widget.playbackCallback!(!_showControls.value); } + _elTooltipController.hide(); }, child: Container( constraints: const BoxConstraints.expand(), @@ -279,22 +328,24 @@ class _VideoWidgetNativeState extends State child: Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ + _VideoDescriptionAndSwitchToMediaKitButton( + file: widget.file, + showControls: _showControls, + elTooltipController: _elTooltipController, + controller: _controller, + selectedPreview: widget.selectedPreview, + ), ValueListenableBuilder( valueListenable: _showControls, builder: (context, value, _) { return PreviewStatusWidget( showControls: value, file: widget.file, + isPreviewPlayer: widget.selectedPreview, onStreamChange: widget.onStreamChange, ); }, ), - _VideoDescriptionAndSwitchToMediaKitButton( - file: widget.file, - showControls: _showControls, - elTooltipController: _elTooltipController, - controller: _controller, - ), ValueListenableBuilder( valueListenable: _isPlaybackReady, builder: @@ -305,6 +356,7 @@ class _VideoWidgetNativeState extends State duration: duration, showControls: _showControls, isSeeking: _isSeeking, + position: position, ) : const SizedBox(); }, @@ -331,17 +383,11 @@ class _VideoWidgetNativeState extends State ); _controller = controller; - controller.onError.addListener(_onError); - controller.onPlaybackEnded.addListener(_onPlaybackEnded); - controller.onPlaybackReady.addListener(_onPlaybackReady); - controller.onPlaybackStatusChanged.addListener(_onPlaybackStatusChanged); + _subscription = controller.events.listen(_listen); + _isSeeking.addListener(_seekListener); - final videoSource = await VideoSource.init( - path: _filePath!, - type: VideoSourceType.file, - ); - await controller.loadVideoSource(videoSource); + await setVideoSource(); } catch (e) { _logger.severe( "Error initializing native video player controller for file gen id: ${widget.file.generatedID}", @@ -350,13 +396,34 @@ class _VideoWidgetNativeState extends State } } + void _listen(PlaybackEvent event) { + switch (event) { + case PlaybackStatusChangedEvent(): + _onPlaybackStatusChanged(); + case PlaybackReadyEvent(): + _onPlaybackReady(); + break; + case PlaybackPositionChangedEvent(): + position = event.positionInMilliseconds; + setState(() {}); + break; + case PlaybackEndedEvent(): + _onPlaybackEnded(); + break; + case PlaybackErrorEvent(): + _onError(event.errorMessage); + break; + default: + } + } + void _seekListener() { if (!_isSeeking.value && - _controller?.playbackInfo?.status == PlaybackStatus.playing) { + _controller?.playbackStatus == PlaybackStatus.playing) { _debouncer.run(() async { if (mounted) { if (_isSeeking.value || - _controller?.playbackInfo?.status != PlaybackStatus.playing) { + _controller?.playbackStatus != PlaybackStatus.playing) { return; } _showControls.value = false; @@ -369,15 +436,20 @@ class _VideoWidgetNativeState extends State } void _onPlaybackStatusChanged() { - if (_isSeeking.value || _controller?.playbackInfo?.positionFraction == 1) { + final duration = widget.file.duration != null + ? widget.file.duration! * 1000 + : _controller?.videoInfo?.durationInMilliseconds; + + if (_isSeeking.value || + _controller?.playbackPosition.inMilliseconds == duration) { return; } - if (_controller!.playbackInfo?.status == PlaybackStatus.playing) { + if (_controller!.playbackStatus == PlaybackStatus.playing) { if (mounted) { _debouncer.run(() async { if (mounted) { if (_isSeeking.value || - _controller!.playbackInfo?.status != PlaybackStatus.playing) { + _controller!.playbackStatus != PlaybackStatus.playing) { return; } _showControls.value = false; @@ -394,28 +466,31 @@ class _VideoWidgetNativeState extends State } } - void _onError() { + void _onError(String errorMessage) { //This doesn't work all the time _logger.severe( "Error in native video player controller for file gen id: ${widget.file.generatedID}", ); - _logger.severe(_controller!.onError.value); + _logger.severe(errorMessage); Bus.instance.fire(UseMediaKitForVideo()); } Future _onPlaybackReady() async { + if (_isPlaybackReady.value) return; await _controller!.play(); unawaited(_controller!.setVolume(1)); _isPlaybackReady.value = true; } - void _onPlaybackEnded() { + void _onPlaybackEnded() async { + await _controller?.stop(); if (localSettings.shouldLoopVideo()) { - _controller?.play(); + Bus.instance.fire(SeekbarTriggeredEvent(position: 0)); + await _controller?.play(); } } - void _loadNetworkVideo() { + void _loadNetworkVideo(bool update) { getFileFromServer( widget.file, progressCallback: (count, total) { @@ -431,7 +506,7 @@ class _VideoWidgetNativeState extends State }, ).then((file) { if (file != null) { - _setFilePathForNativePlayer(file.path); + _setFilePathForNativePlayer(file.path, update); } }).onError((error, stackTrace) { showErrorDialog( @@ -515,18 +590,32 @@ class _VideoWidgetNativeState extends State ); } - void _setFilePathForNativePlayer(String url) { - if (mounted) { - setState(() { - _filePath = url; - }); - _setAspectRatioFromVideoProps().then((_) { - setState(() {}); - }); + void _setFilePathForNativePlayer(String url, bool update) { + if (!mounted) return; + setState(() { + _filePath = url; + }); + _setAspectRatioFromVideoProps().then((_) { + setState(() {}); + }); + + if (update) { + setVideoSource(); } } Future _setAspectRatioFromVideoProps() async { + if (aspectRatio != null && duration != null) return; + + 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!); + } + _logger.info("Getting aspect ratio from preview video"); + return; + } final videoProps = await getVideoPropsAsync(File(_filePath!)); if (videoProps != null) { duration = videoProps.propData?["duration"]; @@ -554,12 +643,14 @@ class _SeekBarAndDuration extends StatelessWidget { final String? duration; final ValueNotifier showControls; final ValueNotifier isSeeking; + final int position; const _SeekBarAndDuration({ required this.controller, required this.duration, required this.showControls, required this.isSeeking, + required this.position, }); @override @@ -607,40 +698,25 @@ class _SeekBarAndDuration extends StatelessWidget { seconds: 5, ), curve: Curves.easeInOut, - child: ValueListenableBuilder( - valueListenable: controller!.onPlaybackPositionChanged, - builder: ( - BuildContext context, - int value, - _, - ) { - return Text( - _secondsToDuration( - value, + child: Text( + secondsToDuration(position ~/ 1000), + style: getEnteTextTheme( + context, + ).mini.copyWith( + color: textBaseDark, ), - style: getEnteTextTheme( - context, - ).mini.copyWith( - color: textBaseDark, - ), - ); - }, ), ), Expanded( child: SeekBar( controller!, - _durationToSeconds( - duration, - ), + durationToSeconds(duration), isSeeking, ), ), Text( duration ?? "0:00", - style: getEnteTextTheme( - context, - ).mini.copyWith( + style: getEnteTextTheme(context).mini.copyWith( color: textBaseDark, ), ), @@ -653,43 +729,6 @@ class _SeekBarAndDuration extends StatelessWidget { }, ); } - - /// Returns the duration in the format "h:mm:ss" or "m:ss". - String _secondsToDuration(int totalSeconds) { - final hours = totalSeconds ~/ 3600; - final minutes = (totalSeconds % 3600) ~/ 60; - final seconds = totalSeconds % 60; - - if (hours > 0) { - return '${hours.toString().padLeft(1, '0')}:${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}'; - } else { - return '${minutes.toString().padLeft(1, '0')}:${seconds.toString().padLeft(2, '0')}'; - } - } - - /// Returns the duration in seconds from the format "h:mm:ss" or "m:ss". - int? _durationToSeconds(String? duration) { - if (duration == null) { - return null; - } - final parts = duration.split(':'); - int seconds = 0; - - if (parts.length == 3) { - // Format: "h:mm:ss" - seconds += int.parse(parts[0]) * 3600; // Hours to seconds - seconds += int.parse(parts[1]) * 60; // Minutes to seconds - seconds += int.parse(parts[2]); // Seconds - } else if (parts.length == 2) { - // Format: "m:ss" - seconds += int.parse(parts[0]) * 60; // Minutes to seconds - seconds += int.parse(parts[1]); // Seconds - } else { - throw FormatException('Invalid duration format: $duration'); - } - - return seconds; - } } class _VideoDescriptionAndSwitchToMediaKitButton extends StatelessWidget { @@ -697,12 +736,14 @@ class _VideoDescriptionAndSwitchToMediaKitButton extends StatelessWidget { final ValueNotifier showControls; final ElTooltipController elTooltipController; final NativeVideoPlayerController? controller; + final bool selectedPreview; const _VideoDescriptionAndSwitchToMediaKitButton({ required this.file, required this.showControls, required this.elTooltipController, required this.controller, + required this.selectedPreview, }); @override @@ -738,7 +779,7 @@ class _VideoDescriptionAndSwitchToMediaKitButton extends StatelessWidget { ), ) : const SizedBox.shrink(), - Platform.isAndroid + Platform.isAndroid && !selectedPreview ? ValueListenableBuilder( valueListenable: showControls, builder: (context, value, _) { 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 74be3d954e..65f05dbdd0 100644 --- a/mobile/lib/ui/viewer/file/zoomable_live_image_new.dart +++ b/mobile/lib/ui/viewer/file/zoomable_live_image_new.dart @@ -6,6 +6,7 @@ import 'package:logging/logging.dart'; import "package:media_kit/media_kit.dart"; import "package:media_kit_video/media_kit_video.dart"; import 'package:motion_photos/motion_photos.dart'; +import "package:path_provider/path_provider.dart"; import "package:photos/core/event_bus.dart"; import "package:photos/events/guest_view_event.dart"; import "package:photos/generated/l10n.dart"; @@ -13,10 +14,9 @@ import "package:photos/models/file/extensions/file_props.dart"; import 'package:photos/models/file/file.dart'; import "package:photos/models/metadata/file_magic.dart"; import "package:photos/services/file_magic_service.dart"; -import "package:photos/services/local_file_update_service.dart"; +import 'package:photos/ui/notification/toast.dart'; import 'package:photos/ui/viewer/file/zoomable_image.dart'; import 'package:photos/utils/file_util.dart'; -import 'package:photos/utils/toast_util.dart'; class ZoomableLiveImageNew extends StatefulWidget { final EnteFile enteFile; @@ -57,9 +57,6 @@ class _ZoomableLiveImageNewState extends State _logger.info( 'initState for ${_enteFile.generatedID} with tag ${_enteFile.tag} and name ${_enteFile.displayName}', ); - if (_enteFile.isLivePhoto && _enteFile.isUploaded) { - LocalFileUpdateService.instance.checkLivePhoto(_enteFile).ignore(); - } _guestViewEventSubscription = Bus.instance.on().listen((event) { setState(() { @@ -202,6 +199,7 @@ class _ZoomableLiveImageNewState extends State ).ignore(); } return motionPhoto.getMotionVideoFile( + await getTemporaryDirectory(), index: index, ); } else if (_enteFile.isMotionPhoto && _enteFile.canEditMetaInfo) { diff --git a/mobile/lib/ui/viewer/file_details/backed_up_time_item_widget.dart b/mobile/lib/ui/viewer/file_details/backed_up_time_item_widget.dart index 28f9cff870..813b32ced9 100644 --- a/mobile/lib/ui/viewer/file_details/backed_up_time_item_widget.dart +++ b/mobile/lib/ui/viewer/file_details/backed_up_time_item_widget.dart @@ -3,7 +3,7 @@ import "package:intl/intl.dart"; import 'package:photos/models/file/file.dart'; import "package:photos/theme/ente_theme.dart"; import "package:photos/ui/components/info_item_widget.dart"; -import "package:photos/utils/date_time_util.dart"; +import "package:photos/utils/standalone/date_time.dart"; class BackedUpTimeItemWidget extends StatelessWidget { final EnteFile file; diff --git a/mobile/lib/ui/viewer/file_details/creation_time_item_widget.dart b/mobile/lib/ui/viewer/file_details/creation_time_item_widget.dart index 90b6ab9d58..e86bbafc18 100644 --- a/mobile/lib/ui/viewer/file_details/creation_time_item_widget.dart +++ b/mobile/lib/ui/viewer/file_details/creation_time_item_widget.dart @@ -6,8 +6,8 @@ import "package:photos/l10n/l10n.dart"; import 'package:photos/models/file/file.dart'; import "package:photos/theme/ente_theme.dart"; import "package:photos/ui/components/info_item_widget.dart"; -import "package:photos/utils/date_time_util.dart"; import "package:photos/utils/magic_util.dart"; +import "package:photos/utils/standalone/date_time.dart"; class CreationTimeItem extends StatefulWidget { final EnteFile file; @@ -21,15 +21,15 @@ class CreationTimeItem extends StatefulWidget { class _CreationTimeItemState extends State { @override Widget build(BuildContext context) { - final dateTime = - DateTime.fromMicrosecondsSinceEpoch(widget.file.creationTime!); + final dateTime = DateTime.fromMicrosecondsSinceEpoch( + widget.file.creationTime!, + isUtc: true, + ).toLocal(); return InfoItemWidget( key: const ValueKey("Creation time"), leadingIcon: Icons.calendar_today_outlined, title: DateFormat.yMMMEd(Localizations.localeOf(context).languageCode) - .format( - DateTime.fromMicrosecondsSinceEpoch(widget.file.creationTime!), - ), + .format(dateTime), subtitleSection: Future.value([ Text( getTimeIn12hrFormat(dateTime) + " " + dateTime.timeZoneName, diff --git a/mobile/lib/ui/viewer/file_details/exif_item_widgets.dart b/mobile/lib/ui/viewer/file_details/exif_item_widgets.dart index 25b1e5e11e..dead77781b 100644 --- a/mobile/lib/ui/viewer/file_details/exif_item_widgets.dart +++ b/mobile/lib/ui/viewer/file_details/exif_item_widgets.dart @@ -5,8 +5,8 @@ import 'package:photos/models/file/file.dart'; import "package:photos/theme/colors.dart"; import "package:photos/theme/ente_theme.dart"; import "package:photos/ui/components/info_item_widget.dart"; +import "package:photos/ui/notification/toast.dart"; import "package:photos/ui/viewer/file/exif_info_dialog.dart"; -import "package:photos/utils/toast_util.dart"; class BasicExifItemWidget extends StatelessWidget { final Map exifData; diff --git a/mobile/lib/ui/viewer/file_details/face_widget.dart b/mobile/lib/ui/viewer/file_details/face_widget.dart index 69d22bcfd5..b841b872c0 100644 --- a/mobile/lib/ui/viewer/file_details/face_widget.dart +++ b/mobile/lib/ui/viewer/file_details/face_widget.dart @@ -18,11 +18,11 @@ import "package:photos/services/machine_learning/face_ml/feedback/cluster_feedba 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"; -import "package:photos/utils/toast_util.dart"; class FaceWidget extends StatefulWidget { final EnteFile file; diff --git a/mobile/lib/ui/viewer/file_details/favorite_widget.dart b/mobile/lib/ui/viewer/file_details/favorite_widget.dart index 3371b14421..11c588b933 100644 --- a/mobile/lib/ui/viewer/file_details/favorite_widget.dart +++ b/mobile/lib/ui/viewer/file_details/favorite_widget.dart @@ -8,7 +8,7 @@ import "package:photos/generated/l10n.dart"; import 'package:photos/models/file/file.dart'; import "package:photos/services/favorites_service.dart"; import "package:photos/ui/common/loading_widget.dart"; -import "package:photos/utils/toast_util.dart"; +import "package:photos/ui/notification/toast.dart"; class FavoriteWidget extends StatefulWidget { final EnteFile file; diff --git a/mobile/lib/ui/viewer/file_details/file_properties_item_widget.dart b/mobile/lib/ui/viewer/file_details/file_properties_item_widget.dart index 0640f92eeb..2cedc1da2b 100644 --- a/mobile/lib/ui/viewer/file_details/file_properties_item_widget.dart +++ b/mobile/lib/ui/viewer/file_details/file_properties_item_widget.dart @@ -4,10 +4,10 @@ import 'package:photos/models/file/file.dart'; import 'package:photos/models/file/file_type.dart'; import "package:photos/theme/ente_theme.dart"; import "package:photos/ui/components/info_item_widget.dart"; -import "package:photos/utils/data_util.dart"; -import "package:photos/utils/date_time_util.dart"; import "package:photos/utils/file_util.dart"; import "package:photos/utils/magic_util.dart"; +import "package:photos/utils/standalone/data.dart"; +import "package:photos/utils/standalone/date_time.dart"; class FilePropertiesItemWidget extends StatefulWidget { final EnteFile file; diff --git a/mobile/lib/ui/viewer/file_details/location_tags_widget.dart b/mobile/lib/ui/viewer/file_details/location_tags_widget.dart index fd85ef491b..b526643f1a 100644 --- a/mobile/lib/ui/viewer/file_details/location_tags_widget.dart +++ b/mobile/lib/ui/viewer/file_details/location_tags_widget.dart @@ -11,7 +11,6 @@ import "package:photos/generated/l10n.dart"; import "package:photos/models/file/file.dart"; import "package:photos/service_locator.dart"; import "package:photos/services/search_service.dart"; -import "package:photos/services/user_remote_flag_service.dart"; import "package:photos/states/location_screen_state.dart"; import "package:photos/theme/ente_theme.dart"; import "package:photos/ui/components/buttons/chip_button_widget.dart"; @@ -183,8 +182,7 @@ class _InfoMapState extends State { @override void initState() { super.initState(); - _hasEnabledMap = userRemoteFlagService - .getCachedBoolValue(UserRemoteFlagService.mapEnabled); + _hasEnabledMap = flagService.mapEnabled; _fileLat = widget.file.location!.latitude!; _fileLng = widget.file.location!.longitude!; diff --git a/mobile/lib/ui/viewer/file_details/objects_item_widget.dart b/mobile/lib/ui/viewer/file_details/objects_item_widget.dart deleted file mode 100644 index c02576c116..0000000000 --- a/mobile/lib/ui/viewer/file_details/objects_item_widget.dart +++ /dev/null @@ -1,66 +0,0 @@ -import "package:flutter/foundation.dart"; -import "package:flutter/material.dart"; -import "package:logging/logging.dart"; -import "package:photos/generated/l10n.dart"; -import 'package:photos/models/file/file.dart'; -import "package:photos/ui/components/buttons/chip_button_widget.dart"; -import "package:photos/ui/components/info_item_widget.dart"; - -class ObjectsItemWidget extends StatelessWidget { - final EnteFile file; - const ObjectsItemWidget(this.file, {super.key}); - - @override - Widget build(BuildContext context) { - return InfoItemWidget( - key: const ValueKey("Objects"), - leadingIcon: Icons.image_search_outlined, - subtitleSection: _objectTags(context, file), - hasChipButtons: true, - ); - } - - Future> _objectTags( - BuildContext context, - EnteFile file, - ) async { - try { - final chipButtons = []; - var objectTags = {}; - - // final thumbnail = await getThumbnail(file); - // if (thumbnail != null) { - // objectTags = await ObjectDetectionService.instance.predict(thumbnail); - // } - if (objectTags.isEmpty) { - return [ - ChipButtonWidget( - S.of(context).noResults, - noChips: true, - ), - ]; - } - // sort by values - objectTags = Map.fromEntries( - objectTags.entries.toList() - ..sort((e1, e2) => e2.value.compareTo(e1.value)), - ); - - for (MapEntry entry in objectTags.entries) { - chipButtons.add( - ChipButtonWidget( - entry.key + - (kDebugMode - ? "-" + (entry.value * 100).round().toString() - : ""), - ), - ); - } - - return chipButtons; - } catch (e, s) { - Logger("ObjctsItemWidget").info(e, s); - return []; - } - } -} diff --git a/mobile/lib/ui/viewer/file_details/preview_properties_item_widget.dart b/mobile/lib/ui/viewer/file_details/preview_properties_item_widget.dart index aec1b9fa3d..6e5d15d7d5 100644 --- a/mobile/lib/ui/viewer/file_details/preview_properties_item_widget.dart +++ b/mobile/lib/ui/viewer/file_details/preview_properties_item_widget.dart @@ -6,7 +6,7 @@ import "package:photos/models/file/file_type.dart"; import "package:photos/services/preview_video_store.dart"; import "package:photos/theme/ente_theme.dart"; import "package:photos/ui/components/info_item_widget.dart"; -import "package:photos/utils/data_util.dart"; +import "package:photos/utils/standalone/data.dart"; class PreviewPropertiesItemWidget extends StatefulWidget { final EnteFile file; diff --git a/mobile/lib/ui/viewer/file_details/upload_icon_widget.dart b/mobile/lib/ui/viewer/file_details/upload_icon_widget.dart index 5c346d3dc1..65ade712bd 100644 --- a/mobile/lib/ui/viewer/file_details/upload_icon_widget.dart +++ b/mobile/lib/ui/viewer/file_details/upload_icon_widget.dart @@ -13,10 +13,10 @@ import "package:photos/models/ignored_file.dart"; import "package:photos/services/collections_service.dart"; import "package:photos/services/hidden_service.dart"; import "package:photos/services/ignored_files_service.dart"; -import "package:photos/services/remote_sync_service.dart"; -import "package:photos/services/sync_service.dart"; +import "package:photos/services/sync/remote_sync_service.dart"; +import "package:photos/services/sync/sync_service.dart"; import "package:photos/ui/common/loading_widget.dart"; -import "package:photos/utils/toast_util.dart"; +import "package:photos/ui/notification/toast.dart"; class UploadIconWidget extends StatefulWidget { final EnteFile file; diff --git a/mobile/lib/ui/viewer/file_details/video_exif_item.dart b/mobile/lib/ui/viewer/file_details/video_exif_item.dart index a3d51d6c9a..d4d9cc205f 100644 --- a/mobile/lib/ui/viewer/file_details/video_exif_item.dart +++ b/mobile/lib/ui/viewer/file_details/video_exif_item.dart @@ -6,8 +6,8 @@ import 'package:photos/models/file/file.dart'; import "package:photos/theme/colors.dart"; import "package:photos/theme/ente_theme.dart"; import "package:photos/ui/components/info_item_widget.dart"; +import "package:photos/ui/notification/toast.dart"; import "package:photos/ui/viewer/file/video_exif_dialog.dart"; -import "package:photos/utils/toast_util.dart"; class VideoExifRowItem extends StatefulWidget { final EnteFile file; diff --git a/mobile/lib/ui/viewer/gallery/collect_photos_bottom_buttons.dart b/mobile/lib/ui/viewer/gallery/collect_photos_bottom_buttons.dart index 5cb6f6c876..df90b8a10c 100644 --- a/mobile/lib/ui/viewer/gallery/collect_photos_bottom_buttons.dart +++ b/mobile/lib/ui/viewer/gallery/collect_photos_bottom_buttons.dart @@ -48,7 +48,7 @@ class _EmptyAlbumStateNewState extends State { final String collectionKey = Base58Encode( CollectionsService.instance.getCollectionKey(widget.c.id), ); - final String url = "${widget.c.publicURLs!.first!.url}#$collectionKey"; + final String url = "${widget.c.publicURLs.first.url}#$collectionKey"; await shareAlbumLinkWithPlaceholder( context, widget.c, @@ -68,7 +68,11 @@ class _EmptyAlbumStateNewState extends State { if (hasUrl) { await _shareAlbumUrl(); } else { - final bool result = await collectionActions.enableUrl(context, widget.c); + final bool result = await collectionActions.enableUrl( + context, + widget.c, + enableCollect: true, + ); if (result) { await _shareAlbumUrl(); } else { diff --git a/mobile/lib/ui/viewer/gallery/component/grid/lazy_grid_view.dart b/mobile/lib/ui/viewer/gallery/component/grid/lazy_grid_view.dart index 248b4c5664..54323fbfd4 100644 --- a/mobile/lib/ui/viewer/gallery/component/grid/lazy_grid_view.dart +++ b/mobile/lib/ui/viewer/gallery/component/grid/lazy_grid_view.dart @@ -30,8 +30,8 @@ class LazyGridView extends StatefulWidget { this.shouldRecycle, this.photoGridSize, { this.limitSelectionToOne = false, - Key? key, - }) : super(key: key); + super.key, + }); @override State createState() => _LazyGridViewState(); diff --git a/mobile/lib/ui/viewer/gallery/component/grid/place_holder_grid_view_widget.dart b/mobile/lib/ui/viewer/gallery/component/grid/place_holder_grid_view_widget.dart index 9cb2984a28..c29d817ab3 100644 --- a/mobile/lib/ui/viewer/gallery/component/grid/place_holder_grid_view_widget.dart +++ b/mobile/lib/ui/viewer/gallery/component/grid/place_holder_grid_view_widget.dart @@ -8,8 +8,8 @@ class PlaceHolderGridViewWidget extends StatelessWidget { const PlaceHolderGridViewWidget( this.count, this.columns, { - Key? key, - }) : super(key: key); + super.key, + }); final int count, columns; diff --git a/mobile/lib/ui/viewer/gallery/component/group/lazy_group_gallery.dart b/mobile/lib/ui/viewer/gallery/component/group/lazy_group_gallery.dart index 299d84d7d3..19ac9a0b7c 100644 --- a/mobile/lib/ui/viewer/gallery/component/group/lazy_group_gallery.dart +++ b/mobile/lib/ui/viewer/gallery/component/group/lazy_group_gallery.dart @@ -43,8 +43,8 @@ class LazyGroupGallery extends StatefulWidget { this.logTag = "", this.photoGridSize = photoGridSizeDefault, this.limitSelectionToOne = false, - Key? key, - }) : super(key: key); + super.key, + }); @override State createState() => _LazyGroupGalleryState(); diff --git a/mobile/lib/ui/viewer/gallery/component/group/type.dart b/mobile/lib/ui/viewer/gallery/component/group/type.dart index 19b1224de9..079ddb1e2e 100644 --- a/mobile/lib/ui/viewer/gallery/component/group/type.dart +++ b/mobile/lib/ui/viewer/gallery/component/group/type.dart @@ -3,7 +3,7 @@ import "package:intl/intl.dart"; import "package:photos/core/constants.dart"; import "package:photos/generated/l10n.dart"; import "package:photos/models/file/file.dart"; -import "package:photos/utils/date_time_util.dart"; +import "package:photos/utils/standalone/date_time.dart"; enum GroupType { day, diff --git a/mobile/lib/ui/viewer/gallery/component/multiple_groups_gallery_view.dart b/mobile/lib/ui/viewer/gallery/component/multiple_groups_gallery_view.dart index d4ecf73520..de38d22fda 100644 --- a/mobile/lib/ui/viewer/gallery/component/multiple_groups_gallery_view.dart +++ b/mobile/lib/ui/viewer/gallery/component/multiple_groups_gallery_view.dart @@ -13,7 +13,7 @@ import 'package:photos/ui/viewer/gallery/component/group/lazy_group_gallery.dart import "package:photos/ui/viewer/gallery/component/group/type.dart"; import "package:photos/ui/viewer/gallery/gallery.dart"; import "package:photos/ui/viewer/gallery/state/gallery_context_state.dart"; -import "package:photos/utils/data_util.dart"; +import "package:photos/utils/standalone/data.dart"; import "package:scrollable_positioned_list/scrollable_positioned_list.dart"; /* diff --git a/mobile/lib/ui/viewer/gallery/device_folder_page.dart b/mobile/lib/ui/viewer/gallery/device_folder_page.dart index 0fbe5785db..0eae4e1de1 100644 --- a/mobile/lib/ui/viewer/gallery/device_folder_page.dart +++ b/mobile/lib/ui/viewer/gallery/device_folder_page.dart @@ -13,7 +13,7 @@ import 'package:photos/models/file/file.dart'; import 'package:photos/models/gallery_type.dart'; import 'package:photos/models/selected_files.dart'; import 'package:photos/services/ignored_files_service.dart'; -import 'package:photos/services/remote_sync_service.dart'; +import 'package:photos/services/sync/remote_sync_service.dart'; import 'package:photos/theme/ente_theme.dart'; import 'package:photos/ui/components/captioned_text_widget.dart'; import 'package:photos/ui/components/menu_item_widget/menu_item_widget.dart'; diff --git a/mobile/lib/ui/viewer/gallery/empty_hidden_widget.dart b/mobile/lib/ui/viewer/gallery/empty_hidden_widget.dart index 0791ffe734..250d69af0c 100644 --- a/mobile/lib/ui/viewer/gallery/empty_hidden_widget.dart +++ b/mobile/lib/ui/viewer/gallery/empty_hidden_widget.dart @@ -5,7 +5,7 @@ import 'package:photos/theme/ente_theme.dart'; import 'package:photos/theme/text_style.dart'; class EmptyHiddenWidget extends StatelessWidget { - const EmptyHiddenWidget({Key? key}) : super(key: key); + const EmptyHiddenWidget({super.key}); @override Widget build(BuildContext context) { @@ -88,8 +88,8 @@ class EmptyHiddenTextWidget extends StatelessWidget { const EmptyHiddenTextWidget( this.text, { - Key? key, - }) : super(key: key); + super.key, + }); @override Widget build(BuildContext context) { diff --git a/mobile/lib/ui/viewer/gallery/empty_state.dart b/mobile/lib/ui/viewer/gallery/empty_state.dart index 80f95000a4..dde2651471 100644 --- a/mobile/lib/ui/viewer/gallery/empty_state.dart +++ b/mobile/lib/ui/viewer/gallery/empty_state.dart @@ -5,7 +5,7 @@ import "package:photos/generated/l10n.dart"; class EmptyState extends StatelessWidget { final String? text; - const EmptyState({Key? key, this.text}) : super(key: key); + const EmptyState({super.key, this.text}); @override Widget build(BuildContext context) { diff --git a/mobile/lib/ui/viewer/gallery/gallery.dart b/mobile/lib/ui/viewer/gallery/gallery.dart index 8eea50057c..f48209669e 100644 --- a/mobile/lib/ui/viewer/gallery/gallery.dart +++ b/mobile/lib/ui/viewer/gallery/gallery.dart @@ -18,8 +18,9 @@ import 'package:photos/ui/viewer/gallery/empty_state.dart'; import "package:photos/ui/viewer/gallery/state/gallery_context_state.dart"; import "package:photos/ui/viewer/gallery/state/gallery_files_inherited_widget.dart"; import "package:photos/ui/viewer/gallery/state/inherited_search_filter_data.dart"; -import "package:photos/utils/debouncer.dart"; import "package:photos/utils/hierarchical_search_util.dart"; +import "package:photos/utils/standalone/date_time.dart"; +import "package:photos/utils/standalone/debouncer.dart"; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; typedef GalleryLoader = Future Function( @@ -131,19 +132,57 @@ class GalleryState extends State { _itemScroller = ItemScrollController(); if (widget.reloadEvent != null) { _reloadEventSubscription = widget.reloadEvent!.listen((event) async { + bool shouldReloadFromDB = true; + if (event.source == 'uploadCompleted') { + final Map genIDToUploadedFiles = {}; + for (int i = 0; i < event.updatedFiles.length; i++) { + if (event.updatedFiles[i].generatedID == null) { + shouldReloadFromDB = true; + break; + } + genIDToUploadedFiles[event.updatedFiles[i].generatedID!] = + event.updatedFiles[i]; + } + for (int i = 0; i < _allGalleryFiles.length; i++) { + final file = _allGalleryFiles[i]; + if (file.generatedID == null) { + continue; + } + final updateFile = genIDToUploadedFiles[file.generatedID!]; + if (updateFile != null && + updateFile.localID == file.localID && + areFromSameDay( + updateFile.creationTime ?? 0, + file.creationTime ?? 0, + )) { + _allGalleryFiles[i] = updateFile; + genIDToUploadedFiles.remove(file.generatedID!); + } + } + shouldReloadFromDB = genIDToUploadedFiles.isNotEmpty; + } + if (!shouldReloadFromDB) { + final bool hasCalledSetState = _onFilesLoaded(_allGalleryFiles); + _logger.info( + 'Skip softRefresh from DB, processed updated in memory with setStateReload $hasCalledSetState', + ); + return; + } + _debouncer.run(() async { // In soft refresh, setState is called for entire gallery only when // number of child change _logger.finest("Soft refresh all files on ${event.reason} "); final result = await _loadFiles(); - final bool hasReloaded = _onFilesLoaded(result.files); - if (hasReloaded && kDebugMode) { + final bool hasTriggeredSetState = _onFilesLoaded(result.files); + if (hasTriggeredSetState && kDebugMode) { _logger.finest( "Reloaded gallery on soft refresh all files on ${event.reason}", ); } - - setState(() {}); + if (!hasTriggeredSetState && mounted) { + setState(() {}); + } }); }); } @@ -208,8 +247,9 @@ class GalleryState extends State { _hasLoadedFiles = true; currentGroupedFiles = updatedGroupedFiles; }); + return true; } - return true; + return false; } else { currentGroupedFiles = updatedGroupedFiles; return false; diff --git a/mobile/lib/ui/viewer/gallery/gallery_app_bar_widget.dart b/mobile/lib/ui/viewer/gallery/gallery_app_bar_widget.dart index d6ae1db26f..d29def8033 100644 --- a/mobile/lib/ui/viewer/gallery/gallery_app_bar_widget.dart +++ b/mobile/lib/ui/viewer/gallery/gallery_app_bar_widget.dart @@ -26,7 +26,7 @@ import "package:photos/models/metadata/common_keys.dart"; import 'package:photos/models/selected_files.dart'; import 'package:photos/service_locator.dart'; import 'package:photos/services/collections_service.dart'; -import 'package:photos/services/sync_service.dart'; +import "package:photos/services/files_service.dart"; import "package:photos/states/location_screen_state.dart"; import "package:photos/theme/colors.dart"; import 'package:photos/ui/actions/collection/collection_sharing_actions.dart'; @@ -39,6 +39,7 @@ import 'package:photos/ui/components/buttons/button_widget.dart'; import 'package:photos/ui/components/models/button_type.dart'; import "package:photos/ui/map/enable_map.dart"; import "package:photos/ui/map/map_screen.dart"; +import 'package:photos/ui/notification/toast.dart'; import 'package:photos/ui/sharing/album_participants_page.dart'; import "package:photos/ui/sharing/manage_links_widget.dart"; import 'package:photos/ui/sharing/share_collection_page.dart'; @@ -49,12 +50,11 @@ 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"; import "package:photos/ui/viewer/location/edit_location_sheet.dart"; -import 'package:photos/utils/data_util.dart'; import 'package:photos/utils/dialog_util.dart'; 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/toast_util.dart'; +import 'package:photos/utils/standalone/data.dart'; import "package:uuid/uuid.dart"; class GalleryAppBarWidget extends StatefulWidget { @@ -296,7 +296,7 @@ class _GalleryAppBarWidgetState extends State { await dialog.show(); BackupStatus status; try { - status = await SyncService.instance + status = await FilesService.instance .getBackupStatus(pathID: widget.deviceCollection!.id); } catch (e) { await dialog.hide(); @@ -820,7 +820,7 @@ class _GalleryAppBarWidgetState extends State { "Cannot share empty collection of type $galleryType", ); } - if (Configuration.instance.getUserID() == widget.collection!.owner!.id) { + if (Configuration.instance.getUserID() == widget.collection!.owner.id) { unawaited( routeToPage( context, diff --git a/mobile/lib/ui/viewer/gallery/shared_public_collection_page.dart b/mobile/lib/ui/viewer/gallery/shared_public_collection_page.dart index f4905eb4c7..a6e341d58c 100644 --- a/mobile/lib/ui/viewer/gallery/shared_public_collection_page.dart +++ b/mobile/lib/ui/viewer/gallery/shared_public_collection_page.dart @@ -13,10 +13,11 @@ import "package:photos/models/file_load_result.dart"; import "package:photos/models/gallery_type.dart"; import "package:photos/models/selected_files.dart"; import "package:photos/services/collections_service.dart"; -import "package:photos/services/remote_sync_service.dart"; +import "package:photos/services/sync/remote_sync_service.dart"; import "package:photos/ui/components/buttons/button_widget.dart"; import "package:photos/ui/components/end_to_end_banner.dart"; import "package:photos/ui/components/models/button_type.dart"; +import "package:photos/ui/notification/toast.dart"; import "package:photos/ui/viewer/actions/file_selection_overlay_bar.dart"; import "package:photos/ui/viewer/gallery/collection_page.dart"; import "package:photos/ui/viewer/gallery/gallery.dart"; @@ -25,7 +26,6 @@ import "package:photos/ui/viewer/gallery/state/gallery_files_inherited_widget.da import "package:photos/ui/viewer/gallery/state/selection_state.dart"; import "package:photos/utils/dialog_util.dart"; import "package:photos/utils/navigation_util.dart"; -import "package:photos/utils/toast_util.dart"; class SharedPublicCollectionPage extends StatefulWidget { final CollectionWithThumbnail c; diff --git a/mobile/lib/ui/viewer/gallery/state/gallery_context_state.dart b/mobile/lib/ui/viewer/gallery/state/gallery_context_state.dart index 4b202c39bd..a3e7ae6048 100644 --- a/mobile/lib/ui/viewer/gallery/state/gallery_context_state.dart +++ b/mobile/lib/ui/viewer/gallery/state/gallery_context_state.dart @@ -11,9 +11,9 @@ class GalleryContextState extends InheritedWidget { this.inSelectionMode = false, this.type = GroupType.day, required this.sortOrderAsc, - required Widget child, - Key? key, - }) : super(key: key, child: child); + required super.child, + super.key, + }); static GalleryContextState? of(BuildContext context) { return context.dependOnInheritedWidgetOfExactType(); diff --git a/mobile/lib/ui/viewer/gallery/trash_page.dart b/mobile/lib/ui/viewer/gallery/trash_page.dart index 4c5570f2ef..499bcc73c5 100644 --- a/mobile/lib/ui/viewer/gallery/trash_page.dart +++ b/mobile/lib/ui/viewer/gallery/trash_page.dart @@ -126,7 +126,7 @@ class TrashPage extends StatelessWidget { } class BottomButtonsWidget extends StatelessWidget { - const BottomButtonsWidget({Key? key}) : super(key: key); + const BottomButtonsWidget({super.key}); @override Widget build(BuildContext context) { diff --git a/mobile/lib/ui/viewer/location/update_location_data_widget.dart b/mobile/lib/ui/viewer/location/update_location_data_widget.dart index edafccd0e4..16414270d1 100644 --- a/mobile/lib/ui/viewer/location/update_location_data_widget.dart +++ b/mobile/lib/ui/viewer/location/update_location_data_widget.dart @@ -13,7 +13,7 @@ import "package:photos/theme/effects.dart"; import "package:photos/theme/ente_theme.dart"; import "package:photos/ui/map/map_button.dart"; import "package:photos/ui/map/tile/layers.dart"; -import "package:photos/utils/toast_util.dart"; +import "package:photos/ui/notification/toast.dart"; class UpdateLocationDataWidget extends StatefulWidget { final List files; @@ -49,8 +49,9 @@ class _UpdateLocationDataWidgetState extends State { FlutterMap( mapController: _mapController, options: MapOptions( - enableMultiFingerGestureRace: true, - zoom: 3, + interactionOptions: + const InteractionOptions(enableMultiFingerGestureRace: true), + initialZoom: 3, maxZoom: 18.0, minZoom: 2.8, onMapEvent: (p0) { @@ -62,8 +63,8 @@ class _UpdateLocationDataWidgetState extends State { }, onTap: (tapPosition, latlng) { final zoom = selectedLocation.value == null - ? _mapController.zoom + 2.0 - : _mapController.zoom; + ? _mapController.camera.zoom + 2.0 + : _mapController.camera.zoom; _mapController.move(latlng, zoom); selectedLocation.value = latlng; @@ -138,8 +139,8 @@ class _UpdateLocationDataWidgetState extends State { icon: Icons.add, onPressed: () { _mapController.move( - _mapController.center, - _mapController.zoom + 1, + _mapController.camera.center, + _mapController.camera.zoom + 1, ); }, heroTag: 'zoom-in', @@ -149,8 +150,8 @@ class _UpdateLocationDataWidgetState extends State { icon: Icons.remove, onPressed: () { _mapController.move( - _mapController.center, - _mapController.zoom - 1, + _mapController.camera.center, + _mapController.camera.zoom - 1, ); }, heroTag: 'zoom-out', diff --git a/mobile/lib/ui/viewer/people/cluster_page.dart b/mobile/lib/ui/viewer/people/cluster_page.dart index 8df8c4b6e6..2b89097be9 100644 --- a/mobile/lib/ui/viewer/people/cluster_page.dart +++ b/mobile/lib/ui/viewer/people/cluster_page.dart @@ -13,6 +13,7 @@ import 'package:photos/models/gallery_type.dart'; import "package:photos/models/ml/face/person.dart"; import 'package:photos/models/selected_files.dart'; import "package:photos/services/machine_learning/face_ml/feedback/cluster_feedback.dart"; +import "package:photos/ui/notification/toast.dart"; import 'package:photos/ui/viewer/actions/file_selection_overlay_bar.dart'; import 'package:photos/ui/viewer/gallery/gallery.dart'; import "package:photos/ui/viewer/gallery/state/gallery_files_inherited_widget.dart"; @@ -24,7 +25,6 @@ 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/search/result/search_result_page.dart"; import "package:photos/utils/navigation_util.dart"; -import "package:photos/utils/toast_util.dart"; class ClusterPage extends StatefulWidget { final List searchResult; diff --git a/mobile/lib/ui/viewer/people/link_email_screen.dart b/mobile/lib/ui/viewer/people/link_email_screen.dart index bf610d0542..2a647754d1 100644 --- a/mobile/lib/ui/viewer/people/link_email_screen.dart +++ b/mobile/lib/ui/viewer/people/link_email_screen.dart @@ -10,8 +10,8 @@ import "package:photos/generated/l10n.dart"; import "package:photos/l10n/l10n.dart"; import "package:photos/models/api/collection/user.dart"; import "package:photos/models/ml/face/person.dart"; +import "package:photos/services/account/user_service.dart"; import "package:photos/services/machine_learning/face_ml/person/person_service.dart"; -import "package:photos/services/user_service.dart"; import 'package:photos/theme/ente_theme.dart'; import 'package:photos/ui/components/buttons/button_widget.dart'; import 'package:photos/ui/components/captioned_text_widget.dart'; diff --git a/mobile/lib/ui/viewer/people/new_person_item_widget.dart b/mobile/lib/ui/viewer/people/new_person_item_widget.dart deleted file mode 100644 index 88fa0fa3de..0000000000 --- a/mobile/lib/ui/viewer/people/new_person_item_widget.dart +++ /dev/null @@ -1,74 +0,0 @@ -import 'package:dotted_border/dotted_border.dart'; -import 'package:flutter/material.dart'; -import "package:photos/generated/l10n.dart"; -import 'package:photos/theme/ente_theme.dart'; - -///https://www.figma.com/file/SYtMyLBs5SAOkTbfMMzhqt/ente-Visual-Design?node-id=10854%3A57947&t=H5AvR79OYDnB9ekw-4 -class NewPersonItemWidget extends StatelessWidget { - const NewPersonItemWidget({ - super.key, - }); - - @override - Widget build(BuildContext context) { - final textTheme = getEnteTextTheme(context); - final colorScheme = getEnteColorScheme(context); - const sideOfThumbnail = 60.0; - return LayoutBuilder( - builder: (context, constraints) { - return Stack( - alignment: Alignment.center, - children: [ - Row( - children: [ - ClipRRect( - borderRadius: const BorderRadius.horizontal( - left: Radius.circular(4), - ), - child: SizedBox( - height: sideOfThumbnail, - width: sideOfThumbnail, - child: Icon( - Icons.add_outlined, - color: colorScheme.strokeMuted, - ), - ), - ), - Padding( - padding: const EdgeInsets.only(left: 12), - child: Text( - S.of(context).addNewPerson, - style: - textTheme.body.copyWith(color: colorScheme.textMuted), - ), - ), - ], - ), - IgnorePointer( - child: DottedBorder( - dashPattern: const [4], - color: colorScheme.strokeFainter, - strokeWidth: 1, - padding: const EdgeInsets.all(0), - borderType: BorderType.RRect, - radius: const Radius.circular(4), - child: SizedBox( - //Have to decrease the height and width by 1 pt as the stroke - //dotted border gives is of strokeAlign.center, so 0.5 inside and - // outside. Here for the row, stroke should be inside so we - //decrease the size of this sizedBox by 1 (so it shrinks 0.5 from - //every side) so that the strokeAlign.center of this sizedBox - //looks like a strokeAlign.inside in the row. - height: sideOfThumbnail - 1, - //This width will work for this only if the row widget takes up the - //full size it's parent (stack). - width: constraints.maxWidth - 1, - ), - ), - ), - ], - ); - }, - ); - } -} 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 ba9379f103..b3a03451c4 100644 --- a/mobile/lib/ui/viewer/people/person_selection_action_widgets.dart +++ b/mobile/lib/ui/viewer/people/person_selection_action_widgets.dart @@ -16,14 +16,15 @@ import "package:photos/ui/common/loading_widget.dart"; 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/utils/dialog_util.dart"; import "package:photos/utils/person_contact_linking_util.dart"; -import "package:photos/utils/toast_util.dart"; class PersonEntityWithThumbnailFile { final PersonEntity person; - final EnteFile thumbnailFile; + final EnteFile? thumbnailFile; const PersonEntityWithThumbnailFile( this.person, @@ -61,7 +62,8 @@ class _LinkContactToPersonSelectionPageState (person.data.isHidden || person.data.isIgnored)) { continue; } - final file = await PersonService.instance.getRecentFileOfPerson(person); + final file = + await PersonService.instance.getThumbnailFileOfPerson(person); result.add(PersonEntityWithThumbnailFile(person, file)); } return result; @@ -70,6 +72,7 @@ class _LinkContactToPersonSelectionPageState @override Widget build(BuildContext context) { + _logger.info("Building LinkContactToPersonSelectionPage"); final smallFontSize = getEnteTextTheme(context).small.fontSize!; final textScaleFactor = MediaQuery.textScalerOf(context).scale(smallFontSize) / smallFontSize; @@ -88,6 +91,11 @@ class _LinkContactToPersonSelectionPageState if (snapshot.connectionState == ConnectionState.waiting) { return const Center(child: EnteLoadingWidget()); } else if (snapshot.hasError) { + _logger.severe( + "Failed to load _personEntitiesWithThumnailFile", + snapshot.error, + snapshot.stackTrace, + ); return const Center(child: Icon(Icons.error_outline_rounded)); } else if (!snapshot.hasData || snapshot.data!.isEmpty) { return Center(child: Text(S.of(context).noResultsFound + '.')); @@ -229,7 +237,8 @@ class _ReassignMeSelectionPageState extends State { (person.data.isHidden || person.data.isIgnored)) { continue; } - final file = await PersonService.instance.getRecentFileOfPerson(person); + final file = + await PersonService.instance.getThumbnailFileOfPerson(person); result.add(PersonEntityWithThumbnailFile(person, file)); } return result; @@ -238,6 +247,7 @@ class _ReassignMeSelectionPageState extends State { @override Widget build(BuildContext context) { + _logger.info("Building ReassignMeSelectionPage"); final smallFontSize = getEnteTextTheme(context).small.fontSize!; final textScaleFactor = MediaQuery.textScalerOf(context).scale(smallFontSize) / smallFontSize; @@ -256,6 +266,11 @@ class _ReassignMeSelectionPageState extends State { if (snapshot.connectionState == ConnectionState.waiting) { return const Center(child: EnteLoadingWidget()); } else if (snapshot.hasError) { + _logger.severe( + "Failed to load _personEntitiesWithThumnailFile", + snapshot.error, + snapshot.stackTrace, + ); return const Center(child: Icon(Icons.error_outline_rounded)); } else if (!snapshot.hasData || snapshot.data!.isEmpty) { return Center(child: Text(S.of(context).noResultsFound + '.')); @@ -410,11 +425,14 @@ class _RoundedPersonFaceWidget extends StatelessWidget { ), ), ), - child: PersonFaceWidget( - personEntitiesWithThumbnailFile.thumbnailFile, - personId: personEntitiesWithThumbnailFile.person.remoteID, - useFullFile: true, - ), + child: personEntitiesWithThumbnailFile.thumbnailFile == null + ? const NoThumbnailWidget(addBorder: false) + : PersonFaceWidget( + personEntitiesWithThumbnailFile.thumbnailFile!, + personId: + personEntitiesWithThumbnailFile.person.remoteID, + useFullFile: true, + ), ), ), ), 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 9216801a20..5e543f7ef4 100644 --- a/mobile/lib/ui/viewer/people/save_or_edit_person.dart +++ b/mobile/lib/ui/viewer/people/save_or_edit_person.dart @@ -18,17 +18,18 @@ import "package:photos/l10n/l10n.dart"; import "package:photos/models/api/collection/user.dart"; import "package:photos/models/file/file.dart"; 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/services/user_service.dart"; import "package:photos/theme/ente_theme.dart"; import "package:photos/ui/common/date_input.dart"; import "package:photos/ui/common/loading_widget.dart"; import "package:photos/ui/components/action_sheet_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/album_share_info_widget.dart"; import "package:photos/ui/sharing/user_avator_widget.dart"; import "package:photos/ui/viewer/file/no_thumbnail_widget.dart"; @@ -40,7 +41,6 @@ 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"; -import "package:photos/utils/toast_util.dart"; class SaveOrEditPerson extends StatefulWidget { final String? clusterID; diff --git a/mobile/lib/ui/viewer/search/result/no_result_widget.dart b/mobile/lib/ui/viewer/search/result/no_result_widget.dart index dc64a8e322..fe08b86ba5 100644 --- a/mobile/lib/ui/viewer/search/result/no_result_widget.dart +++ b/mobile/lib/ui/viewer/search/result/no_result_widget.dart @@ -7,7 +7,7 @@ import "package:photos/states/all_sections_examples_state.dart"; import "package:photos/theme/ente_theme.dart"; class NoResultWidget extends StatefulWidget { - const NoResultWidget({Key? key}) : super(key: key); + const NoResultWidget({super.key}); @override State createState() => _NoResultWidgetState(); 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 ec0be2f447..aa0b6dc6d0 100644 --- a/mobile/lib/ui/viewer/search/result/search_thumbnail_widget.dart +++ b/mobile/lib/ui/viewer/search/result/search_thumbnail_widget.dart @@ -89,7 +89,7 @@ class _ContactSearchThumbnailWidgetState if (person == null) { return null; } else { - return PersonService.instance.getRecentFileOfPerson(person); + return PersonService.instance.getThumbnailFileOfPerson(person); } }); } diff --git a/mobile/lib/ui/viewer/search/search_suffix_icon_widget.dart b/mobile/lib/ui/viewer/search/search_suffix_icon_widget.dart index 744684422d..9f94bb863b 100644 --- a/mobile/lib/ui/viewer/search/search_suffix_icon_widget.dart +++ b/mobile/lib/ui/viewer/search/search_suffix_icon_widget.dart @@ -5,7 +5,7 @@ import "package:photos/theme/ente_theme.dart"; class SearchSuffixIcon extends StatefulWidget { final bool shouldShowSpinner; - const SearchSuffixIcon(this.shouldShowSpinner, {Key? key}) : super(key: key); + const SearchSuffixIcon(this.shouldShowSpinner, {super.key}); @override State createState() => _SearchSuffixIconState(); diff --git a/mobile/lib/ui/viewer/search/search_widget.dart b/mobile/lib/ui/viewer/search/search_widget.dart index a71254a0c7..1461d37f99 100644 --- a/mobile/lib/ui/viewer/search/search_widget.dart +++ b/mobile/lib/ui/viewer/search/search_widget.dart @@ -13,8 +13,8 @@ import "package:photos/models/search/search_result.dart"; import "package:photos/services/search_service.dart"; import "package:photos/theme/ente_theme.dart"; import "package:photos/ui/viewer/search/search_suffix_icon_widget.dart"; -import "package:photos/utils/date_time_util.dart"; -import "package:photos/utils/debouncer.dart"; +import "package:photos/utils/standalone/date_time.dart"; +import "package:photos/utils/standalone/debouncer.dart"; class SearchWidget extends StatefulWidget { const SearchWidget({super.key}); diff --git a/mobile/lib/ui/viewer/search_tab/contacts_section.dart b/mobile/lib/ui/viewer/search_tab/contacts_section.dart index d92a314684..bc751fbfa4 100644 --- a/mobile/lib/ui/viewer/search_tab/contacts_section.dart +++ b/mobile/lib/ui/viewer/search_tab/contacts_section.dart @@ -163,7 +163,7 @@ class _ContactRecommendationState extends State { if (person == null) { return null; } else { - return PersonService.instance.getRecentFileOfPerson(person); + return PersonService.instance.getThumbnailFileOfPerson(person); } }); } diff --git a/mobile/lib/ui/viewer/search_tab/search_tab.dart b/mobile/lib/ui/viewer/search_tab/search_tab.dart index 27e3f46830..ec616f7ec4 100644 --- a/mobile/lib/ui/viewer/search_tab/search_tab.dart +++ b/mobile/lib/ui/viewer/search_tab/search_tab.dart @@ -10,7 +10,6 @@ import "package:photos/models/search/index_of_indexed_stack.dart"; import "package:photos/models/search/search_result.dart"; import "package:photos/models/search/search_types.dart"; import "package:photos/service_locator.dart"; -import "package:photos/services/user_remote_flag_service.dart"; import "package:photos/states/all_sections_examples_state.dart"; import "package:photos/ui/common/loading_widget.dart"; import "package:photos/ui/viewer/search/result/no_result_widget.dart"; @@ -119,9 +118,7 @@ class _AllSearchSectionsState extends State { itemBuilder: (context, index) { switch (searchTypes[index]) { case SectionType.face: - if (!userRemoteFlagService.getCachedBoolValue( - UserRemoteFlagService.mlEnabled, - )) { + if (!flagService.hasGrantedMLConsent) { return const SizedBox.shrink(); } return PeopleSection( diff --git a/mobile/lib/utils/delete_file_util.dart b/mobile/lib/utils/delete_file_util.dart index ef771f455d..1285675003 100644 --- a/mobile/lib/utils/delete_file_util.dart +++ b/mobile/lib/utils/delete_file_util.dart @@ -13,21 +13,25 @@ import 'package:photos/events/files_updated_event.dart'; import "package:photos/events/force_reload_trash_page_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/api/collection/trash_item_request.dart'; +import "package:photos/models/backup_status.dart"; import 'package:photos/models/file/file.dart'; import "package:photos/models/files_split.dart"; import 'package:photos/models/selected_files.dart'; -import 'package:photos/models/trash_item_request.dart'; import "package:photos/service_locator.dart"; -import 'package:photos/services/remote_sync_service.dart'; -import 'package:photos/services/sync_service.dart'; +import "package:photos/services/files_service.dart"; +import "package:photos/services/sync/local_sync_service.dart"; +import 'package:photos/services/sync/remote_sync_service.dart'; +import 'package:photos/services/sync/sync_service.dart'; import 'package:photos/ui/common/linear_progress_dialog.dart'; import 'package:photos/ui/components/action_sheet_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/utils/device_info.dart"; import 'package:photos/utils/dialog_util.dart'; import 'package:photos/utils/file_util.dart'; -import 'package:photos/utils/toast_util.dart'; final _logger = Logger("DeleteFileUtil"); @@ -325,6 +329,7 @@ Future deleteLocalFiles( BuildContext context, List localIDs, ) async { + _logger.info("Trying to delete local files "); final List deletedIDs = []; final List localAssetIDs = []; final List localSharedMediaIDs = []; @@ -374,14 +379,167 @@ Future deleteLocalFiles( //This is a workaround so that users are not shown an error message on //android 10 if (!await isAndroidSDKVersionLowerThan(android11SDKINT)) { - showToast(context, S.of(context).couldNotFreeUpSpace); return false; } return true; } } catch (e, s) { _logger.severe("Could not delete local files", e, s); - showToast(context, S.of(context).couldNotFreeUpSpace); + return false; + } +} + +Future deleteLocalFilesAfterRemovingAlreadyDeletedIDs( + BuildContext context, + List localIDs, +) async { + _logger.info( + "Trying to delete local files after removing already deleted IDs", + ); + + final List deletedIDs = []; + final List localAssetIDs = []; + final List localSharedMediaIDs = []; + final List alreadyDeletedIDs = []; // to ignore already deleted files + + final dialog = createProgressDialog(context, "Loading..."); + await dialog.show(); + try { + final files = + await FilesDB.instance.getLocalFiles(localIDs, dedupeByLocalID: true); + for (final file in files) { + if (!(await _localFileExist(file))) { + _logger.warning("Already deleted " + file.toString()); + alreadyDeletedIDs.add(file.localID!); + } else if (file.localID!.startsWith(oldSharedMediaIdentifier) || + file.localID!.startsWith(sharedMediaIdentifier)) { + localSharedMediaIDs.add(file.localID!); + } else { + localAssetIDs.add(file.localID!); + } + } + deletedIDs.addAll(alreadyDeletedIDs); + deletedIDs.addAll(await _tryDeleteSharedMediaFiles(localSharedMediaIDs)); + + await dialog.hide(); + + final bool shouldDeleteInBatches = + await isAndroidSDKVersionLowerThan(android11SDKINT); + if (shouldDeleteInBatches) { + _logger.info("Deleting in batches"); + deletedIDs + .addAll(await deleteLocalFilesInBatches(context, localAssetIDs)); + } else { + _logger.info("Deleting in one shot"); + deletedIDs + .addAll(await _deleteLocalFilesInOneShot(context, localAssetIDs)); + } + // In IOS, the library returns no error and fail to delete any file is + // there's any shared file. As a stop-gap solution, we initiate deletion in + // batches. Similar in Android, for large number of files, we have observed + // that the library fails to delete any file. So, we initiate deletion in + // batches. + if (deletedIDs.isEmpty && Platform.isIOS) { + deletedIDs.addAll( + await _iosDeleteLocalFilesInBatchesFallback(context, localAssetIDs), + ); + } + + if (deletedIDs.isNotEmpty) { + final deletedFiles = await FilesDB.instance.getLocalFiles(deletedIDs); + await FilesDB.instance.deleteLocalFiles(deletedIDs); + _logger.info(deletedFiles.length.toString() + " files deleted locally"); + Bus.instance.fire( + LocalPhotosUpdatedEvent(deletedFiles, source: "deleteLocal"), + ); + return true; + } else { + //On android 10, even if files were deleted, deletedIDs is empty. + //This is a workaround so that users are not shown an error message on + //android 10 + if (!await isAndroidSDKVersionLowerThan(android11SDKINT)) { + return false; + } + return true; + } + } catch (e, s) { + _logger.severe("Could not delete local files", e, s); + await dialog.hide(); + return false; + } +} + +/// Only to be used on Android +Future retryFreeUpSpaceAfterRemovingAssetsNonExistingInDisk( + BuildContext context, +) async { + _logger.info( + "Retrying free up space after removing assets non-existing in disk", + ); + + final dialog = + createProgressDialog(context, context.l10n.pleaseWaitThisWillTakeAWhile); + await dialog.show(); + try { + final stopwatch = Stopwatch()..start(); + final res = await PhotoManager.editor.android.removeAllNoExistsAsset(); + if (res == false) { + _logger.warning("Failed to remove non-existing assets"); + } + _logger.info( + "removeAllNoExistsAsset took: ${stopwatch.elapsedMilliseconds}ms", + ); + await LocalSyncService.instance.sync(); + + late final BackupStatus status; + final List deletedIDs = []; + final List localAssetIDs = []; + final List localSharedMediaIDs = []; + status = await FilesService.instance.getBackupStatus(); + + for (String localID in status.localIDs) { + if (localID.startsWith(oldSharedMediaIdentifier) || + localID.startsWith(sharedMediaIdentifier)) { + localSharedMediaIDs.add(localID); + } else { + localAssetIDs.add(localID); + } + } + deletedIDs.addAll(await _tryDeleteSharedMediaFiles(localSharedMediaIDs)); + + await dialog.hide(); + + final bool shouldDeleteInBatches = + await isAndroidSDKVersionLowerThan(android11SDKINT); + if (shouldDeleteInBatches) { + _logger.info("Deleting in batches"); + deletedIDs + .addAll(await deleteLocalFilesInBatches(context, localAssetIDs)); + } else { + _logger.info("Deleting in one shot"); + deletedIDs + .addAll(await _deleteLocalFilesInOneShot(context, localAssetIDs)); + } + + if (deletedIDs.isNotEmpty) { + final deletedFiles = await FilesDB.instance.getLocalFiles(deletedIDs); + await FilesDB.instance.deleteLocalFiles(deletedIDs); + _logger.info(deletedFiles.length.toString() + " files deleted locally"); + Bus.instance.fire( + LocalPhotosUpdatedEvent(deletedFiles, source: "deleteLocal"), + ); + return true; + } else { + //On android 10, even if files were deleted, deletedIDs is empty. + //This is a workaround so that users are not shown an error message on + //android 10 + if (!await isAndroidSDKVersionLowerThan(android11SDKINT)) { + return false; + } + return true; + } + } catch (e) { + await dialog.hide(); return false; } } diff --git a/mobile/lib/utils/device_info.dart b/mobile/lib/utils/device_info.dart index 4925a0b144..ea9509b54e 100644 --- a/mobile/lib/utils/device_info.dart +++ b/mobile/lib/utils/device_info.dart @@ -1,46 +1,9 @@ import 'dart:io'; import "package:device_info_plus/device_info_plus.dart"; -import 'package:flutter/foundation.dart'; import 'package:logging/logging.dart'; DeviceInfoPlugin deviceInfoPlugin = DeviceInfoPlugin(); -// https://gist.github.com/adamawolf/3048717 -final Set iOSLowEndMachineCodes = { - "iPhone5,1", //iPhone 5 (GSM) - "iPhone5,2", //iPhone 5 (GSM+CDMA) - "iPhone5,3", //iPhone 5C (GSM) - "iPhone5,4", //iPhone 5C (Global) - "iPhone6,1", //iPhone 5S (GSM) - "iPhone6,2", //iPhone 5S (Global) - "iPhone7,1", //iPhone 6 Plus - "iPhone7,2", //iPhone 6 - "iPhone8,1", // iPhone 6s - "iPhone8,2", // iPhone 6s Plus - "iPhone8,4", // iPhone SE (GSM) - "iPhone9,1", // iPhone 7 - "iPhone9,2", // iPhone 7 Plus - "iPhone9,3", // iPhone 7 - "iPhone9,4", // iPhone 7 Plus - "iPhone10,1", // iPhone 8 - "iPhone10,2", // iPhone 8 Plus - "iPhone10,3", // iPhone X Global - "iPhone10,4", // iPhone 8 - "iPhone10,5", // iPhone 8 -}; - -Future isLowSpecDevice() async { - try { - if (Platform.isIOS) { - final IosDeviceInfo iosInfo = await deviceInfoPlugin.iosInfo; - debugPrint("ios utc name ${iosInfo.utsname.machine}"); - return iOSLowEndMachineCodes.contains(iosInfo.utsname.machine); - } - } catch (e) { - Logger("device_info").severe("deviceSpec check failed", e); - } - return false; -} Future isAndroidSDKVersionLowerThan(int inputSDK) async { if (Platform.isAndroid) { diff --git a/mobile/lib/utils/dialog_util.dart b/mobile/lib/utils/dialog_util.dart index 8d3f65e438..9fcadb2763 100644 --- a/mobile/lib/utils/dialog_util.dart +++ b/mobile/lib/utils/dialog_util.dart @@ -76,7 +76,7 @@ Future showErrorDialogForException({ }) async { String errorMessage = message ?? S.of(context).tempErrorContactSupportIfPersists; - if (exception is DioError && + if (exception is DioException && exception.response != null && exception.response!.data["code"] != null) { errorMessage = @@ -107,9 +107,12 @@ String parseErrorForUI( if (error == null) { return genericError; } - if (error is DioError) { - final DioError dioError = error; - if (dioError.type == DioErrorType.other) { + if (error is DioException) { + final DioException dioError = error; + if (dioError.type == DioExceptionType.receiveTimeout || + dioError.type == DioExceptionType.connectionError || + dioError.type == DioExceptionType.sendTimeout || + dioError.type == DioExceptionType.cancel) { if (dioError.error.toString().contains('Failed host lookup')) { return S.of(context).networkHostLookUpErr; } else if (dioError.error.toString().contains('SocketException')) { @@ -122,15 +125,15 @@ String parseErrorForUI( return genericError; } String errorInfo = ""; - if (error is DioError) { - final DioError dioError = error; - if (dioError.type == DioErrorType.response) { + if (error is DioException) { + final DioException dioError = error; + if (dioError.type == DioExceptionType.badResponse) { if (dioError.response?.data["code"] != null) { errorInfo = "Reason: " + dioError.response!.data["code"]; } else { errorInfo = "Reason: " + dioError.response!.data.toString(); } - } else if (dioError.type == DioErrorType.other) { + } else if (dioError.type == DioExceptionType.badCertificate) { errorInfo = "Reason: " + dioError.error.toString(); } else { errorInfo = "Reason: " + dioError.type.toString(); diff --git a/mobile/lib/utils/email_util.dart b/mobile/lib/utils/email_util.dart index d85db2bb20..c92659e380 100644 --- a/mobile/lib/utils/email_util.dart +++ b/mobile/lib/utils/email_util.dart @@ -1,7 +1,6 @@ import 'dart:io'; import 'package:archive/archive_io.dart'; -import "package:cross_file/cross_file.dart"; import 'package:email_validator/email_validator.dart'; import "package:file_saver/file_saver.dart"; import 'package:flutter/cupertino.dart'; @@ -20,9 +19,9 @@ import "package:photos/ui/common/progress_dialog.dart"; 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/tools/debug/log_file_viewer.dart'; import 'package:photos/utils/dialog_util.dart'; -import 'package:photos/utils/toast_util.dart'; import 'package:share_plus/share_plus.dart'; import 'package:url_launcher/url_launcher.dart'; diff --git a/mobile/lib/utils/exif_util.dart b/mobile/lib/utils/exif_util.dart index 652336ebee..a94e02bece 100644 --- a/mobile/lib/utils/exif_util.dart +++ b/mobile/lib/utils/exif_util.dart @@ -47,7 +47,7 @@ Future> getExif(EnteFile file) async { } } -Future?> getExifFromSourceFile(File originFile) async { +Future?> tryExifFromFile(File originFile) async { try { final exif = await readExifAsync(originFile); return exif; @@ -125,7 +125,25 @@ bool? checkPanoramaFromEXIF(File? file, Map? exifData) { return element?.printable == "6"; } -Future getCreationTimeFromEXIF( +class ParsedExifDateTime { + late final DateTime? time; + late final String? dateTime; + late final String? offsetTime; + ParsedExifDateTime(DateTime this.time, String? dateTime, this.offsetTime) { + if (dateTime != null && dateTime.endsWith('Z')) { + this.dateTime = dateTime.substring(0, dateTime.length - 1); + } else { + this.dateTime = dateTime; + } + } + + @override + String toString() { + return "ParsedExifDateTime{time: $time, dateTime: $dateTime, offsetTime: $offsetTime}"; + } +} + +Future tryParseExifDateTime( File? file, Map? exifData, ) async { @@ -137,46 +155,55 @@ Future getCreationTimeFromEXIF( : exif.containsKey(kImageDateTime) ? exif[kImageDateTime]!.printable : null; - if (exifTime != null && exifTime != kEmptyExifDateTime) { - String? exifOffsetTime; - for (final key in kExifOffSetKeys) { - if (exif.containsKey(key)) { - exifOffsetTime = exif[key]!.printable; - break; - } - } - return getDateTimeInDeviceTimezone(exifTime, exifOffsetTime); + if (exifTime == null || exifTime == kEmptyExifDateTime) { + return null; } + String? exifOffsetTime; + for (final key in kExifOffSetKeys) { + if (exif.containsKey(key)) { + exifOffsetTime = exif[key]!.printable; + break; + } + } + return getDateTimeInDeviceTimezone(exifTime, exifOffsetTime); } catch (e) { _logger.severe("failed to getCreationTimeFromEXIF", e); } return null; } -DateTime getDateTimeInDeviceTimezone(String exifTime, String? offsetString) { - final DateTime result = DateFormat(kExifDateTimePattern).parse(exifTime); - if (offsetString == null) { - return result; +ParsedExifDateTime getDateTimeInDeviceTimezone( + String exifTime, + String? offsetString, +) { + final hasOffset = (offsetString ?? '') != ''; + final DateTime result = + DateFormat(kExifDateTimePattern).parse(exifTime, hasOffset); + if (hasOffset && offsetString!.toUpperCase() != "Z") { + try { + final List splitHHMM = offsetString.split(":"); + final int offsetHours = int.parse(splitHHMM[0]); + final int offsetMinutes = + int.parse(splitHHMM[1]) * (offsetHours.isNegative ? -1 : 1); + // Adjust the date for the offset to get the photo's correct UTC time + final photoUtcDate = + result.add(Duration(hours: -offsetHours, minutes: -offsetMinutes)); + // Convert the UTC time to the device's local time + final deviceLocalTime = photoUtcDate.toLocal(); + return ParsedExifDateTime( + deviceLocalTime, + result.toIso8601String(), + offsetString, + ); + } catch (e, s) { + _logger.severe("offset parsing failed $exifTime && $offsetString", e, s); + } } - try { - final List splitHHMM = offsetString.split(":"); - // Parse the offset from the photo's time zone - final int offsetHours = int.parse(splitHHMM[0]); - final int offsetMinutes = - int.parse(splitHHMM[1]) * (offsetHours.isNegative ? -1 : 1); - // Adjust the date for the offset to get the photo's correct UTC time - final photoUtcDate = - result.add(Duration(hours: -offsetHours, minutes: -offsetMinutes)); - // Getting the current device's time zone offset from UTC - final now = DateTime.now(); - final localOffset = now.timeZoneOffset; - // Adjusting the photo's UTC time to the device's local time - final deviceLocalTime = photoUtcDate.add(localOffset); - return deviceLocalTime; - } catch (e, s) { - _logger.severe("tz offset adjust failed $offsetString", e, s); - } - return result; + return ParsedExifDateTime( + result, + result.toIso8601String(), + (offsetString ?? '').toUpperCase() == 'Z' ? 'Z' : null, + ); } Location? locationFromExif(Map exif) { diff --git a/mobile/lib/utils/face/face_box_crop.dart b/mobile/lib/utils/face/face_box_crop.dart index c39e164977..c75be4a533 100644 --- a/mobile/lib/utils/face/face_box_crop.dart +++ b/mobile/lib/utils/face/face_box_crop.dart @@ -216,7 +216,7 @@ Future?> _getFaceCrops( faceBoxes, ); final Map result = {}; - for (int i = 0; i < faceIds.length; i++) { + for (int i = 0; i < faceCrop.length; i++) { result[faceIds[i]] = faceCrop[i]; } return result; diff --git a/mobile/lib/utils/face/face_util.dart b/mobile/lib/utils/face/face_util.dart deleted file mode 100644 index cb5b186b04..0000000000 --- a/mobile/lib/utils/face/face_util.dart +++ /dev/null @@ -1,175 +0,0 @@ -import "dart:math"; -import "dart:typed_data"; - -import "package:computer/computer.dart"; -import "package:flutter_image_compress/flutter_image_compress.dart"; -import "package:image/image.dart" as img; -import "package:logging/logging.dart"; -import "package:photos/models/ml/face/box.dart"; - -/// Bounding box of a face. -/// -/// [xMin] and [yMin] are the coordinates of the top left corner of the box, and -/// [width] and [height] are the width and height of the box. -/// -/// One unit is equal to one pixel in the original image. -class FaceBoxImage { - final int xMin; - final int yMin; - final int width; - final int height; - - FaceBoxImage({ - required this.xMin, - required this.yMin, - required this.width, - required this.height, - }); -} - -final _logger = Logger("FaceUtil"); -final _computer = Computer.shared(); -const _faceImageBufferFactor = 0.2; - -///Convert img.Image to ui.Image and use RawImage to display. -Future> generateImgFaceThumbnails( - String imagePath, - List faceBoxes, -) async { - final faceThumbnails = []; - - final image = await decodeToImgImage(imagePath); - - for (FaceBox faceBox in faceBoxes) { - final croppedImage = cropFaceBoxFromImage(image, faceBox); - faceThumbnails.add(croppedImage); - } - - return faceThumbnails; -} - -Future> generateJpgFaceThumbnails( - String imagePath, - List faceBoxes, -) async { - final image = await decodeToImgImage(imagePath); - final croppedImages = []; - for (FaceBox faceBox in faceBoxes) { - final croppedImage = cropFaceBoxFromImage(image, faceBox); - croppedImages.add(croppedImage); - } - - return await _computer - .compute(_encodeImagesToJpg, param: {"images": croppedImages}); -} - -Future decodeToImgImage(String imagePath) async { - img.Image? image = - await _computer.compute(_decodeImageFile, param: {"filePath": imagePath}); - - if (image == null) { - _logger.info( - "Failed to decode image. Compressing to jpg and decoding", - ); - final compressedJPGImage = - await FlutterImageCompress.compressWithFile(imagePath); - image = await _computer.compute( - _decodeJpg, - param: {"image": compressedJPGImage}, - ); - - if (image == null) { - throw Exception("Failed to decode image"); - } else { - return image; - } - } else { - return image; - } -} - -/// Returns an Image from 'package:image/image.dart' -img.Image cropFaceBoxFromImage(img.Image image, FaceBox faceBox) { - final squareFaceBox = _getSquareFaceBoxImage(image, faceBox); - final squareFaceBoxWithBuffer = - _addBufferAroundFaceBox(squareFaceBox, _faceImageBufferFactor); - return img.copyCrop( - image, - x: squareFaceBoxWithBuffer.xMin, - y: squareFaceBoxWithBuffer.yMin, - width: squareFaceBoxWithBuffer.width, - height: squareFaceBoxWithBuffer.height, - antialias: false, - ); -} - -/// Returns a square face box image from the original image with -/// side length equal to the maximum of the width and height of the face box in -/// the OG image. -FaceBoxImage _getSquareFaceBoxImage(img.Image image, FaceBox faceBox) { - final width = (image.width * faceBox.width).round(); - final height = (image.height * faceBox.height).round(); - final side = max(width, height); - final xImage = (image.width * faceBox.x).round(); - final yImage = (image.height * faceBox.y).round(); - - if (height >= width) { - final xImageAdj = (xImage - (height - width) / 2).round(); - return FaceBoxImage( - xMin: xImageAdj, - yMin: yImage, - width: side, - height: side, - ); - } else { - final yImageAdj = (yImage - (width - height) / 2).round(); - return FaceBoxImage( - xMin: xImage, - yMin: yImageAdj, - width: side, - height: side, - ); - } -} - -///To add some buffer around the face box so that the face isn't cropped -///too close to the face. -FaceBoxImage _addBufferAroundFaceBox( - FaceBoxImage faceBoxImage, - double bufferFactor, -) { - final heightBuffer = faceBoxImage.height * bufferFactor; - final widthBuffer = faceBoxImage.width * bufferFactor; - final xMinWithBuffer = faceBoxImage.xMin - widthBuffer; - final yMinWithBuffer = faceBoxImage.yMin - heightBuffer; - final widthWithBuffer = faceBoxImage.width + 2 * widthBuffer; - final heightWithBuffer = faceBoxImage.height + 2 * heightBuffer; - //Do not add buffer if the top left edge of the image is out of bounds - //after adding the buffer. - if (xMinWithBuffer < 0 || yMinWithBuffer < 0) { - return faceBoxImage; - } - //Another similar case that can be handled is when the bottom right edge - //of the image is out of bounds after adding the buffer. But the - //the visual difference is not as significant as when the top left edge - //is out of bounds, so we are not handling that case. - return FaceBoxImage( - xMin: xMinWithBuffer.round(), - yMin: yMinWithBuffer.round(), - width: widthWithBuffer.round(), - height: heightWithBuffer.round(), - ); -} - -List _encodeImagesToJpg(Map args) { - final images = args["images"] as List; - return images.map((img.Image image) => img.encodeJpg(image)).toList(); -} - -Future _decodeImageFile(Map args) async { - return await img.decodeImageFile(args["filePath"]); -} - -img.Image? _decodeJpg(Map args) { - return img.decodeJpg(args["image"])!; -} diff --git a/mobile/lib/utils/file_download_util.dart b/mobile/lib/utils/file_download_util.dart index b998c22db3..420ad34713 100644 --- a/mobile/lib/utils/file_download_util.dart +++ b/mobile/lib/utils/file_download_util.dart @@ -3,6 +3,7 @@ import "dart:collection"; import 'dart:io'; import 'package:dio/dio.dart'; +import 'package:ente_crypto/ente_crypto.dart'; import "package:flutter/foundation.dart"; import 'package:logging/logging.dart'; import 'package:path/path.dart' as file_path; @@ -17,12 +18,11 @@ import "package:photos/models/file/file_type.dart"; import "package:photos/models/ignored_file.dart"; import "package:photos/services/collections_service.dart"; import "package:photos/services/ignored_files_service.dart"; -import "package:photos/services/local_sync_service.dart"; -import 'package:photos/utils/crypto_util.dart'; -import "package:photos/utils/data_util.dart"; -import "package:photos/utils/fake_progress.dart"; +import "package:photos/services/sync/local_sync_service.dart"; import "package:photos/utils/file_key.dart"; import "package:photos/utils/file_util.dart"; +import "package:photos/utils/standalone/data.dart"; +import "package:photos/utils/standalone/fake_progress.dart"; final _logger = Logger("file_download_util"); diff --git a/mobile/lib/utils/file_key.dart b/mobile/lib/utils/file_key.dart index 2a27d33bc2..345c6f6b1d 100644 --- a/mobile/lib/utils/file_key.dart +++ b/mobile/lib/utils/file_key.dart @@ -1,9 +1,9 @@ import "dart:typed_data"; import "package:computer/computer.dart"; +import "package:ente_crypto/ente_crypto.dart"; import "package:photos/models/file/file.dart"; import "package:photos/services/collections_service.dart"; -import "package:photos/utils/crypto_util.dart"; Uint8List getFileKey(EnteFile file) { final encryptedKey = CryptoUtil.base642bin(file.encryptedKey!); diff --git a/mobile/lib/utils/file_uploader.dart b/mobile/lib/utils/file_uploader.dart index 05bf94a947..5d78af6a06 100644 --- a/mobile/lib/utils/file_uploader.dart +++ b/mobile/lib/utils/file_uploader.dart @@ -6,6 +6,7 @@ import 'dart:math' as math; import 'package:collection/collection.dart'; import 'package:dio/dio.dart'; +import 'package:ente_crypto/ente_crypto.dart'; import 'package:flutter/foundation.dart'; import 'package:logging/logging.dart'; import "package:path/path.dart"; @@ -23,28 +24,27 @@ import 'package:photos/events/files_updated_event.dart'; import 'package:photos/events/local_photos_updated_event.dart'; import 'package:photos/events/subscription_purchased_event.dart'; import 'package:photos/main.dart'; +import "package:photos/models/api/metadata.dart"; import "package:photos/models/backup/backup_item.dart"; import "package:photos/models/backup/backup_item_status.dart"; -import 'package:photos/models/encryption_result.dart'; import 'package:photos/models/file/file.dart'; import 'package:photos/models/file/file_type.dart'; import "package:photos/models/metadata/file_magic.dart"; -import 'package:photos/models/upload_url.dart'; import "package:photos/models/user_details.dart"; +import 'package:photos/module/upload/model/upload_url.dart'; 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/file_magic_service.dart"; -import 'package:photos/services/local_sync_service.dart'; import "package:photos/services/preview_video_store.dart"; -import 'package:photos/services/sync_service.dart'; -import "package:photos/services/user_service.dart"; -import 'package:photos/utils/crypto_util.dart'; -import 'package:photos/utils/data_util.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"; import "package:photos/utils/file_key.dart"; import 'package:photos/utils/file_uploader_util.dart'; import "package:photos/utils/file_util.dart"; import "package:photos/utils/network_util.dart"; +import 'package:photos/utils/standalone/data.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:tuple/tuple.dart'; import "package:uuid/uuid.dart"; @@ -82,7 +82,6 @@ class FileUploader { int _uploadCounter = 0; int _videoUploadCounter = 0; late ProcessType _processType; - late bool _isBackground; late SharedPreferences _prefs; // _hasInitiatedForceUpload is used to track if user attempted force upload @@ -104,7 +103,6 @@ class FileUploader { Future init(SharedPreferences preferences, bool isBackground) async { _prefs = preferences; - _isBackground = isBackground; _processType = isBackground ? ProcessType.background : ProcessType.foreground; final currentTime = DateTime.now().microsecondsSinceEpoch; @@ -538,7 +536,7 @@ class FileUploader { MediaUploadData? mediaUploadData; try { - mediaUploadData = await getUploadDataFromEnteFile(file); + mediaUploadData = await getUploadDataFromEnteFile(file, parseExif: true); } catch (e) { // This additional try catch block is added because for resumable upload, // we need to compute the hash before the next step. Previously, this @@ -730,8 +728,13 @@ class FileUploader { encThumbSize, ); } + final ParsedExifDateTime? exifTime = await tryParseExifDateTime( + null, + mediaUploadData.exifData, + ); + final metadata = + await file.getMetadataForUpload(mediaUploadData, exifTime); - final metadata = await file.getMetadataForUpload(mediaUploadData); final encryptedMetadataResult = await CryptoUtil.encryptChaCha( utf8.encode(jsonEncode(metadata)), fileAttributes.key!, @@ -748,6 +751,12 @@ class FileUploader { if (SyncService.instance.shouldStopSync()) { throw SyncStopRequestedError(); } + final stillLocked = + await _uploadLocks.isLocked(lockKey, _processType.toString()); + if (!stillLocked) { + _logger.warning('file ${file.tag} report paused is missing'); + throw LockFreedError(); + } EnteFile remoteFile; if (isUpdatedFile) { @@ -773,22 +782,9 @@ class FileUploader { CryptoUtil.bin2base64(encryptedFileKeyData.encryptedData!); final keyDecryptionNonce = CryptoUtil.bin2base64(encryptedFileKeyData.nonce!); - final Map pubMetadata = {}; + final Map pubMetadata = + _buildPublicMagicData(mediaUploadData, exifTime); MetadataRequest? pubMetadataRequest; - if ((mediaUploadData.height ?? 0) != 0 && - (mediaUploadData.width ?? 0) != 0) { - pubMetadata[heightKey] = mediaUploadData.height; - pubMetadata[widthKey] = mediaUploadData.width; - pubMetadata[mediaTypeKey] = - mediaUploadData.isPanorama == true ? 1 : 0; - } - if (mediaUploadData.motionPhotoStartIndex != null) { - pubMetadata[motionVideoIndexKey] = - mediaUploadData.motionPhotoStartIndex; - } - if (mediaUploadData.thumbnail == null) { - pubMetadata[noThumbKey] = true; - } if (pubMetadata.isNotEmpty) { pubMetadataRequest = await getPubMetadataRequest( file, @@ -823,14 +819,12 @@ class FileUploader { } await UploadLocksDB.instance.deleteMultipartTrack(lockKey); - if (!_isBackground) { - Bus.instance.fire( - LocalPhotosUpdatedEvent( - [remoteFile], - source: "downloadComplete", - ), - ); - } + Bus.instance.fire( + LocalPhotosUpdatedEvent( + [remoteFile], + source: "uploadCompleted", + ), + ); _logger.info("File upload complete for " + remoteFile.toString()); uploadCompleted = true; Bus.instance.fire(FileUploadedEvent(remoteFile)); @@ -872,8 +866,36 @@ class FileUploader { } } + Map _buildPublicMagicData( + MediaUploadData mediaUploadData, + ParsedExifDateTime? exifTime, + ) { + final Map pubMetadata = {}; + if ((mediaUploadData.height ?? 0) != 0 && + (mediaUploadData.width ?? 0) != 0) { + pubMetadata[heightKey] = mediaUploadData.height; + pubMetadata[widthKey] = mediaUploadData.width; + pubMetadata[mediaTypeKey] = mediaUploadData.isPanorama == true ? 1 : 0; + } + if (mediaUploadData.motionPhotoStartIndex != null) { + pubMetadata[motionVideoIndexKey] = mediaUploadData.motionPhotoStartIndex; + } + if (mediaUploadData.thumbnail == null) { + pubMetadata[noThumbKey] = true; + } + if (exifTime != null) { + if (exifTime.dateTime != null) { + pubMetadata[dateTimeKey] = exifTime.dateTime; + } + if (exifTime.offsetTime != null) { + pubMetadata[offsetTimeKey] = exifTime.offsetTime; + } + } + return pubMetadata; + } + bool isPutOrUpdateFileError(Object e) { - if (e is DioError) { + if (e is DioException) { return e.requestOptions.path.contains("/files") || e.requestOptions.path.contains("/files/update"); } @@ -1168,13 +1190,16 @@ class FileUploader { file.thumbnailDecryptionHeader = thumbnailDecryptionHeader; file.metadataDecryptionHeader = metadataDecryptionHeader; return file; - } on DioError catch (e) { - if (e.response?.statusCode == 413) { + } on DioException catch (e) { + final int statusCode = e.response?.statusCode ?? -1; + if (statusCode == 413) { throw FileTooLargeForPlanError(); - } else if (e.response?.statusCode == 426) { + } else if (statusCode == 426) { _onStorageLimitExceeded(); - } else if (attempt < kMaximumUploadAttempts) { - _logger.info("Upload file failed, will retry in 3 seconds"); + } else if (attempt < kMaximumUploadAttempts && statusCode == -1) { + // retry when DioException contains no response/status code + _logger + .info("Upload file (${file.tag}) failed, will retry in 3 seconds"); await Future.delayed(const Duration(seconds: 3)); return _uploadFile( file, @@ -1193,6 +1218,8 @@ class FileUploader { attempt: attempt + 1, pubMetadata: pubMetadata, ); + } else { + _logger.severe("Failed to upload file ${file.tag}", e); } rethrow; } @@ -1236,11 +1263,13 @@ class FileUploader { file.thumbnailDecryptionHeader = thumbnailDecryptionHeader; file.metadataDecryptionHeader = metadataDecryptionHeader; return file; - } on DioError catch (e) { - if (e.response?.statusCode == 426) { + } on DioException catch (e) { + final int statusCode = e.response?.statusCode ?? -1; + if (statusCode == 426) { _onStorageLimitExceeded(); - } else if (attempt < kMaximumUploadAttempts) { - _logger.info("Update file failed, will retry in 3 seconds"); + } else if (attempt < kMaximumUploadAttempts && statusCode == -1) { + _logger + .info("Update file (${file.tag}) failed, will retry in 3 seconds"); await Future.delayed(const Duration(seconds: 3)); return _updateFile( file, @@ -1254,6 +1283,8 @@ class FileUploader { metadataDecryptionHeader, attempt: attempt + 1, ); + } else { + _logger.severe("Failed to update file ${file.tag}", e); } rethrow; } @@ -1292,7 +1323,7 @@ class FileUploader { .map((e) => UploadURL.fromMap(e)) .toList(); _uploadURLs.addAll(urls); - } on DioError catch (e, s) { + } on DioException catch (e, s) { if (e.response != null) { if (e.response!.statusCode == 402) { final error = NoActiveSubscriptionError(); @@ -1342,8 +1373,8 @@ class FileUploader { ); return uploadURL.objectKey; - } on DioError catch (e) { - if (e.message.startsWith("HttpException: Content size")) { + } on DioException catch (e) { + if (e.message?.startsWith("HttpException: Content size") ?? false) { rethrow; } else if (attempt < kMaximumUploadAttempts) { _logger.info("Upload failed for $fileName, retrying"); diff --git a/mobile/lib/utils/file_uploader_util.dart b/mobile/lib/utils/file_uploader_util.dart index 6ad6622f5f..5ca93defe0 100644 --- a/mobile/lib/utils/file_uploader_util.dart +++ b/mobile/lib/utils/file_uploader_util.dart @@ -6,6 +6,8 @@ import 'dart:ui' as ui; import "package:archive/archive_io.dart"; import "package:computer/computer.dart"; +import 'package:ente_crypto/ente_crypto.dart'; +import "package:exif/exif.dart"; import 'package:logging/logging.dart'; import "package:motion_photos/motion_photos.dart"; import 'package:motionphoto/motionphoto.dart'; @@ -15,14 +17,13 @@ import 'package:photo_manager/photo_manager.dart'; import 'package:photos/core/configuration.dart'; import 'package:photos/core/constants.dart'; import 'package:photos/core/errors.dart'; +import "package:photos/models/api/metadata.dart"; import "package:photos/models/ffmpeg/ffprobe_props.dart"; import "package:photos/models/file/extensions/file_props.dart"; import 'package:photos/models/file/file.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/services/file_magic_service.dart"; -import 'package:photos/utils/crypto_util.dart'; import "package:photos/utils/exif_util.dart"; import 'package:photos/utils/file_util.dart'; import "package:uuid/uuid.dart"; @@ -30,7 +31,6 @@ import 'package:video_thumbnail/video_thumbnail.dart'; final _logger = Logger("FileUtil"); const kMaximumThumbnailCompressionAttempts = 2; -const kLivePhotoHashSeparator = ':'; class MediaUploadData { final File? sourceFile; @@ -44,6 +44,8 @@ class MediaUploadData { // For iOS, this value will be always null. final int? motionPhotoStartIndex; + final Map? exifData; + bool? isPanorama; MediaUploadData( @@ -55,6 +57,7 @@ class MediaUploadData { this.width, this.motionPhotoStartIndex, this.isPanorama, + this.exifData, }); } @@ -69,20 +72,27 @@ class FileHashData { FileHashData(this.fileHash, {this.zipHash}); } -Future getUploadDataFromEnteFile(EnteFile file) async { +Future getUploadDataFromEnteFile( + EnteFile file, { + bool parseExif = false, +}) async { if (file.isSharedMediaToAppSandbox) { - return await _getMediaUploadDataFromAppCache(file); + return await _getMediaUploadDataFromAppCache(file, parseExif); } else { - return await _getMediaUploadDataFromAssetFile(file); + return await _getMediaUploadDataFromAssetFile(file, parseExif); } } -Future _getMediaUploadDataFromAssetFile(EnteFile file) async { +Future _getMediaUploadDataFromAssetFile( + EnteFile file, + bool parseExif, +) async { File? sourceFile; Uint8List? thumbnailData; bool isDeleted; String? zipHash; String fileHash; + Map? exifData; // The timeouts are to safeguard against https://github.com/CaiJingLong/flutter_photo_manager/issues/467 final asset = await file.getAsset @@ -115,8 +125,11 @@ Future _getMediaUploadDataFromAssetFile(EnteFile file) async { InvalidReason.sourceFileMissing, ); } + if (parseExif) { + exifData = await tryExifFromFile(sourceFile); + } // h4ck to fetch location data if missing (thank you Android Q+) lazily only during uploads - await _decorateEnteFileData(file, asset, sourceFile); + await _decorateEnteFileData(file, asset, sourceFile, exifData); fileHash = CryptoUtil.bin2base64(await CryptoUtil.getHash(sourceFile)); if (file.fileType == FileType.livePhoto && Platform.isIOS) { @@ -177,6 +190,7 @@ Future _getMediaUploadDataFromAssetFile(EnteFile file) async { height: h, width: w, motionPhotoStartIndex: motionPhotoStartingIndex, + exifData: exifData, ); } @@ -284,6 +298,7 @@ Future _decorateEnteFileData( EnteFile file, AssetEntity asset, File sourceFile, + Map? exifData, ) async { // h4ck to fetch location data if missing (thank you Android Q+) lazily only during uploads if (file.location == null || @@ -298,6 +313,13 @@ Future _decorateEnteFileData( file.location = props.location; } } + if (Platform.isAndroid && exifData != null) { + //Fix for missing location data in lower android versions. + final Location? exifLocation = locationFromExif(exifData); + if (Location.isValidLocation(exifLocation)) { + file.location = exifLocation; + } + } if (file.title == null || file.title!.isEmpty) { _logger.warning("Title was missing ${file.tag}"); file.title = await asset.titleAsync; @@ -319,7 +341,7 @@ Future getPubMetadataRequest( file.pubMmdEncodedJson = jsonEncode(jsonToUpdate); file.pubMagicMetadata = PubMagicMetadata.fromJson(jsonToUpdate); final encryptedMMd = await CryptoUtil.encryptChaCha( - utf8.encode(jsonEncode(jsonToUpdate)) as Uint8List, + utf8.encode(jsonEncode(jsonToUpdate)), fileKey, ); return MetadataRequest( @@ -330,9 +352,13 @@ Future getPubMetadataRequest( ); } -Future _getMediaUploadDataFromAppCache(EnteFile file) async { +Future _getMediaUploadDataFromAppCache( + EnteFile file, + bool parseExif, +) async { File sourceFile; Uint8List? thumbnailData; + Map? exifData; const bool isDeleted = false; final localPath = getSharedMediaFilePath(file); sourceFile = File(localPath); @@ -350,6 +376,7 @@ Future _getMediaUploadDataFromAppCache(EnteFile file) async { Map? dimensions; if (file.fileType == FileType.image) { dimensions = await getImageHeightAndWith(imagePath: localPath); + exifData = await tryExifFromFile(sourceFile); } else if (thumbnailData != null) { // the thumbnail null check is to ensure that we are able to generate thum // for video, we need to use the thumbnail data with any max width/height @@ -368,6 +395,7 @@ Future _getMediaUploadDataFromAppCache(EnteFile file) async { FileHashData(fileHash), height: dimensions?['height'], width: dimensions?['width'], + exifData: exifData, ); } catch (e, s) { _logger.warning("failed to generate thumbnail", e, s); diff --git a/mobile/lib/utils/file_util.dart b/mobile/lib/utils/file_util.dart index 8b0f76669f..facde2840e 100644 --- a/mobile/lib/utils/file_util.dart +++ b/mobile/lib/utils/file_util.dart @@ -254,7 +254,7 @@ Future<_LivePhoto?> _downloadLivePhoto( if (compressResult == null) { throw Exception("Failed to compress file"); } else { - imageConvertedFile = compressResult; + imageConvertedFile = File(compressResult.path); } } imageFileCache = await DefaultCacheManager().putFile( @@ -316,7 +316,7 @@ Future _downloadAndCache( if (compressResult == null) { throw Exception("Failed to convert heic to jpg"); } else { - outputFile = compressResult; + outputFile = File(compressResult.path); } await decryptedFile.delete(); } diff --git a/mobile/lib/utils/gzip.dart b/mobile/lib/utils/gzip.dart index 8de71f49df..3c8fbafe9e 100644 --- a/mobile/lib/utils/gzip.dart +++ b/mobile/lib/utils/gzip.dart @@ -2,8 +2,8 @@ import "dart:convert"; import "dart:io"; import "package:computer/computer.dart"; +import "package:ente_crypto/ente_crypto.dart"; import "package:flutter/foundation.dart"; -import "package:photos/utils/crypto_util.dart"; class ChaChaEncryptionResult { final String encData; diff --git a/mobile/lib/utils/image_ml_util.dart b/mobile/lib/utils/image_ml_util.dart index dd1e4512bc..bdd84d96f1 100644 --- a/mobile/lib/utils/image_ml_util.dart +++ b/mobile/lib/utils/image_ml_util.dart @@ -170,7 +170,7 @@ Future> generateFaceThumbnailsUsingCanvas( return faceThumbnails; } catch (e, s) { _logger.severe( - 'Error generating face thumbnails. cropImage problematic input argument: ${faceBoxes[i]}', + 'Error generating face thumbnails. cropImage problematic input argument: ${i}th facebox: ${faceBoxes[i].toString()}', e, s, ); diff --git a/mobile/lib/utils/lat_lon_util.dart b/mobile/lib/utils/lat_lon_util.dart deleted file mode 100644 index a270e39024..0000000000 --- a/mobile/lib/utils/lat_lon_util.dart +++ /dev/null @@ -1,14 +0,0 @@ -String convertLatLng(double decimal, bool isLat) { - final degree = "${decimal.toString().split(".")[0]}°"; - final minutesBeforeConversion = - double.parse("0.${decimal.toString().split(".")[1]}"); - final minutes = "${(minutesBeforeConversion * 60).toString().split('.')[0]}'"; - final secondsBeforeConversion = double.parse( - "0.${(minutesBeforeConversion * 60).toString().split('.')[1]}", - ); - final seconds = - '${double.parse((secondsBeforeConversion * 60).toString()).toStringAsFixed(0)}" '; - final dmsOutput = - "$degree$minutes$seconds${isLat ? decimal > 0 ? 'N' : 'S' : decimal > 0 ? 'E' : 'W'}"; - return dmsOutput; -} diff --git a/mobile/lib/utils/local_settings.dart b/mobile/lib/utils/local_settings.dart index 204db7d3ca..9fcd9e9adc 100644 --- a/mobile/lib/utils/local_settings.dart +++ b/mobile/lib/utils/local_settings.dart @@ -55,6 +55,20 @@ class LocalSettings { } } + // getEstimatedInstallTimeInMs returns the time when the app was installed + // The time is stored in shared preferences and will be reset on logout + DateTime getInstallDateTime() { + if (_prefs.containsKey('ls.install_time')) { + return DateTime.fromMillisecondsSinceEpoch( + _prefs.getInt('ls.install_time')!, + ); + } else { + final installTime = DateTime.now(); + _prefs.setInt('ls.install_time', installTime.millisecondsSinceEpoch); + return installTime; + } + } + Future setRateUsShownCount(int value) async { await _prefs.setInt(kRateUsShownCount, value); } diff --git a/mobile/lib/utils/lock_screen_settings.dart b/mobile/lib/utils/lock_screen_settings.dart index 2a42b1d930..f60ed48504 100644 --- a/mobile/lib/utils/lock_screen_settings.dart +++ b/mobile/lib/utils/lock_screen_settings.dart @@ -1,9 +1,9 @@ import "dart:convert"; +import "package:ente_crypto/ente_crypto.dart"; import "package:flutter/foundation.dart"; import "package:flutter_secure_storage/flutter_secure_storage.dart"; import "package:flutter_sodium/flutter_sodium.dart"; -import "package:photos/utils/crypto_util.dart"; import "package:privacy_screen/privacy_screen.dart"; import "package:shared_preferences/shared_preferences.dart"; diff --git a/mobile/lib/utils/magic_util.dart b/mobile/lib/utils/magic_util.dart index 62411de438..923eb07b61 100644 --- a/mobile/lib/utils/magic_util.dart +++ b/mobile/lib/utils/magic_util.dart @@ -17,8 +17,8 @@ import "package:photos/models/metadata/file_magic.dart"; import 'package:photos/services/collections_service.dart'; import 'package:photos/services/file_magic_service.dart'; import 'package:photos/ui/common/progress_dialog.dart'; +import 'package:photos/ui/notification/toast.dart'; import 'package:photos/utils/dialog_util.dart'; -import 'package:photos/utils/toast_util.dart'; final _logger = Logger('MagicUtil'); diff --git a/mobile/lib/utils/multipart_upload_util.dart b/mobile/lib/utils/multipart_upload_util.dart deleted file mode 100644 index 6b9ccafb97..0000000000 --- a/mobile/lib/utils/multipart_upload_util.dart +++ /dev/null @@ -1,157 +0,0 @@ -// ignore_for_file: implementation_imports - -import "dart:io"; - -import "package:dio/dio.dart"; -import "package:logging/logging.dart"; -import "package:photos/core/constants.dart"; -import "package:photos/core/network/network.dart"; -import 'package:photos/module/upload/model/xml.dart'; -import "package:photos/service_locator.dart"; - -final _enteDio = NetworkClient.instance.enteDio; -final _dio = NetworkClient.instance.getDio(); - -class PartETag extends XmlParsableObject { - final int partNumber; - final String eTag; - - PartETag(this.partNumber, this.eTag); - - @override - String get elementName => "Part"; - - @override - Map toMap() { - return { - "PartNumber": partNumber, - "ETag": eTag, - }; - } -} - -class MultipartUploadURLs { - final String objectKey; - final List partsURLs; - final String completeURL; - - MultipartUploadURLs({ - required this.objectKey, - required this.partsURLs, - required this.completeURL, - }); - - factory MultipartUploadURLs.fromMap(Map map) { - return MultipartUploadURLs( - objectKey: map["urls"]["objectKey"], - partsURLs: (map["urls"]["partURLs"] as List).cast(), - completeURL: map["urls"]["completeURL"], - ); - } -} - -Future calculatePartCount(int fileSize) async { - final partCount = (fileSize / multipartPartSize).ceil(); - return partCount; -} - -Future getMultipartUploadURLs(int count) async { - try { - assert( - flagService.internalUser, - "Multipart upload should not be enabled for external users.", - ); - final response = await _enteDio.get( - "/files/multipart-upload-urls", - queryParameters: { - "count": count, - }, - ); - - return MultipartUploadURLs.fromMap(response.data); - } on Exception catch (e) { - Logger("MultipartUploadURL").severe(e); - rethrow; - } -} - -Future putMultipartFile( - MultipartUploadURLs urls, - File encryptedFile, -) async { - // upload individual parts and get their etags - final etags = await uploadParts(urls.partsURLs, encryptedFile); - - // complete the multipart upload - await completeMultipartUpload(etags, urls.completeURL); - - return urls.objectKey; -} - -Future> uploadParts( - List partsURLs, - File encryptedFile, -) async { - final partsLength = partsURLs.length; - final etags = {}; - - for (int i = 0; i < partsLength; i++) { - final partURL = partsURLs[i]; - final isLastPart = i == partsLength - 1; - final fileSize = isLastPart - ? encryptedFile.lengthSync() % multipartPartSize - : multipartPartSize; - - final response = await _dio.put( - partURL, - data: encryptedFile.openRead( - i * multipartPartSize, - isLastPart ? null : multipartPartSize, - ), - options: Options( - headers: { - Headers.contentLengthHeader: fileSize, - }, - ), - ); - - final eTag = response.headers.value("etag"); - - if (eTag?.isEmpty ?? true) { - throw Exception('ETAG_MISSING'); - } - - etags[i] = eTag!; - } - - return etags; -} - -Future completeMultipartUpload( - Map partEtags, - String completeURL, -) async { - final body = convertJs2Xml({ - 'CompleteMultipartUpload': partEtags.entries - .map( - (e) => PartETag( - e.key + 1, - e.value, - ), - ) - .toList(), - }).replaceAll('"', '').replaceAll('"', ''); - - try { - await _dio.post( - completeURL, - data: body, - options: Options( - contentType: "text/xml", - ), - ); - } catch (e) { - Logger("MultipartUpload").severe(e); - rethrow; - } -} diff --git a/mobile/lib/utils/primitive_wrapper.dart b/mobile/lib/utils/primitive_wrapper.dart deleted file mode 100644 index 20ea9bbb6e..0000000000 --- a/mobile/lib/utils/primitive_wrapper.dart +++ /dev/null @@ -1,6 +0,0 @@ -///This is useful when you want to pass a primitive by reference. - -class PrimitiveWrapper { - var value; - PrimitiveWrapper(this.value); -} diff --git a/mobile/lib/utils/share_util.dart b/mobile/lib/utils/share_util.dart index c39d1733b9..2efd8bd1e6 100644 --- a/mobile/lib/utils/share_util.dart +++ b/mobile/lib/utils/share_util.dart @@ -14,10 +14,10 @@ import "package:photos/models/collection/collection.dart"; import 'package:photos/models/file/file.dart'; import 'package:photos/models/file/file_type.dart'; import "package:photos/ui/sharing/show_images_prevew.dart"; -import 'package:photos/utils/date_time_util.dart'; import 'package:photos/utils/dialog_util.dart'; import 'package:photos/utils/exif_util.dart'; import 'package:photos/utils/file_util.dart'; +import 'package:photos/utils/standalone/date_time.dart'; import 'package:receive_sharing_intent/receive_sharing_intent.dart'; import "package:screenshot/screenshot.dart"; import 'package:share_plus/share_plus.dart'; @@ -47,12 +47,6 @@ Future share( pathFutures.add( getFile(file, isOrigin: true).then((fetchedFile) => fetchedFile?.path), ); - if (file.fileType == FileType.livePhoto) { - pathFutures.add( - getFile(file, liveVideo: true) - .then((fetchedFile) => fetchedFile?.path), - ); - } } final paths = await Future.wait(pathFutures); await dialog.hide(); @@ -165,9 +159,9 @@ Future> convertIncomingSharedMediaToFile( enteFile.fileType = media.type == SharedMediaType.image ? FileType.image : FileType.video; if (enteFile.fileType == FileType.image) { - final exifTime = await getCreationTimeFromEXIF(ioFile, null); - if (exifTime != null) { - enteFile.creationTime = exifTime.microsecondsSinceEpoch; + final dateResult = await tryParseExifDateTime(ioFile, null); + if (dateResult != null && dateResult.time != null) { + enteFile.creationTime = dateResult.time!.microsecondsSinceEpoch; } } else if (enteFile.fileType == FileType.video) { enteFile.duration = (media.duration ?? 0) ~/ 1000; diff --git a/mobile/lib/utils/standalone/README.md b/mobile/lib/utils/standalone/README.md new file mode 100644 index 0000000000..f0e2884aef --- /dev/null +++ b/mobile/lib/utils/standalone/README.md @@ -0,0 +1,3 @@ +## Standalone Utils +This folder contains standalone utilities that can be used in any project. These utilities are not dependent on any other part of the project and can be used independently. +The utils inside this folder are not dependent on any other part of the project and can be used independently. \ No newline at end of file diff --git a/mobile/lib/utils/data_util.dart b/mobile/lib/utils/standalone/data.dart similarity index 100% rename from mobile/lib/utils/data_util.dart rename to mobile/lib/utils/standalone/data.dart diff --git a/mobile/lib/utils/date_time_util.dart b/mobile/lib/utils/standalone/date_time.dart similarity index 84% rename from mobile/lib/utils/date_time_util.dart rename to mobile/lib/utils/standalone/date_time.dart index afcd92aa16..bb29960319 100644 --- a/mobile/lib/utils/date_time_util.dart +++ b/mobile/lib/utils/standalone/date_time.dart @@ -216,3 +216,40 @@ bool isNumeric(String? s) { } return double.tryParse(s) != null; } + +/// Returns the duration in seconds from the format "h:mm:ss" or "m:ss". +int? durationToSeconds(String? duration) { + if (duration == null) { + return null; + } + final parts = duration.split(':'); + int seconds = 0; + + if (parts.length == 3) { + // Format: "h:mm:ss" + seconds += int.parse(parts[0]) * 3600; // Hours to seconds + seconds += int.parse(parts[1]) * 60; // Minutes to seconds + seconds += int.parse(parts[2]); // Seconds + } else if (parts.length == 2) { + // Format: "m:ss" + seconds += int.parse(parts[0]) * 60; // Minutes to seconds + seconds += int.parse(parts[1]); // Seconds + } else { + throw FormatException('Invalid duration format: $duration'); + } + + return seconds; +} + +/// Returns the duration in the format "h:mm:ss" or "m:ss". +String secondsToDuration(int totalSeconds) { + final hours = totalSeconds ~/ 3600; + final minutes = (totalSeconds % 3600) ~/ 60; + final seconds = totalSeconds % 60; + + if (hours > 0) { + return '${hours.toString().padLeft(1, '0')}:${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}'; + } else { + return '${minutes.toString().padLeft(1, '0')}:${seconds.toString().padLeft(2, '0')}'; + } +} diff --git a/mobile/lib/utils/debouncer.dart b/mobile/lib/utils/standalone/debouncer.dart similarity index 95% rename from mobile/lib/utils/debouncer.dart rename to mobile/lib/utils/standalone/debouncer.dart index 07097440f0..fc0e6d6359 100644 --- a/mobile/lib/utils/debouncer.dart +++ b/mobile/lib/utils/standalone/debouncer.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'package:flutter/material.dart'; -import "package:photos/models/typedefs.dart"; ///Do not forget to cancel the debounce's timer using [cancelDebounceTimer] ///when the debouncer is no longer needed @@ -21,7 +20,7 @@ class Debouncer { final Stopwatch _stopwatch = Stopwatch(); - void run(FutureVoidCallback fn) { + void run(Future Function() fn) { if (leading && !isActive()) { _stopwatch.stop(); _stopwatch.reset(); diff --git a/mobile/lib/utils/directory_content.dart b/mobile/lib/utils/standalone/directory_content.dart similarity index 98% rename from mobile/lib/utils/directory_content.dart rename to mobile/lib/utils/standalone/directory_content.dart index 654e3af245..a868e51c0c 100644 --- a/mobile/lib/utils/directory_content.dart +++ b/mobile/lib/utils/standalone/directory_content.dart @@ -1,7 +1,7 @@ import 'dart:io'; import "package:path/path.dart"; -import "package:photos/utils/data_util.dart"; +import "package:photos/utils/standalone/data.dart"; class DirectoryStat { final String path; diff --git a/mobile/lib/utils/fake_progress.dart b/mobile/lib/utils/standalone/fake_progress.dart similarity index 100% rename from mobile/lib/utils/fake_progress.dart rename to mobile/lib/utils/standalone/fake_progress.dart diff --git a/mobile/lib/utils/parse.dart b/mobile/lib/utils/standalone/parse.dart similarity index 100% rename from mobile/lib/utils/parse.dart rename to mobile/lib/utils/standalone/parse.dart diff --git a/mobile/lib/utils/thumbnail_util.dart b/mobile/lib/utils/thumbnail_util.dart index c8db830735..17d52c6432 100644 --- a/mobile/lib/utils/thumbnail_util.dart +++ b/mobile/lib/utils/thumbnail_util.dart @@ -4,6 +4,7 @@ import 'dart:io'; import "dart:typed_data"; import 'package:dio/dio.dart'; +import 'package:ente_crypto/ente_crypto.dart'; import 'package:logging/logging.dart'; import 'package:photo_manager/photo_manager.dart'; import 'package:photos/core/cache/thumbnail_in_memory_cache.dart'; @@ -13,7 +14,6 @@ import 'package:photos/core/errors.dart'; import 'package:photos/core/network/network.dart'; import 'package:photos/models/file/file.dart'; import "package:photos/services/collections_service.dart"; -import 'package:photos/utils/crypto_util.dart'; import "package:photos/utils/file_key.dart"; import 'package:photos/utils/file_uploader_util.dart'; import 'package:photos/utils/file_util.dart'; @@ -192,7 +192,7 @@ Future _downloadAndDecryptThumbnail(FileDownloadItem item) async { .data; } } catch (e) { - if (e is DioError && CancelToken.isCancel(e)) { + if (e is DioException && CancelToken.isCancel(e)) { return; } rethrow; diff --git a/mobile/lib/utils/trash_diff_fetcher.dart b/mobile/lib/utils/trash_diff_fetcher.dart deleted file mode 100644 index e525125a82..0000000000 --- a/mobile/lib/utils/trash_diff_fetcher.dart +++ /dev/null @@ -1,129 +0,0 @@ -import 'dart:convert'; -import 'dart:math'; - -import "package:dio/dio.dart"; -import 'package:logging/logging.dart'; -import 'package:photos/models/file/trash_file.dart'; -import "package:photos/models/metadata/file_magic.dart"; -import 'package:photos/utils/crypto_util.dart'; -import "package:photos/utils/file_key.dart"; - -class TrashDiffFetcher { - final _logger = Logger("TrashDiffFetcher"); - final Dio _enteDio; - - TrashDiffFetcher(this._enteDio); - - Future getTrashFilesDiff(int sinceTime) async { - try { - final response = await _enteDio.get( - "/trash/v2/diff", - queryParameters: { - "sinceTime": sinceTime, - }, - ); - int latestUpdatedAtTime = 0; - final trashedFiles = []; - final deletedUploadIDs = []; - final restoredFiles = []; - - final diff = response.data["diff"] as List; - final bool hasMore = response.data["hasMore"] as bool; - final startTime = DateTime.now(); - for (final item in diff) { - final trash = TrashFile(); - trash.createdAt = item['createdAt']; - trash.updateAt = item['updatedAt']; - latestUpdatedAtTime = max(latestUpdatedAtTime, trash.updateAt); - if (item["isDeleted"]) { - deletedUploadIDs.add(item["file"]["id"]); - continue; - } - - trash.deleteBy = item['deleteBy']; - trash.uploadedFileID = item["file"]["id"]; - trash.collectionID = item["file"]["collectionID"]; - trash.updationTime = item["file"]["updationTime"]; - trash.ownerID = item["file"]["ownerID"]; - trash.encryptedKey = item["file"]["encryptedKey"]; - trash.keyDecryptionNonce = item["file"]["keyDecryptionNonce"]; - trash.fileDecryptionHeader = item["file"]["file"]["decryptionHeader"]; - trash.thumbnailDecryptionHeader = - item["file"]["thumbnail"]["decryptionHeader"]; - trash.metadataDecryptionHeader = - item["file"]["metadata"]["decryptionHeader"]; - final fileDecryptionKey = getFileKey(trash); - final encodedMetadata = await CryptoUtil.decryptChaCha( - CryptoUtil.base642bin(item["file"]["metadata"]["encryptedData"]), - fileDecryptionKey, - CryptoUtil.base642bin(trash.metadataDecryptionHeader!), - ); - final Map metadata = - jsonDecode(utf8.decode(encodedMetadata)); - trash.applyMetadata(metadata); - if (item["file"]['magicMetadata'] != null) { - final utfEncodedMmd = await CryptoUtil.decryptChaCha( - CryptoUtil.base642bin(item["file"]['magicMetadata']['data']), - fileDecryptionKey, - CryptoUtil.base642bin(item["file"]['magicMetadata']['header']), - ); - trash.mMdEncodedJson = utf8.decode(utfEncodedMmd); - trash.mMdVersion = item["file"]['magicMetadata']['version']; - } - if (item["file"]['pubMagicMetadata'] != null) { - final utfEncodedMmd = await CryptoUtil.decryptChaCha( - CryptoUtil.base642bin(item["file"]['pubMagicMetadata']['data']), - fileDecryptionKey, - CryptoUtil.base642bin(item["file"]['pubMagicMetadata']['header']), - ); - trash.pubMmdEncodedJson = utf8.decode(utfEncodedMmd); - trash.pubMmdVersion = item["file"]['pubMagicMetadata']['version']; - trash.pubMagicMetadata = - PubMagicMetadata.fromEncodedJson(trash.pubMmdEncodedJson!); - } - if (item['isRestored']) { - restoredFiles.add(trash); - continue; - } - trashedFiles.add(trash); - } - - final endTime = DateTime.now(); - _logger.info( - "time for parsing " + - diff.length.toString() + - ": " + - Duration( - microseconds: (endTime.microsecondsSinceEpoch - - startTime.microsecondsSinceEpoch), - ).inMilliseconds.toString(), - ); - return Diff( - trashedFiles, - restoredFiles, - deletedUploadIDs, - hasMore, - latestUpdatedAtTime, - ); - } catch (e, s) { - _logger.severe(e, s); - rethrow; - } - } -} - -class Diff { - final List trashedFiles; - final List restoredFiles; - final List deletedUploadIDs; - final bool hasMore; - final int lastSyncedTimeStamp; - - Diff( - this.trashedFiles, - this.restoredFiles, - this.deletedUploadIDs, - this.hasMore, - this.lastSyncedTimeStamp, - ); -} diff --git a/mobile/lib/utils/validator_util.dart b/mobile/lib/utils/validator_util.dart index fb8db69e14..b7a812a7ae 100644 --- a/mobile/lib/utils/validator_util.dart +++ b/mobile/lib/utils/validator_util.dart @@ -2,7 +2,7 @@ import 'dart:convert'; import 'dart:typed_data'; import 'package:logging/logging.dart'; -import 'package:photos/models/key_attributes.dart'; +import 'package:photos/models/api/user/key_attributes.dart'; Logger _logger = Logger("Validator"); @@ -33,7 +33,7 @@ void validatePreVerificationStateCheck( } // check password encoding issues try { - final Uint8List passwordL = utf8.encode(password!) as Uint8List; + final Uint8List passwordL = utf8.encode(password!); try { utf8.decode(passwordL); } catch (e) { diff --git a/mobile/lib/utils/xml_parser_util.dart b/mobile/lib/utils/xml_parser_util.dart deleted file mode 100644 index 8b13789179..0000000000 --- a/mobile/lib/utils/xml_parser_util.dart +++ /dev/null @@ -1 +0,0 @@ - diff --git a/mobile/plugins/ente_cast/pubspec.lock b/mobile/plugins/ente_cast/pubspec.lock index 8de07d1e38..7c17037ed2 100644 --- a/mobile/plugins/ente_cast/pubspec.lock +++ b/mobile/plugins/ente_cast/pubspec.lock @@ -1,6 +1,14 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + async: + dependency: transitive + description: + name: async + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" + url: "https://pub.dev" + source: hosted + version: "2.13.0" characters: dependency: transitive description: @@ -21,10 +29,18 @@ packages: dependency: "direct main" description: name: dio - sha256: "7d328c4d898a61efc3cd93655a0955858e29a0aa647f0f9e02d59b3bb275e2e8" + sha256: "253a18bbd4851fecba42f7343a1df3a9a4c1d31a2c1b37e221086b4fa8c8dbc9" url: "https://pub.dev" source: hosted - version: "4.0.6" + version: "5.8.0+1" + dio_web_adapter: + dependency: transitive + description: + name: dio_web_adapter + sha256: e485c7a39ff2b384fa1d7e09b4e25f755804de8384358049124830b04fc4f93a + url: "https://pub.dev" + source: hosted + version: "2.1.0" ffi: dependency: transitive description: diff --git a/mobile/plugins/ente_cast/pubspec.yaml b/mobile/plugins/ente_cast/pubspec.yaml index 967e147e91..6cc08a0849 100644 --- a/mobile/plugins/ente_cast/pubspec.yaml +++ b/mobile/plugins/ente_cast/pubspec.yaml @@ -7,7 +7,7 @@ environment: dependencies: collection: - dio: ^4.0.6 + dio: ^5.8.0+1 flutter: sdk: flutter shared_preferences: ^2.0.5 diff --git a/mobile/plugins/ente_cast_none/lib/src/service.dart b/mobile/plugins/ente_cast_none/lib/src/service.dart index c781889733..bcc6fb4e5a 100644 --- a/mobile/plugins/ente_cast_none/lib/src/service.dart +++ b/mobile/plugins/ente_cast_none/lib/src/service.dart @@ -17,19 +17,16 @@ class CastServiceImpl extends CastService { @override Future> searchDevices() { - // TODO: implement searchDevices throw UnimplementedError(); } @override Future closeActiveCasts() { - // TODO: implement closeActiveCasts throw UnimplementedError(); } @override Map getActiveSessions() { - // TODO: implement getActiveSessions throw UnimplementedError(); } } diff --git a/mobile/plugins/ente_cast_none/pubspec.lock b/mobile/plugins/ente_cast_none/pubspec.lock index ee83b2bd96..aeb3c20955 100644 --- a/mobile/plugins/ente_cast_none/pubspec.lock +++ b/mobile/plugins/ente_cast_none/pubspec.lock @@ -1,6 +1,14 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + async: + dependency: transitive + description: + name: async + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" + url: "https://pub.dev" + source: hosted + version: "2.13.0" characters: dependency: transitive description: @@ -21,10 +29,18 @@ packages: dependency: transitive description: name: dio - sha256: "7d328c4d898a61efc3cd93655a0955858e29a0aa647f0f9e02d59b3bb275e2e8" + sha256: "253a18bbd4851fecba42f7343a1df3a9a4c1d31a2c1b37e221086b4fa8c8dbc9" url: "https://pub.dev" source: hosted - version: "4.0.6" + version: "5.8.0+1" + dio_web_adapter: + dependency: transitive + description: + name: dio_web_adapter + sha256: e485c7a39ff2b384fa1d7e09b4e25f755804de8384358049124830b04fc4f93a + url: "https://pub.dev" + source: hosted + version: "2.1.0" ente_cast: dependency: "direct main" description: diff --git a/mobile/plugins/ente_cast_normal/pubspec.lock b/mobile/plugins/ente_cast_normal/pubspec.lock index 2d37f86c87..e1786a9a8f 100644 --- a/mobile/plugins/ente_cast_normal/pubspec.lock +++ b/mobile/plugins/ente_cast_normal/pubspec.lock @@ -38,10 +38,18 @@ packages: dependency: transitive description: name: dio - sha256: "7d328c4d898a61efc3cd93655a0955858e29a0aa647f0f9e02d59b3bb275e2e8" + sha256: "253a18bbd4851fecba42f7343a1df3a9a4c1d31a2c1b37e221086b4fa8c8dbc9" url: "https://pub.dev" source: hosted - version: "4.0.6" + version: "5.8.0+1" + dio_web_adapter: + dependency: transitive + description: + name: dio_web_adapter + sha256: e485c7a39ff2b384fa1d7e09b4e25f755804de8384358049124830b04fc4f93a + url: "https://pub.dev" + source: hosted + version: "2.1.0" ente_cast: dependency: "direct main" description: diff --git a/mobile/plugins/ente_crypto/.metadata b/mobile/plugins/ente_crypto/.metadata new file mode 100644 index 0000000000..9fc7ede54d --- /dev/null +++ b/mobile/plugins/ente_crypto/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: 0b8abb4724aa590dd0f429683339b1e045a1594d + channel: stable + +project_type: plugin diff --git a/mobile/plugins/ente_crypto/analysis_options.yaml b/mobile/plugins/ente_crypto/analysis_options.yaml new file mode 100644 index 0000000000..fac60e247c --- /dev/null +++ b/mobile/plugins/ente_crypto/analysis_options.yaml @@ -0,0 +1 @@ +include: ../../analysis_options.yaml \ No newline at end of file diff --git a/mobile/plugins/ente_crypto/lib/ente_crypto.dart b/mobile/plugins/ente_crypto/lib/ente_crypto.dart new file mode 100644 index 0000000000..02b6d0bb58 --- /dev/null +++ b/mobile/plugins/ente_crypto/lib/ente_crypto.dart @@ -0,0 +1,4 @@ +export 'src/crypto.dart'; +export 'src/models/derived_key_result.dart'; +export 'src/models/encryption_result.dart'; +export 'src/models/errors.dart'; diff --git a/mobile/lib/utils/crypto_util.dart b/mobile/plugins/ente_crypto/lib/src/crypto.dart similarity index 98% rename from mobile/lib/utils/crypto_util.dart rename to mobile/plugins/ente_crypto/lib/src/crypto.dart index 52ed85f857..7a3ec6c3c5 100644 --- a/mobile/lib/utils/crypto_util.dart +++ b/mobile/plugins/ente_crypto/lib/src/crypto.dart @@ -2,12 +2,12 @@ import "dart:convert"; import "dart:io"; import 'dart:typed_data'; -import 'package:computer/computer.dart'; -import 'package:flutter_sodium/flutter_sodium.dart'; -import 'package:logging/logging.dart'; -import "package:photos/core/errors.dart"; -import 'package:photos/models/derived_key_result.dart'; -import 'package:photos/models/encryption_result.dart'; +import "package:computer/computer.dart"; +import "package:ente_crypto/src/models/derived_key_result.dart"; +import "package:ente_crypto/src/models/encryption_result.dart"; +import "package:ente_crypto/src/models/errors.dart"; +import "package:flutter_sodium/flutter_sodium.dart"; +import "package:logging/logging.dart"; const int encryptionChunkSize = 4 * 1024 * 1024; final int decryptionChunkSize = diff --git a/mobile/lib/models/derived_key_result.dart b/mobile/plugins/ente_crypto/lib/src/models/derived_key_result.dart similarity index 100% rename from mobile/lib/models/derived_key_result.dart rename to mobile/plugins/ente_crypto/lib/src/models/derived_key_result.dart diff --git a/mobile/lib/models/encryption_result.dart b/mobile/plugins/ente_crypto/lib/src/models/encryption_result.dart similarity index 100% rename from mobile/lib/models/encryption_result.dart rename to mobile/plugins/ente_crypto/lib/src/models/encryption_result.dart diff --git a/mobile/plugins/ente_crypto/lib/src/models/errors.dart b/mobile/plugins/ente_crypto/lib/src/models/errors.dart new file mode 100644 index 0000000000..3d4a186456 --- /dev/null +++ b/mobile/plugins/ente_crypto/lib/src/models/errors.dart @@ -0,0 +1,3 @@ +class KeyDerivationError extends Error {} + +class LoginKeyDerivationError extends Error {} diff --git a/mobile/plugins/ente_crypto/pubspec.lock b/mobile/plugins/ente_crypto/pubspec.lock new file mode 100644 index 0000000000..0807ae8a31 --- /dev/null +++ b/mobile/plugins/ente_crypto/pubspec.lock @@ -0,0 +1,105 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + characters: + dependency: transitive + description: + name: characters + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + url: "https://pub.dev" + source: hosted + version: "1.3.0" + collection: + dependency: "direct main" + description: + name: collection + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + url: "https://pub.dev" + source: hosted + version: "1.18.0" + computer: + dependency: "direct main" + description: + path: "." + ref: HEAD + resolved-ref: "82e365fed8a1a76f6eea0220de98389eed7b0445" + url: "https://github.com/ente-io/computer.git" + source: git + version: "3.2.1" + ffi: + dependency: transitive + description: + name: ffi + sha256: "13a6ccf6a459a125b3fcdb6ec73bd5ff90822e071207c663bfd1f70062d51d18" + url: "https://pub.dev" + source: hosted + version: "1.2.1" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1" + url: "https://pub.dev" + source: hosted + version: "5.0.0" + flutter_sodium: + dependency: "direct main" + description: + name: flutter_sodium + sha256: "0e8c475088f8c9a60fda0ef19ee42cdcb574f8549c8ea809f736ef60e8ad51a6" + url: "https://pub.dev" + source: hosted + version: "0.2.0" + lints: + dependency: transitive + description: + name: lints + sha256: "3315600f3fb3b135be672bf4a178c55f274bebe368325ae18462c89ac1e3b413" + url: "https://pub.dev" + source: hosted + version: "5.0.0" + logging: + dependency: "direct main" + description: + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.dev" + source: hosted + version: "1.3.0" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + url: "https://pub.dev" + source: hosted + version: "0.11.1" + meta: + dependency: transitive + description: + name: meta + sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 + url: "https://pub.dev" + source: hosted + version: "1.15.0" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.99" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" + source: hosted + version: "2.1.4" +sdks: + dart: ">=3.5.0 <4.0.0" + flutter: ">=1.12.0" diff --git a/mobile/plugins/ente_crypto/pubspec.yaml b/mobile/plugins/ente_crypto/pubspec.yaml new file mode 100644 index 0000000000..a3aea1ca8a --- /dev/null +++ b/mobile/plugins/ente_crypto/pubspec.yaml @@ -0,0 +1,20 @@ +name: ente_crypto +version: 0.0.1 +publish_to: none + +environment: + sdk: '>=3.3.0 <4.0.0' + +dependencies: + collection: + computer: + git: "https://github.com/ente-io/computer.git" + flutter: + sdk: flutter + flutter_sodium: ^0.2.0 + logging: ^1.3.0 + +dev_dependencies: + flutter_lints: + +flutter: \ No newline at end of file diff --git a/mobile/plugins/ente_feature_flag/lib/src/model.dart b/mobile/plugins/ente_feature_flag/lib/src/model.dart index 412ef11a39..925b722a7e 100644 --- a/mobile/plugins/ente_feature_flag/lib/src/model.dart +++ b/mobile/plugins/ente_feature_flag/lib/src/model.dart @@ -26,12 +26,36 @@ class RemoteFlags { required this.castUrl, }); + RemoteFlags copyWith({ + bool? enableStripe, + bool? disableCFWorker, + bool? mapEnabled, + bool? faceSearchEnabled, + bool? recoveryKeyVerified, + bool? internalUser, + bool? betaUser, + bool? enableMobMultiPart, + String? castUrl, + }) { + return RemoteFlags( + enableStripe: enableStripe ?? this.enableStripe, + disableCFWorker: disableCFWorker ?? this.disableCFWorker, + mapEnabled: mapEnabled ?? this.mapEnabled, + faceSearchEnabled: faceSearchEnabled ?? this.faceSearchEnabled, + recoveryKeyVerified: recoveryKeyVerified ?? this.recoveryKeyVerified, + internalUser: internalUser ?? this.internalUser, + betaUser: betaUser ?? this.betaUser, + enableMobMultiPart: enableMobMultiPart ?? this.enableMobMultiPart, + castUrl: castUrl ?? this.castUrl, + ); + } + static RemoteFlags defaultValue = RemoteFlags( enableStripe: Platform.isAndroid, disableCFWorker: false, mapEnabled: false, faceSearchEnabled: false, - recoveryKeyVerified: false, + recoveryKeyVerified: true, internalUser: kDebugMode, betaUser: kDebugMode, enableMobMultiPart: false, diff --git a/mobile/plugins/ente_feature_flag/lib/src/service.dart b/mobile/plugins/ente_feature_flag/lib/src/service.dart index c9e1a94628..1ba4195605 100644 --- a/mobile/plugins/ente_feature_flag/lib/src/service.dart +++ b/mobile/plugins/ente_feature_flag/lib/src/service.dart @@ -1,5 +1,6 @@ // ignore_for_file: always_use_package_imports +import "dart:async"; import "dart:convert"; import "dart:developer"; import "dart:io"; @@ -13,10 +14,8 @@ import "model.dart"; class FlagService { final SharedPreferences _prefs; final Dio _enteDio; - late final bool _usingEnteEmail; FlagService(this._prefs, this._enteDio) { - _usingEnteEmail = _prefs.getString("email")?.endsWith("@ente.io") ?? false; Future.delayed(const Duration(seconds: 5), () { _fetch(); }); @@ -39,25 +38,9 @@ class FlagService { } } - Future _fetch() async { - try { - if (!_prefs.containsKey("token")) { - log("token not found, skip", name: "FlagService"); - return; - } - log("fetching feature flags", name: "FlagService"); - final response = await _enteDio.get("/remote-store/feature-flags"); - final remoteFlags = RemoteFlags.fromMap(response.data); - await _prefs.setString("remote_flags", remoteFlags.toJson()); - _flags = remoteFlags; - } catch (e) { - debugPrint("Failed to sync feature flags $e"); - } - } - bool get disableCFWorker => flags.disableCFWorker; - bool get internalUser => flags.internalUser || _usingEnteEmail || kDebugMode; + bool get internalUser => flags.internalUser || kDebugMode; bool get betaUser => flags.betaUser; @@ -75,5 +58,73 @@ class FlagService { bool get enableMobMultiPart => flags.enableMobMultiPart || internalUser; + bool get showSmartMemories => internalUser; + String get castUrl => flags.castUrl; + + Future setMapEnabled(bool isEnabled) async { + await _updateKeyValue("mapEnabled", isEnabled.toString()); + _updateFlags(flags.copyWith(mapEnabled: isEnabled)); + } + + Future setMLConsent(bool isEnabled) async { + await _updateKeyValue("faceSearchEnabled", isEnabled.toString()); + _updateFlags(flags.copyWith(faceSearchEnabled: isEnabled)); + } + + Future setRecoveryKeyVerified(bool isVerified) async { + await _updateKeyValue("recoveryKeyVerified", isVerified.toString()); + _updateFlags(flags.copyWith(recoveryKeyVerified: isVerified)); + } + + Completer? _fetchCompleter; + Future _fetch() async { + if (_fetchCompleter != null) { + await _fetchCompleter!.future; + return; + } + _fetchCompleter = Completer(); + try { + if (!_prefs.containsKey("token")) { + log("token not found, skip", name: "FlagService"); + _fetchCompleter!.complete(); + _fetchCompleter = null; + return; + } + log("fetching feature flags", name: "FlagService"); + final response = await _enteDio.get("/remote-store/feature-flags"); + final remoteFlags = RemoteFlags.fromMap(response.data); + await _prefs.setString("remote_flags", remoteFlags.toJson()); + _flags = remoteFlags; + } catch (e) { + debugPrint("Failed to sync feature flags $e"); + } finally { + _fetchCompleter!.complete(); + _fetchCompleter = null; + } + } + + Future _updateKeyValue(String key, String value) async { + try { + final response = await _enteDio.post( + "/remote-store/update", + data: { + "key": key, + "value": value, + }, + ); + if (response.statusCode != HttpStatus.ok) { + throw Exception("Unexpected state"); + } + } catch (e) { + debugPrint("Failed to set flag for $key $e"); + rethrow; + } + } + + void _updateFlags(RemoteFlags flags) { + _flags = flags; + _prefs.setString("remote_flags", flags.toJson()); + _fetch().ignore(); + } } diff --git a/mobile/plugins/ente_feature_flag/pubspec.lock b/mobile/plugins/ente_feature_flag/pubspec.lock index 4f2db8cd08..e58ff1231c 100644 --- a/mobile/plugins/ente_feature_flag/pubspec.lock +++ b/mobile/plugins/ente_feature_flag/pubspec.lock @@ -1,6 +1,14 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + async: + dependency: transitive + description: + name: async + sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 + url: "https://pub.dev" + source: hosted + version: "2.12.0" characters: dependency: transitive description: @@ -21,10 +29,18 @@ packages: dependency: "direct main" description: name: dio - sha256: "7d328c4d898a61efc3cd93655a0955858e29a0aa647f0f9e02d59b3bb275e2e8" + sha256: "253a18bbd4851fecba42f7343a1df3a9a4c1d31a2c1b37e221086b4fa8c8dbc9" url: "https://pub.dev" source: hosted - version: "4.0.6" + version: "5.8.0+1" + dio_web_adapter: + dependency: transitive + description: + name: dio_web_adapter + sha256: e485c7a39ff2b384fa1d7e09b4e25f755804de8384358049124830b04fc4f93a + url: "https://pub.dev" + source: hosted + version: "2.1.0" ffi: dependency: transitive description: @@ -273,5 +289,5 @@ packages: source: hosted version: "1.0.4" sdks: - dart: ">=3.3.0 <4.0.0" + dart: ">=3.4.0 <4.0.0" flutter: ">=3.19.0" diff --git a/mobile/plugins/ente_feature_flag/pubspec.yaml b/mobile/plugins/ente_feature_flag/pubspec.yaml index 7507d61f1c..6ad2c7626d 100644 --- a/mobile/plugins/ente_feature_flag/pubspec.yaml +++ b/mobile/plugins/ente_feature_flag/pubspec.yaml @@ -7,7 +7,7 @@ environment: dependencies: collection: - dio: ^4.0.6 + dio: ^5.8.0+1 flutter: sdk: flutter shared_preferences: ^2.0.5 diff --git a/mobile/plugins/onnx_dart/pubspec.yaml b/mobile/plugins/onnx_dart/pubspec.yaml index fe7dd8c484..8757519440 100644 --- a/mobile/plugins/onnx_dart/pubspec.yaml +++ b/mobile/plugins/onnx_dart/pubspec.yaml @@ -1,7 +1,7 @@ name: onnx_dart description: "A new Flutter plugin project." version: 0.0.1 -homepage: http://ente.io +homepage: https://ente.io environment: sdk: '>=3.4.3 <4.0.0' diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index ac57cc2ccd..37b487b0ef 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -34,10 +34,10 @@ packages: dependency: "direct main" description: name: android_intent_plus - sha256: "38921ec22ebb3b9a7eb678792cf6fab0b6f458b61b9d327688573449c9b47db3" + sha256: dfc1fd3a577205ae8f11e990fb4ece8c90cceabbee56fcf48e463ecf0bd6aae3 url: "https://pub.dev" source: hosted - version: "5.2.0" + version: "5.3.0" animate_do: dependency: "direct main" description: @@ -82,10 +82,10 @@ packages: dependency: transitive description: name: args - sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a" + sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6 url: "https://pub.dev" source: hosted - version: "2.5.0" + version: "2.6.0" asn1lib: dependency: transitive description: @@ -113,10 +113,11 @@ packages: battery_info: dependency: "direct main" description: - name: battery_info - sha256: "5d5249c87a600a0a20d6b2f5ffdf90d711bccb1bfd3a58e5a6228f270031c680" - url: "https://pub.dev" - source: hosted + path: "." + ref: HEAD + resolved-ref: "95c7a9f0400b968b3bfdc98c8c10a07fb4a62ea2" + url: "https://github.com/ente-io/battery_info" + source: git version: "1.1.1" bip39: dependency: "direct main" @@ -194,10 +195,10 @@ packages: dependency: transitive description: name: built_value - sha256: c7913a9737ee4007efedaffc968c049fd0f3d0e49109e778edc10de9426005cb + sha256: ea90e81dc4a25a043d9bee692d20ed6d1c4a1662a28c03a96417446c093ed6b4 url: "https://pub.dev" source: hosted - version: "8.9.2" + version: "8.9.5" cached_network_image: dependency: "direct main" description: @@ -251,19 +252,19 @@ packages: dependency: "direct main" description: path: "." - ref: forked_video_player_plus - resolved-ref: "2d8908efe9d7533ec76abe2e59444547c4031f28" + ref: mybranched + resolved-ref: "539079ac2758086ef4dfb602a5f8785bf5295fb3" url: "https://github.com/ente-io/chewie.git" source: git - version: "1.7.1" + version: "1.10.0" cli_util: dependency: transitive description: name: cli_util - sha256: c05b7406fdabc7a49a3929d4af76bcaccbbffcbcdcf185b082e1ae07da323d19 + sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c url: "https://pub.dev" source: hosted - version: "0.4.1" + version: "0.4.2" clock: dependency: transitive description: @@ -276,10 +277,10 @@ packages: dependency: transitive description: name: code_builder - sha256: f692079e25e7869c14132d39f223f8eec9830eb76131925143b2129c4bb01b37 + sha256: "0ec10bf4a89e4c613960bf1e8b42c64127021740fb21640c29c909826a5eea3e" url: "https://pub.dev" source: hosted - version: "4.10.0" + version: "4.10.1" collection: dependency: "direct main" description: @@ -301,10 +302,10 @@ packages: dependency: "direct main" description: name: connectivity_plus - sha256: "2056db5241f96cdc0126bd94459fc4cdc13876753768fc7a31c425e50a7177d0" + sha256: "04bf81bb0b77de31557b58d052b24b3eee33f09a6e7a8c68a3e247c7df19ec27" url: "https://pub.dev" source: hosted - version: "6.0.5" + version: "6.1.3" connectivity_plus_platform_interface: dependency: transitive description: @@ -317,18 +318,26 @@ packages: dependency: transitive description: name: convert - sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.1.2" coverage: dependency: transitive description: name: coverage - sha256: c1fb2dce3c0085f39dc72668e85f8e0210ec7de05345821ff58530567df345a5 + sha256: e3493833ea012784c740e341952298f1cc77f1f01b1bbc3eb4eecf6984fb7f43 url: "https://pub.dev" source: hosted - version: "1.9.2" + version: "1.11.1" + cronet_http: + dependency: transitive + description: + name: cronet_http + sha256: "3af9c4d57bf07ef4b307e77b22be4ad61bea19ee6ff65e62184863f3a09f1415" + url: "https://pub.dev" + source: hosted + version: "1.3.2" cross_file: dependency: "direct main" description: @@ -341,18 +350,26 @@ packages: dependency: "direct main" description: name: crypto - sha256: ec30d999af904f33454ba22ed9a86162b35e52b44ac4807d1d93c288041d7d27 + sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" url: "https://pub.dev" source: hosted - version: "3.0.5" + version: "3.0.6" csslib: dependency: transitive description: name: csslib - sha256: "706b5707578e0c1b4b7550f64078f0a0f19dec3f50a178ffae7006b0a9ca58fb" + sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e" url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.0.2" + cupertino_http: + dependency: transitive + description: + name: cupertino_http + sha256: "6fcf79586ad872ddcd6004d55c8c2aab3cdf0337436e8f99837b1b6c30665d0c" + url: "https://pub.dev" + source: hosted + version: "2.0.2" cupertino_icons: dependency: "direct main" description: @@ -381,10 +398,10 @@ packages: dependency: transitive description: name: dbus - sha256: "365c771ac3b0e58845f39ec6deebc76e3276aa9922b0cc60840712094d9047ac" + sha256: "79e0c23480ff85dc68de79e2cd6334add97e48f7f4865d17686dd6ea81a47e8c" url: "https://pub.dev" source: hosted - version: "0.7.10" + version: "0.7.11" defer_pointer: dependency: "direct main" description: @@ -405,18 +422,26 @@ packages: dependency: transitive description: name: device_info_plus_platform_interface - sha256: "282d3cf731045a2feb66abfe61bbc40870ae50a3ed10a4d3d217556c35c8c2ba" + sha256: "0b04e02b30791224b31969eb1b50d723498f402971bff3630bca2ba839bd1ed2" url: "https://pub.dev" source: hosted - version: "7.0.1" + version: "7.0.2" dio: dependency: "direct main" description: name: dio - sha256: "7d328c4d898a61efc3cd93655a0955858e29a0aa647f0f9e02d59b3bb275e2e8" + sha256: "253a18bbd4851fecba42f7343a1df3a9a4c1d31a2c1b37e221086b4fa8c8dbc9" url: "https://pub.dev" source: hosted - version: "4.0.6" + version: "5.8.0+1" + dio_web_adapter: + dependency: transitive + description: + name: dio_web_adapter + sha256: "7586e476d70caecaf1686d21eee7247ea43ef5c345eab9e0cc3583ff13378d78" + url: "https://pub.dev" + source: hosted + version: "2.1.1" dots_indicator: dependency: "direct main" description: @@ -479,6 +504,13 @@ packages: relative: true source: path version: "0.0.1" + ente_crypto: + dependency: "direct main" + description: + path: "plugins/ente_crypto" + relative: true + source: path + version: "0.0.1" ente_feature_flag: dependency: "direct main" description: @@ -490,10 +522,10 @@ packages: dependency: "direct main" description: name: equatable - sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2 + sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7" url: "https://pub.dev" source: hosted - version: "2.0.5" + version: "2.0.7" event_bus: dependency: "direct main" description: @@ -538,10 +570,10 @@ packages: dependency: transitive description: name: extended_image_library - sha256: "9a94ec9314aa206cfa35f16145c3cd6e2c924badcc670eaaca8a3a8063a68cd7" + sha256: e61dafd94400fff6ef7ed1523d445ff3af137f198f3228e4a3107bc5b4bec5d1 url: "https://pub.dev" source: hosted - version: "4.0.5" + version: "4.0.6" fade_indexed_stack: dependency: "direct main" description: @@ -593,11 +625,12 @@ packages: figma_squircle: dependency: "direct main" description: - name: figma_squircle - sha256: "790b91a9505e90d246f6efe2fa065ff7fffe658c7b44fe9b5b20c7b0ad3818c0" - url: "https://pub.dev" - source: hosted - version: "0.5.3" + path: "." + ref: "7cc383b30e96c07acd4e484c1d6731d054f7f6ec" + resolved-ref: "7cc383b30e96c07acd4e484c1d6731d054f7f6ec" + url: "https://github.com/aloisdeniel/figma_squircle.git" + source: git + version: "1.0.0" file: dependency: transitive description: @@ -609,20 +642,19 @@ packages: file_saver: dependency: "direct main" description: - path: "." - ref: HEAD - resolved-ref: "01b2e6b6fe520cfa5d2d91342ccbfbaefa8f6482" - url: "https://github.com/jesims/file_saver.git" - source: git - version: "0.2.9" + name: file_saver + sha256: "017a127de686af2d2fbbd64afea97052d95f2a0f87d19d25b87e097407bf9c1e" + url: "https://pub.dev" + source: hosted + version: "0.2.14" file_selector_linux: dependency: transitive description: name: file_selector_linux - sha256: "712ce7fab537ba532c8febdb1a8f167b32441e74acd68c3ccb2e36dcb52c4ab2" + sha256: "54cbbd957e1156d29548c7d9b9ec0c0ebb6de0a90452198683a7d23aed617a33" url: "https://pub.dev" source: hosted - version: "0.9.3" + version: "0.9.3+2" file_selector_macos: dependency: transitive description: @@ -643,26 +675,18 @@ packages: dependency: transitive description: name: file_selector_windows - sha256: "8f5d2f6590d51ecd9179ba39c64f722edc15226cc93dcc8698466ad36a4a85a4" + sha256: "320fcfb6f33caa90f0b58380489fc5ac05d99ee94b61aa96ec2bff0ba81d3c2b" url: "https://pub.dev" source: hosted - version: "0.9.3+3" + version: "0.9.3+4" fixnum: dependency: transitive description: name: fixnum - sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be url: "https://pub.dev" source: hosted - version: "1.1.0" - fk_user_agent: - dependency: "direct main" - description: - name: fk_user_agent - sha256: fd6c94e120786985a292d12f61422a581f4e851148d5940af38b819357b8ad0d - url: "https://pub.dev" - source: hosted - version: "2.1.0" + version: "1.1.1" flutter: dependency: "direct main" description: flutter @@ -672,10 +696,10 @@ packages: dependency: "direct main" description: name: flutter_animate - sha256: "7c8a6594a9252dad30cc2ef16e33270b6248c4dedc3b3d06c86c4f3f4dc05ae5" + sha256: "7befe2d3252728afb77aecaaea1dec88a89d35b9b1d2eea6d04479e8af9117b5" url: "https://pub.dev" source: hosted - version: "4.5.0" + version: "4.5.2" flutter_cache_manager: dependency: "direct main" description: @@ -725,26 +749,66 @@ packages: dependency: "direct main" description: name: flutter_email_sender - sha256: "9e253c69617f43d4cb5de672e93a7a19c12a21fb6a75e66c6ce7626336c4c1bc" + sha256: d39eb5e91358fc19ec4050da69accec21f9d5b2b6bcf188aa246327b6ca2352c url: "https://pub.dev" source: hosted - version: "5.2.0" + version: "7.0.0" flutter_image_compress: dependency: "direct main" description: name: flutter_image_compress - sha256: "37f1b26399098e5f97b74c1483f534855e7dff68ead6ddaccf747029fb03f29f" + sha256: "51d23be39efc2185e72e290042a0da41aed70b14ef97db362a6b5368d0523b27" url: "https://pub.dev" source: hosted - version: "1.1.3" + version: "2.4.0" + flutter_image_compress_common: + dependency: transitive + description: + name: flutter_image_compress_common + sha256: c5c5d50c15e97dd7dc72ff96bd7077b9f791932f2076c5c5b6c43f2c88607bfb + url: "https://pub.dev" + source: hosted + version: "1.0.6" + flutter_image_compress_macos: + dependency: transitive + description: + name: flutter_image_compress_macos + sha256: "20019719b71b743aba0ef874ed29c50747461e5e8438980dfa5c2031898f7337" + url: "https://pub.dev" + source: hosted + version: "1.0.3" + flutter_image_compress_ohos: + dependency: transitive + description: + name: flutter_image_compress_ohos + sha256: e76b92bbc830ee08f5b05962fc78a532011fcd2041f620b5400a593e96da3f51 + url: "https://pub.dev" + source: hosted + version: "0.0.3" + flutter_image_compress_platform_interface: + dependency: transitive + description: + name: flutter_image_compress_platform_interface + sha256: "579cb3947fd4309103afe6442a01ca01e1e6f93dc53bb4cbd090e8ce34a41889" + url: "https://pub.dev" + source: hosted + version: "1.0.5" + flutter_image_compress_web: + dependency: transitive + description: + name: flutter_image_compress_web + sha256: b9b141ac7c686a2ce7bb9a98176321e1182c9074650e47bb140741a44b6f5a96 + url: "https://pub.dev" + source: hosted + version: "0.1.5" flutter_inappwebview: dependency: "direct main" description: name: flutter_inappwebview - sha256: "93cfcca02bdda4b26cd700cf70d9ddba09d8348e3e8f2857638c23ed23a4fcb4" + sha256: "80092d13d3e29b6227e25b67973c67c7210bd5e35c4b747ca908e31eb71a46d5" url: "https://pub.dev" source: hosted - version: "6.1.4" + version: "6.1.5" flutter_inappwebview_android: dependency: transitive description: @@ -757,10 +821,10 @@ packages: dependency: transitive description: name: flutter_inappwebview_internal_annotations - sha256: "5f80fd30e208ddded7dbbcd0d569e7995f9f63d45ea3f548d8dd4c0b473fb4c8" + sha256: "787171d43f8af67864740b6f04166c13190aa74a1468a1f1f1e9ee5b90c359cd" url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.2.0" flutter_inappwebview_ios: dependency: transitive description: @@ -797,10 +861,10 @@ packages: dependency: transitive description: name: flutter_inappwebview_windows - sha256: "95ebc65aecfa63b2084c822aec6ba0545f0a0afaa3899f2c752ec96c09108db5" + sha256: "8b4d3a46078a2cdc636c4a3d10d10f2a16882f6be607962dbfff8874d1642055" url: "https://pub.dev" source: hosted - version: "0.5.0+2" + version: "0.6.0" flutter_launcher_icons: dependency: "direct main" description: @@ -821,10 +885,10 @@ packages: dependency: "direct main" description: name: flutter_local_notifications - sha256: "49eeef364fddb71515bc78d5a8c51435a68bccd6e4d68e25a942c5e47761ae71" + sha256: "674173fd3c9eda9d4c8528da2ce0ea69f161577495a9cc835a2a4ecd7eadeb35" url: "https://pub.dev" source: hosted - version: "17.2.3" + version: "17.2.4" flutter_local_notifications_linux: dependency: transitive description: @@ -874,10 +938,10 @@ packages: dependency: "direct main" description: name: flutter_native_splash - sha256: aa06fec78de2190f3db4319dd60fdc8d12b2626e93ef9828633928c2dcaea840 + sha256: "7062602e0dbd29141fb8eb19220b5871ca650be5197ab9c1f193a28b17537bc7" url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.4.4" flutter_password_strength: dependency: "direct main" description: @@ -890,34 +954,34 @@ packages: dependency: transitive description: name: flutter_plugin_android_lifecycle - sha256: "9b78450b89f059e96c9ebb355fa6b3df1d6b330436e0b885fb49594c41721398" + sha256: "1c2b787f99bdca1f3718543f81d38aa1b124817dfeb9fb196201bea85b6134bf" url: "https://pub.dev" source: hosted - version: "2.0.23" + version: "2.0.26" flutter_secure_storage: dependency: "direct main" description: name: flutter_secure_storage - sha256: "22dbf16f23a4bcf9d35e51be1c84ad5bb6f627750565edd70dab70f3ff5fff8f" + sha256: "9cad52d75ebc511adfae3d447d5d13da15a55a92c9410e50f67335b6d21d16ea" url: "https://pub.dev" source: hosted - version: "8.1.0" + version: "9.2.4" flutter_secure_storage_linux: dependency: transitive description: name: flutter_secure_storage_linux - sha256: "4d91bfc23047422cbcd73ac684bc169859ee766482517c22172c86596bf1464b" + sha256: bf7404619d7ab5c0a1151d7c4e802edad8f33535abfbeff2f9e1fe1274e2d705 url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.2.2" flutter_secure_storage_macos: dependency: transitive description: name: flutter_secure_storage_macos - sha256: "1693ab11121a5f925bbea0be725abfcfbbcf36c1e29e571f84a0c0f436147a81" + sha256: "6c0a2795a2d1de26ae202a0d78527d163f4acbb11cde4c75c670f3a0fc064247" url: "https://pub.dev" source: hosted - version: "3.1.2" + version: "3.1.3" flutter_secure_storage_platform_interface: dependency: transitive description: @@ -938,10 +1002,10 @@ packages: dependency: transitive description: name: flutter_secure_storage_windows - sha256: "38f9501c7cb6f38961ef0e1eacacee2b2d4715c63cc83fe56449c4d3d0b47255" + sha256: b20b07cb5ed4ed74fc567b78a72936203f587eba460af1df11281c9326cd3709 url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "3.1.2" flutter_shaders: dependency: transitive description: @@ -953,10 +1017,11 @@ packages: flutter_sodium: dependency: "direct main" description: - name: flutter_sodium - sha256: "0e8c475088f8c9a60fda0ef19ee42cdcb574f8549c8ea809f736ef60e8ad51a6" - url: "https://pub.dev" - source: hosted + path: "." + ref: HEAD + resolved-ref: "134ecc72bc09d55500d240df677b8360f140f9ab" + url: "https://github.com/ente-io/flutter_sodium" + source: git version: "0.2.0" flutter_spinkit: dependency: transitive @@ -978,10 +1043,10 @@ packages: dependency: "direct main" description: name: flutter_svg - sha256: "7b4ca6cf3304575fe9c8ec64813c8d02ee41d2afe60bcfe0678bcb5375d596a2" + sha256: c200fd79c918a40c5cd50ea0877fa13f81bdaf6f0a5d3dbcc2a13e3285d6aa1b url: "https://pub.dev" source: hosted - version: "2.0.10+1" + version: "2.0.17" flutter_test: dependency: "direct dev" description: flutter @@ -996,10 +1061,10 @@ packages: dependency: "direct main" description: name: fluttertoast - sha256: "95f349437aeebe524ef7d6c9bde3e6b4772717cf46a0eb6a3ceaddc740b297cc" + sha256: "25e51620424d92d3db3832464774a6143b5053f15e382d8ffbfd40b6e795dcf1" url: "https://pub.dev" source: hosted - version: "8.2.8" + version: "8.2.12" fraction: dependency: "direct main" description: @@ -1041,18 +1106,18 @@ packages: dependency: transitive description: name: glob - sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.3" google_nav_bar: dependency: "direct main" description: name: google_nav_bar - sha256: "1c8e3882fa66ee7b74c24320668276ca23affbd58f0b14a24c1e5590f4d07ab0" + sha256: bb12dd21514ee1b041ab3127673e2fd85e693337df308f7f2b75cd1e8e92eaf4 url: "https://pub.dev" source: hosted - version: "5.0.6" + version: "5.0.7" graphs: dependency: transitive description: @@ -1081,10 +1146,10 @@ packages: dependency: transitive description: name: html - sha256: "3a7812d5bcd2894edf53dfaf8cd640876cf6cef50a8f238745c8b8120ea74d3a" + sha256: "1fc58edeaec4307368c60d59b7e15b9d658b57d7f3125098b6294153c75337ec" url: "https://pub.dev" source: hosted - version: "0.15.4" + version: "0.15.5" html_unescape: dependency: "direct main" description: @@ -1097,10 +1162,10 @@ packages: dependency: "direct main" description: name: http - sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010 + sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f url: "https://pub.dev" source: hosted - version: "1.2.2" + version: "1.3.0" http_client_helper: dependency: transitive description: @@ -1113,10 +1178,10 @@ packages: dependency: transitive description: name: http_multi_server - sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" + sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 url: "https://pub.dev" source: hosted - version: "3.2.1" + version: "3.2.2" http_parser: dependency: transitive description: @@ -1125,14 +1190,22 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.2" + http_profile: + dependency: transitive + description: + name: http_profile + sha256: "7e679e355b09aaee2ab5010915c932cce3f2d1c11c3b2dc177891687014ffa78" + url: "https://pub.dev" + source: hosted + version: "0.1.0" image: dependency: "direct main" description: name: image - sha256: "2237616a36c0d69aef7549ab439b833fb7f9fb9fc861af2cc9ac3eedddd69ca8" + sha256: f31d52537dc417fdcde36088fdf11d191026fd5e4fae742491ebd40e5a8bea7d url: "https://pub.dev" source: hosted - version: "4.2.0" + version: "4.3.0" image_editor: dependency: "direct main" description: @@ -1177,26 +1250,26 @@ packages: dependency: transitive description: name: image_picker_android - sha256: d3e5e00fdfeca8fd4ffb3227001264d449cc8950414c2ff70b0e06b9c628e643 + sha256: "82652a75e3dd667a91187769a6a2cc81bd8c111bbead698d8e938d2b63e5e89a" url: "https://pub.dev" source: hosted - version: "0.8.12+15" + version: "0.8.12+21" image_picker_for_web: dependency: transitive description: name: image_picker_for_web - sha256: "65d94623e15372c5c51bebbcb820848d7bcb323836e12dfdba60b5d3a8b39e50" + sha256: "717eb042ab08c40767684327be06a5d8dbb341fe791d514e4b92c7bbe1b7bb83" url: "https://pub.dev" source: hosted - version: "3.0.5" + version: "3.0.6" image_picker_ios: dependency: transitive description: name: image_picker_ios - sha256: "6703696ad49f5c3c8356d576d7ace84d1faf459afb07accbb0fae780753ff447" + sha256: "05da758e67bc7839e886b3959848aa6b44ff123ab4b28f67891008afe8ef9100" url: "https://pub.dev" source: hosted - version: "0.8.12" + version: "0.8.12+2" image_picker_linux: dependency: transitive description: @@ -1209,18 +1282,18 @@ packages: dependency: transitive description: name: image_picker_macos - sha256: "3f5ad1e8112a9a6111c46d0b57a7be2286a9a07fc6e1976fdf5be2bd31d4ff62" + sha256: "1b90ebbd9dcf98fb6c1d01427e49a55bd96b5d67b8c67cf955d60a5de74207c1" url: "https://pub.dev" source: hosted - version: "0.2.1+1" + version: "0.2.1+2" image_picker_platform_interface: dependency: transitive description: name: image_picker_platform_interface - sha256: "9ec26d410ff46f483c5519c29c02ef0e02e13a543f882b152d4bfd2f06802f80" + sha256: "886d57f0be73c4b140004e78b9f28a8914a09e50c2d816bdd0520051a71236a0" url: "https://pub.dev" source: hosted - version: "2.10.0" + version: "2.10.1" image_picker_windows: dependency: transitive description: @@ -1246,20 +1319,28 @@ packages: dependency: "direct dev" description: name: intl_utils - sha256: c2b1f5c72c25512cbeef5ab015c008fc50fe7e04813ba5541c25272300484bf4 + sha256: "35f9a55004871f241e24b07465cf402914992d8549a60205ee0816576a8ddee7" url: "https://pub.dev" source: hosted - version: "2.8.7" + version: "2.8.8" io: dependency: transitive description: name: io - sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" + sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b url: "https://pub.dev" source: hosted - version: "1.0.4" - js: + version: "1.0.5" + jni: dependency: transitive + description: + name: jni + sha256: f377c585ea9c08d48b427dc2e03780af2889d1bb094440da853c6883c1acba4b + url: "https://pub.dev" + source: hosted + version: "0.10.1" + js: + dependency: "direct overridden" description: name: js sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 @@ -1278,10 +1359,10 @@ packages: dependency: "direct dev" description: name: json_serializable - sha256: ea1432d167339ea9b5bb153f0571d0039607a873d6e04e0117af043f14a1fd4b + sha256: c2fcb3920cf2b6ae6845954186420fca40bc0a8abcc84903b7801f17d7050d7c url: "https://pub.dev" source: hosted - version: "6.8.0" + version: "6.9.0" latlong2: dependency: "direct main" description: @@ -1358,18 +1439,18 @@ packages: dependency: "direct main" description: name: local_auth_android - sha256: "6763aaf8965f21822624cb2fd3c03d2a8b3791037b5efb0fe4b13e110f5afc92" + sha256: "8bba79f4f0f7bc812fce2ca20915d15618c37721246ba6c3ef2aa7a763a90cf2" url: "https://pub.dev" source: hosted - version: "1.0.46" + version: "1.0.47" local_auth_darwin: dependency: transitive description: name: local_auth_darwin - sha256: "6d2950da311d26d492a89aeb247c72b4653ddc93601ea36a84924a396806d49c" + sha256: "630996cd7b7f28f5ab92432c4b35d055dd03a747bc319e5ffbb3c4806a3e50d2" url: "https://pub.dev" source: hosted - version: "1.4.1" + version: "1.4.3" local_auth_ios: dependency: "direct main" description: @@ -1398,18 +1479,18 @@ packages: dependency: transitive description: name: logger - sha256: "697d067c60c20999686a0add96cf6aba723b3aa1f83ecf806a8097231529ec32" + sha256: be4b23575aac7ebf01f225a241eb7f6b5641eeaf43c6a8613510fc2f8cf187d1 url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.5.0" logging: dependency: "direct main" description: name: logging - sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.0" lottie: dependency: "direct main" description: @@ -1430,10 +1511,10 @@ packages: dependency: "direct main" description: name: maps_launcher - sha256: "57ba3c31db96e30f58c23fcb22a1fac6accc5683535b2cf344c534bbb9f8f910" + sha256: dac4c609720211fa6336b5903d917fe45e545c6b5665978efc3db2a3f436b1ae url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "3.0.0+1" matcher: dependency: transitive description: @@ -1454,18 +1535,19 @@ packages: dependency: "direct main" description: path: "." - ref: deeplink_fixes - resolved-ref: "520a200a6f76dece843f3e0eedb9b1f89c805cdf" + ref: HEAD + resolved-ref: "23a38887fa6b61330452555f8e686c3a3a1c91a2" url: "https://github.com/ente-io/media_extension.git" source: git version: "1.0.2" media_kit: dependency: "direct main" description: - name: media_kit - sha256: "1f1deee148533d75129a6f38251ff8388e33ee05fc2d20a6a80e57d6051b7b62" - url: "https://pub.dev" - source: hosted + path: media_kit + ref: HEAD + resolved-ref: d2145a50f68394096845915a28874341fbf5b3fe + url: "https://github.com/media-kit/media-kit" + source: git version: "1.1.11" media_kit_libs_android_video: dependency: transitive @@ -1478,10 +1560,11 @@ packages: media_kit_libs_ios_video: dependency: "direct main" description: - name: media_kit_libs_ios_video - sha256: b5382994eb37a4564c368386c154ad70ba0cc78dacdd3fb0cd9f30db6d837991 - url: "https://pub.dev" - source: hosted + path: "libs/ios/media_kit_libs_ios_video" + ref: HEAD + resolved-ref: d2145a50f68394096845915a28874341fbf5b3fe + url: "https://github.com/media-kit/media-kit" + source: git version: "1.1.4" media_kit_libs_linux: dependency: transitive @@ -1502,10 +1585,11 @@ packages: media_kit_libs_video: dependency: "direct main" description: - name: media_kit_libs_video - sha256: "20bb4aefa8fece282b59580e1cd8528117297083a6640c98c2e98cfc96b93288" - url: "https://pub.dev" - source: hosted + path: "libs/universal/media_kit_libs_video" + ref: HEAD + resolved-ref: d2145a50f68394096845915a28874341fbf5b3fe + url: "https://github.com/media-kit/media-kit" + source: git version: "1.0.5" media_kit_libs_windows_video: dependency: transitive @@ -1515,21 +1599,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.10" - media_kit_native_event_loop: - dependency: transitive - description: - name: media_kit_native_event_loop - sha256: "7d82e3b3e9ded5c35c3146c5ba1da3118d1dd8ac3435bac7f29f458181471b40" - url: "https://pub.dev" - source: hosted - version: "1.0.9" media_kit_video: dependency: "direct main" description: - name: media_kit_video - sha256: "2cc3b966679963ba25a4ce5b771e532a521ebde7c6aa20e9802bec95d9916c8f" - url: "https://pub.dev" - source: hosted + path: media_kit_video + ref: HEAD + resolved-ref: d2145a50f68394096845915a28874341fbf5b3fe + url: "https://github.com/media-kit/media-kit" + source: git version: "1.2.5" meta: dependency: transitive @@ -1551,10 +1628,10 @@ packages: dependency: transitive description: name: mime - sha256: "801fd0b26f14a4a58ccb09d5892c3fbdeff209594300a542492cf13fba9d247a" + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" url: "https://pub.dev" source: hosted - version: "1.0.6" + version: "2.0.0" ml_linalg: dependency: "direct main" description: @@ -1576,10 +1653,10 @@ packages: description: path: "." ref: HEAD - resolved-ref: f1ec3d35d5cee2ec1c098b5ca276f311a96d800a + resolved-ref: ddf5eba70103c2add7c20010430d9db654ddf95c url: "https://github.com/ente-io/motion_photo.git" source: git - version: "0.0.6" + version: "1.0.0" motion_sensors: dependency: transitive description: @@ -1594,26 +1671,27 @@ packages: description: path: "." ref: HEAD - resolved-ref: fc483093e065ed847de81df2af0651a394ffc75e + resolved-ref: "7814e2c61ee1fa74cef73b946eb08519c35bdaa5" url: "https://github.com/ente-io/motionphoto.git" source: git version: "0.0.1" move_to_background: dependency: "direct main" description: - name: move_to_background - sha256: "00caad17a6ce149910777131503f43f8ed80025681f94684e3a6a87d979b914c" - url: "https://pub.dev" - source: hosted + path: "." + ref: HEAD + resolved-ref: "3b862fea9665a50e59bcce96cab6997cc7c0cb9e" + url: "https://github.com/ente-io/move_to_background.git" + source: git version: "1.0.2" multicast_dns: dependency: transitive description: name: multicast_dns - sha256: "982c4cc4cda5f98dd477bddfd623e8e4bd1014e7dbf9e7b05052e14a5b550b99" + sha256: "0a568c8411ab0979ab8cd4af1c29b6d316d854ab81592463ccceb92b35fde813" url: "https://pub.dev" source: hosted - version: "0.3.2+7" + version: "0.3.2+8" nanoid: dependency: "direct main" description: @@ -1622,15 +1700,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + native_dio_adapter: + dependency: "direct main" + description: + name: native_dio_adapter + sha256: "7420bc9517b2abe09810199a19924617b45690a44ecfb0616ac9babc11875c03" + url: "https://pub.dev" + source: hosted + version: "1.4.0" native_video_player: dependency: "direct main" description: - path: "." - ref: notify_if_video_isnt_playable - resolved-ref: dd09ee801939372ca3d1a504a9dbd576d4bf6dcf - url: "https://github.com/ashilkn/native_video_player.git" - source: git - version: "1.3.1" + name: native_video_player + sha256: "571d2ddb9ce297a653ca69ced40e30135c6a59c5a9be9a38e3b370dd6e4bdb0e" + url: "https://pub.dev" + source: hosted + version: "3.0.0-dev.4" nested: dependency: transitive description: @@ -1655,6 +1740,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.2" + objective_c: + dependency: transitive + description: + name: objective_c + sha256: "62e79ab8c3ed6f6a340ea50dd48d65898f5d70425d404f0d99411f6e56e04584" + url: "https://pub.dev" + source: hosted + version: "4.1.0" octo_image: dependency: transitive description: @@ -1682,43 +1775,44 @@ packages: open_mail_app: dependency: "direct main" description: - name: open_mail_app - sha256: ffc93edc0590ef4586136f505187b25014a336fe0374ad3cc528e727b3f2e726 - url: "https://pub.dev" - source: hosted - version: "0.4.5" + path: "." + ref: HEAD + resolved-ref: "83740bf55dac38a0be01c863609cf900c4d1aab1" + url: "https://github.com/ente-io/open-mail-app-flutter.git" + source: git + version: "0.4.4" package_config: dependency: transitive description: name: package_config - sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" + sha256: "92d4488434b520a62570293fbd33bb556c7d49230791c1b4bbd973baf6d2dc67" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" package_info_plus: dependency: "direct main" description: name: package_info_plus - sha256: "7e76fad405b3e4016cd39d08f455a4eb5199723cf594cd1b8916d47140d93017" + sha256: "7976bfe4c583170d6cdc7077e3237560b364149fcd268b5f53d95a991963b191" url: "https://pub.dev" source: hosted - version: "4.2.0" + version: "8.3.0" package_info_plus_platform_interface: dependency: transitive description: name: package_info_plus_platform_interface - sha256: "9bc8ba46813a4cc42c66ab781470711781940780fd8beddd0c3da62506d3a6c6" + sha256: "6c935fb612dff8e3cc9632c2b301720c77450a126114126ffaafe28d2e87956c" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.2.0" page_transition: dependency: "direct main" description: name: page_transition - sha256: dee976b1f23de9bbef5cd512fe567e9f6278caee11f5eaca9a2115c19dc49ef6 + sha256: "9d2a780d7d68b53ae82fbcc43e06a16195e6775e9aae40e55dc0cbb593460f9d" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.2.1" panorama: dependency: "direct main" description: @@ -1756,34 +1850,34 @@ packages: dependency: transitive description: name: path_parsing - sha256: e3e67b1629e6f7e8100b367d3db6ba6af4b1f0bb80f64db18ef1fbabd2fa9ccf + sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca" url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.1.0" path_provider: dependency: "direct main" description: name: path_provider - sha256: fec0d61223fba3154d87759e3cc27fe2c8dc498f6386c6d6fc80d1afdd1bf378 + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.1.5" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: c464428172cb986b758c6d1724c603097febb8fb855aa265aeecc9280c294d4a + sha256: "4adf4fd5423ec60a29506c76581bc05854c55e3a0b72d35bb28d661c9686edf2" url: "https://pub.dev" source: hosted - version: "2.2.12" + version: "2.2.15" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16 + sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.4.1" path_provider_linux: dependency: transitive description: @@ -1820,42 +1914,42 @@ packages: dependency: "direct main" description: name: permission_handler - sha256: "18bf33f7fefbd812f37e72091a15575e72d5318854877e0e4035a24ac1113ecb" + sha256: "59adad729136f01ea9e35a48f5d1395e25cba6cea552249ddbe9cf950f5d7849" url: "https://pub.dev" source: hosted - version: "11.3.1" + version: "11.4.0" permission_handler_android: dependency: transitive description: name: permission_handler_android - sha256: "76e4ab092c1b240d31177bb64d2b0bea43f43d0e23541ec866151b9f7b2490fa" + sha256: d3971dcdd76182a0c198c096b5db2f0884b0d4196723d21a866fc4cdea057ebc url: "https://pub.dev" source: hosted - version: "12.0.12" + version: "12.1.0" permission_handler_apple: dependency: transitive description: name: permission_handler_apple - sha256: e6f6d73b12438ef13e648c4ae56bd106ec60d17e90a59c4545db6781229082a0 + sha256: f84a188e79a35c687c132a0a0556c254747a08561e99ab933f12f6ca71ef3c98 url: "https://pub.dev" source: hosted - version: "9.4.5" + version: "9.4.6" permission_handler_html: dependency: transitive description: name: permission_handler_html - sha256: af26edbbb1f2674af65a8f4b56e1a6f526156bc273d0e65dd8075fab51c78851 + sha256: "38f000e83355abb3392140f6bc3030660cfaef189e1f87824facb76300b4ff24" url: "https://pub.dev" source: hosted - version: "0.1.3+2" + version: "0.1.3+5" permission_handler_platform_interface: dependency: transitive description: name: permission_handler_platform_interface - sha256: e9c8eadee926c4532d0305dff94b85bf961f16759c3af791486613152af4b4f9 + sha256: eb99b295153abce5d683cac8c02e22faab63e50679b937fa1bf67d58bb282878 url: "https://pub.dev" source: hosted - version: "4.2.3" + version: "4.3.0" permission_handler_windows: dependency: transitive description: @@ -1900,10 +1994,10 @@ packages: dependency: "direct main" description: name: pinput - sha256: "7bf9aa7d0eeb3da9f7d49d2087c7bc7d36cd277d2e94cc31c6da52e1ebb048d0" + sha256: "8a73be426a91fefec90a7f130763ca39772d547e92f19a827cf4aa02e323d35a" url: "https://pub.dev" source: hosted - version: "5.0.0" + version: "5.0.1" platform: dependency: transitive description: @@ -1947,10 +2041,11 @@ packages: privacy_screen: dependency: "direct main" description: - name: privacy_screen - sha256: b80297d2726d96e8a8341149e81a415302755f02d3af7c05c820d9e191bbfbee - url: "https://pub.dev" - source: hosted + path: "." + ref: "pb/FIX-obsolete-android-plugin" + resolved-ref: "949de7b6beedf163507d649a71b1f55bc8f083ca" + url: "https://github.com/ente-io/privacy_screen.git" + source: git version: "0.0.6" process: dependency: transitive @@ -1988,18 +2083,18 @@ packages: dependency: transitive description: name: pub_semver - sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.2.0" pubspec_parse: dependency: transitive description: name: pubspec_parse - sha256: c799b721d79eb6ee6fa56f00c04b472dcd44a30d258fac2174a6ec57302678f8 + sha256: "81876843eb50dc2e1e5b151792c9a985c5ed2536914115ed04e9c8528f6647b0" url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.0" quiver: dependency: "direct main" description: @@ -2011,11 +2106,12 @@ packages: receive_sharing_intent: dependency: "direct main" description: - name: receive_sharing_intent - sha256: f127989f8662ea15e193bd1e10605e5a0ab6bb92dffd51f3ce002feb0ce24c93 - url: "https://pub.dev" - source: hosted - version: "1.8.0" + path: "." + ref: HEAD + resolved-ref: "2cea396843cd3ab1b5ec4334be4233864637874e" + url: "https://github.com/KasemJaffer/receive_sharing_intent.git" + source: git + version: "1.8.1" rxdart: dependency: transitive description: @@ -2028,58 +2124,26 @@ packages: dependency: transitive description: name: safe_local_storage - sha256: ede4eb6cb7d88a116b3d3bf1df70790b9e2038bc37cb19112e381217c74d9440 + sha256: e9a21b6fec7a8aa62cc2585ff4c1b127df42f3185adbd2aca66b47abe2e80236 url: "https://pub.dev" source: hosted - version: "1.0.2" - screen_brightness: - dependency: transitive - description: - name: screen_brightness - sha256: ed8da4a4511e79422fc1aa88138e920e4008cd312b72cdaa15ccb426c0faaedd - url: "https://pub.dev" - source: hosted - version: "0.2.2+1" + version: "2.0.1" screen_brightness_android: dependency: transitive description: name: screen_brightness_android - sha256: "3df10961e3a9e968a5e076fe27e7f4741fa8a1d3950bdeb48cf121ed529d0caf" + sha256: "6ba1b5812f66c64e9e4892be2d36ecd34210f4e0da8bdec6a2ea34f1aa42683e" url: "https://pub.dev" source: hosted - version: "0.1.0+2" - screen_brightness_ios: - dependency: transitive - description: - name: screen_brightness_ios - sha256: "99adc3ca5490b8294284aad5fcc87f061ad685050e03cf45d3d018fe398fd9a2" - url: "https://pub.dev" - source: hosted - version: "0.1.0" - screen_brightness_macos: - dependency: transitive - description: - name: screen_brightness_macos - sha256: "64b34e7e3f4900d7687c8e8fb514246845a73ecec05ab53483ed025bd4a899fd" - url: "https://pub.dev" - source: hosted - version: "0.1.0+1" + version: "2.1.1" screen_brightness_platform_interface: dependency: transitive description: name: screen_brightness_platform_interface - sha256: b211d07f0c96637a15fb06f6168617e18030d5d74ad03795dd8547a52717c171 + sha256: "737bd47b57746bc4291cab1b8a5843ee881af499514881b0247ec77447ee769c" url: "https://pub.dev" source: hosted - version: "0.1.0" - screen_brightness_windows: - dependency: transitive - description: - name: screen_brightness_windows - sha256: "9261bf33d0fc2707d8cf16339ce25768100a65e70af0fcabaf032fc12408ba86" - url: "https://pub.dev" - source: hosted - version: "0.1.3" + version: "2.1.0" screenshot: dependency: "direct main" description: @@ -2100,58 +2164,58 @@ packages: dependency: "direct main" description: name: sentry - sha256: "033287044a6644a93498969449d57c37907e56f5cedb17b88a3ff20a882261dd" + sha256: "90c2f956c146bcc9c4843406dd4a65d08b25575828dc2ad51de0ca5cd713209f" url: "https://pub.dev" source: hosted - version: "8.9.0" + version: "8.13.2" sentry_flutter: dependency: "direct main" description: name: sentry_flutter - sha256: "3780b5a0bb6afd476857cfbc6c7444d969c29a4d9bd1aa5b6960aa76c65b737a" + sha256: ee6b41956ad570706bf5c2489915d71d75522d154200c0df24be2c4e5654ca21 url: "https://pub.dev" source: hosted - version: "8.9.0" + version: "8.13.2" share_plus: dependency: "direct main" description: name: share_plus - sha256: "468c43f285207c84bcabf5737f33b914ceb8eb38398b91e5e3ad1698d1b72a52" + sha256: fce43200aa03ea87b91ce4c3ac79f0cecd52e2a7a56c7a4185023c271fbfa6da url: "https://pub.dev" source: hosted - version: "10.0.2" + version: "10.1.4" share_plus_platform_interface: dependency: transitive description: name: share_plus_platform_interface - sha256: "6ababf341050edff57da8b6990f11f4e99eaba837865e2e6defe16d039619db5" + sha256: cc012a23fc2d479854e6c80150696c4a5f5bb62cb89af4de1c505cf78d0a5d0b url: "https://pub.dev" source: hosted - version: "5.0.0" + version: "5.0.2" shared_preferences: dependency: "direct main" description: name: shared_preferences - sha256: "746e5369a43170c25816cc472ee016d3a66bc13fcf430c0bc41ad7b4b2922051" + sha256: "846849e3e9b68f3ef4b60c60cf4b3e02e9321bc7f4d8c4692cf87ffa82fc8a3a" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.5.2" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: "3b9febd815c9ca29c9e3520d50ec32f49157711e143b7a4ca039eb87e8ade5ab" + sha256: "9f9f3d372d4304723e6136663bb291c0b93f5e4c8a4a6314347f481a33bda2b1" url: "https://pub.dev" source: hosted - version: "2.3.3" + version: "2.4.7" shared_preferences_foundation: dependency: transitive description: name: shared_preferences_foundation - sha256: "07e050c7cd39bad516f8d64c455f04508d09df104be326d8c02551590a0d513d" + sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03" url: "https://pub.dev" source: hosted - version: "2.5.3" + version: "2.5.4" shared_preferences_linux: dependency: transitive description: @@ -2172,10 +2236,10 @@ packages: dependency: transitive description: name: shared_preferences_web - sha256: d2ca4132d3946fec2184261726b355836a82c33d7d5b67af32692aff18a4684e + sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.4.3" shared_preferences_windows: dependency: transitive description: @@ -2212,10 +2276,10 @@ packages: dependency: transitive description: name: shelf_web_socket - sha256: "073c147238594ecd0d193f3456a5fe91c4b0abbcc68bf5cd95b36c4e194ac611" + sha256: cc36c297b52866d203dbf9332263c94becc2fe0ceaa9681d07b6ef9807023b67 url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.0.1" simple_cluster: dependency: "direct main" description: @@ -2241,10 +2305,10 @@ packages: dependency: transitive description: name: source_helper - sha256: "6adebc0006c37dd63fe05bca0a929b99f06402fc95aa35bf36d67f5c06de01fd" + sha256: "86d247119aedce8e63f4751bd9626fc9613255935558447569ad42f9f5b48b3c" url: "https://pub.dev" source: hosted - version: "1.3.4" + version: "1.3.5" source_map_stack_trace: dependency: transitive description: @@ -2257,10 +2321,10 @@ packages: dependency: transitive description: name: source_maps - sha256: "708b3f6b97248e5781f493b765c3337db11c5d2c81c3094f10904bfa8004c703" + sha256: "190222579a448b03896e0ca6eca5998fa810fda630c1d65e2f78b3f638f54812" url: "https://pub.dev" source: hosted - version: "0.10.12" + version: "0.10.13" source_span: dependency: transitive description: @@ -2281,10 +2345,10 @@ packages: dependency: "direct main" description: name: sqflite - sha256: "79a297dc3cc137e758c6a4baf83342b039e5a6d2436fcdf3f96a00adaaf2ad62" + sha256: "2d7299468485dca85efeeadf5d38986909c5eb0cd71fd3db2c2f000e6c9454bb" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.4.1" sqflite_android: dependency: transitive description: @@ -2297,18 +2361,18 @@ packages: dependency: transitive description: name: sqflite_common - sha256: "4468b24876d673418a7b7147e5a08a715b4998a7ae69227acafaab762e0e5490" + sha256: "761b9740ecbd4d3e66b8916d784e581861fd3c3553eda85e167bc49fdb68f709" url: "https://pub.dev" source: hosted - version: "2.5.4+5" + version: "2.5.4+6" sqflite_darwin: dependency: transitive description: name: sqflite_darwin - sha256: "769733dddf94622d5541c73e4ddc6aa7b252d865285914b6fcd54a63c4b4f027" + sha256: "22adfd9a2c7d634041e96d6241e6e1c8138ca6817018afc5d443fef91dcefa9c" url: "https://pub.dev" source: hosted - version: "2.4.1-1" + version: "2.4.1+1" sqflite_migration: dependency: "direct main" description: @@ -2329,18 +2393,18 @@ packages: dependency: transitive description: name: sqlite3 - sha256: "45f168ae2213201b54e09429ed0c593dc2c88c924a1488d6f9c523a255d567cb" + sha256: "310af39c40dd0bb2058538333c9d9840a2725ae0b9f77e4fd09ad6696aa8f66e" url: "https://pub.dev" source: hosted - version: "2.4.6" + version: "2.7.5" sqlite3_flutter_libs: dependency: "direct main" description: name: sqlite3_flutter_libs - sha256: "62bbb4073edbcdf53f40c80775f33eea01d301b7b81417e5b3fb7395416258c1" + sha256: "7adb4cc96dc08648a5eb1d80a7619070796ca6db03901ff2b6dcb15ee30468f3" url: "https://pub.dev" source: hosted - version: "0.5.24" + version: "0.5.31" sqlite_async: dependency: "direct main" description: @@ -2377,10 +2441,10 @@ packages: dependency: transitive description: name: stream_transform - sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" + sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" string_scanner: dependency: transitive description: @@ -2433,10 +2497,10 @@ packages: dependency: "direct main" description: name: system_info_plus - sha256: b915c811c6605b802f3988859bc2bb79c95f735762a75b5451741f7a2b949d1b + sha256: df94187e95527f9cb459e6a9f6e0b1ea20c157d8029bc233de34b3c1e17e1c48 url: "https://pub.dev" source: hosted - version: "0.0.5" + version: "0.0.6" term_glyph: dependency: transitive description: @@ -2481,10 +2545,10 @@ packages: dependency: transitive description: name: timing - sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32" + sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe" url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.0.2" transparent_image: dependency: transitive description: @@ -2505,25 +2569,26 @@ packages: dependency: transitive description: name: typed_data - sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 url: "https://pub.dev" source: hosted - version: "1.3.2" + version: "1.4.0" ua_client_hints: dependency: "direct main" description: name: ua_client_hints - sha256: dfea54a1b4d259c057d0f33f198094cf4e09e1a21d347baadbe6dbd3d820c0d4 + sha256: "1b8759a46bfeab355252881df27f2604c01bded86aa2b578869fb1b638b23118" url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.4.1" uni_links: dependency: "direct main" description: - name: uni_links - sha256: "051098acfc9e26a9fde03b487bef5d3d228ca8f67693480c6f33fd4fbb8e2b6e" - url: "https://pub.dev" - source: hosted + path: uni_links + ref: HEAD + resolved-ref: b9168d982885bf64625154d6e6f8d68b92b98b49 + url: "https://github.com/ente-io/uni_links.git" + source: git version: "0.5.1" uni_links_platform_interface: dependency: transitive @@ -2569,50 +2634,50 @@ packages: dependency: transitive description: name: uri_parser - sha256: "6543c9fd86d2862fac55d800a43e67c0dcd1a41677cb69c2f8edfe73bbcf1835" + sha256: ff4d2c720aca3f4f7d5445e23b11b2d15ef8af5ddce5164643f38ff962dcb270 url: "https://pub.dev" source: hosted - version: "2.0.2" + version: "3.0.0" url_launcher: dependency: "direct main" description: name: url_launcher - sha256: "21b704ce5fa560ea9f3b525b43601c678728ba46725bab9b01187b4831377ed3" + sha256: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603" url: "https://pub.dev" source: hosted - version: "6.3.0" + version: "6.3.1" url_launcher_android: dependency: transitive description: name: url_launcher_android - sha256: "8fc3bae0b68c02c47c5c86fa8bfa74471d42687b0eded01b78de87872db745e2" + sha256: "6fc2f56536ee873eeb867ad176ae15f304ccccc357848b351f6f0d8d4a40d193" url: "https://pub.dev" source: hosted - version: "6.3.12" + version: "6.3.14" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: e43b677296fadce447e987a2f519dcf5f6d1e527dc35d01ffab4fff5b8a7063e + sha256: "16a513b6c12bb419304e72ea0ae2ab4fed569920d1c7cb850263fe3acc824626" url: "https://pub.dev" source: hosted - version: "6.3.1" + version: "6.3.2" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - sha256: e2b9622b4007f97f504cd64c0128309dfb978ae66adbe944125ed9e1750f06af + sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935" url: "https://pub.dev" source: hosted - version: "3.2.0" + version: "3.2.1" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - sha256: "769549c999acdb42b8bcfa7c43d72bf79a382ca7441ab18a808e101149daf672" + sha256: "17ba2000b847f334f16626a574c702b196723af2a289e7a93ffcb79acff855c2" url: "https://pub.dev" source: hosted - version: "3.2.1" + version: "3.2.2" url_launcher_platform_interface: dependency: transitive description: @@ -2633,10 +2698,10 @@ packages: dependency: transitive description: name: url_launcher_windows - sha256: "49c10f879746271804767cb45551ec5592cdab00ee105c06dddde1a98f73b185" + sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77" url: "https://pub.dev" source: hosted - version: "3.1.2" + version: "3.1.4" utility: dependency: transitive description: @@ -2657,26 +2722,26 @@ packages: dependency: transitive description: name: vector_graphics - sha256: "32c3c684e02f9bc0afb0ae0aa653337a2fe022e8ab064bcd7ffda27a74e288e3" + sha256: "44cc7104ff32563122a929e4620cf3efd584194eec6d1d913eb5ba593dbcf6de" url: "https://pub.dev" source: hosted - version: "1.1.11+1" + version: "1.1.18" vector_graphics_codec: dependency: transitive description: name: vector_graphics_codec - sha256: c86987475f162fadff579e7320c7ddda04cd2fdeffbe1129227a85d9ac9e03da + sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146" url: "https://pub.dev" source: hosted - version: "1.1.11+1" + version: "1.1.13" vector_graphics_compiler: dependency: transitive description: name: vector_graphics_compiler - sha256: "12faff3f73b1741a36ca7e31b292ddeb629af819ca9efe9953b70bd63fc8cd81" + sha256: "1b4b9e706a10294258727674a340ae0d6e64a7231980f9f9a3d12e4b42407aad" url: "https://pub.dev" source: hosted - version: "1.1.11+1" + version: "1.1.16" vector_math: dependency: transitive description: @@ -2691,7 +2756,7 @@ packages: path: "." ref: HEAD resolved-ref: "1eeb18e2b1ce36bd8ca70178c0d4b485e9982257" - url: "https://github.com/prateekmedia/video_editor.git" + url: "https://github.com/ente-io/video_editor_fork.git" source: git version: "3.0.0" video_player: @@ -2707,49 +2772,42 @@ packages: dependency: transitive description: name: video_player_android - sha256: ae5287ca367e206eb74d7b3dc1ce0b8912ab9a3fc0597b6a101a0a5239f229d3 + sha256: "391e092ba4abe2f93b3e625bd6b6a6ec7d7414279462c1c0ee42b5ab8d0a0898" url: "https://pub.dev" source: hosted - version: "2.7.9" + version: "2.7.16" video_player_avfoundation: dependency: transitive description: name: video_player_avfoundation - sha256: cd5ab8a8bc0eab65ab0cea40304097edc46da574c8c1ecdee96f28cd8ef3792f + sha256: "84b4752745eeccb6e75865c9aab39b3d28eb27ba5726d352d45db8297fbd75bc" url: "https://pub.dev" source: hosted - version: "2.6.2" - video_player_media_kit: - dependency: "direct main" - description: - name: video_player_media_kit - sha256: eadf78b85d0ecc6f65bb5ca84c5ad9546a8609c6c0ee207e81673f7969461f3b - url: "https://pub.dev" - source: hosted - version: "1.0.5" + version: "2.7.0" video_player_platform_interface: dependency: transitive description: name: video_player_platform_interface - sha256: "229d7642ccd9f3dc4aba169609dd6b5f3f443bb4cc15b82f7785fcada5af9bbb" + sha256: df534476c341ab2c6a835078066fc681b8265048addd853a1e3c78740316a844 url: "https://pub.dev" source: hosted - version: "6.2.3" + version: "6.3.0" video_player_web: dependency: transitive description: name: video_player_web - sha256: "6dcdd298136523eaf7dfc31abaf0dfba9aa8a8dbc96670e87e9d42b6f2caf774" + sha256: "3ef40ea6d72434edbfdba4624b90fd3a80a0740d260667d91e7ecd2d79e13476" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.3.4" video_thumbnail: dependency: "direct main" description: - name: video_thumbnail - sha256: "3455c189d3f0bb4e3fc2236475aa84fe598b9b2d0e08f43b9761f5bc44210016" - url: "https://pub.dev" - source: hosted + path: "." + ref: HEAD + resolved-ref: ba086b927621434d1bbd73eeffbe7504d7abebfa + url: "https://github.com/ente-io/video_thumbnail_fork.git" + source: git version: "0.5.3" visibility_detector: dependency: "direct main" @@ -2771,50 +2829,51 @@ packages: dependency: transitive description: name: volume_controller - sha256: c71d4c62631305df63b72da79089e078af2659649301807fa746088f365cb48e + sha256: "30863a51338db47fe16f92902b1a6c4ee5e15c9287b46573d7c2eb6be1f197d2" url: "https://pub.dev" source: hosted - version: "2.0.8" + version: "3.3.1" wakelock_plus: dependency: "direct main" description: name: wakelock_plus - sha256: f268ca2116db22e57577fb99d52515a24bdc1d570f12ac18bb762361d43b043d + sha256: "36c88af0b930121941345306d259ec4cc4ecca3b151c02e3a9e71aede83c615e" url: "https://pub.dev" source: hosted - version: "1.1.4" + version: "1.2.10" wakelock_plus_platform_interface: dependency: transitive description: name: wakelock_plus_platform_interface - sha256: "422d1cdbb448079a8a62a5a770b69baa489f8f7ca21aef47800c726d404f9d16" + sha256: "70e780bc99796e1db82fe764b1e7dcb89a86f1e5b3afb1db354de50f2e41eb7a" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.2.2" wallpaper_manager_flutter: dependency: "direct main" description: - name: wallpaper_manager_flutter - sha256: "37286f08293ec62590448599b7dc3b94b11ee1175ab7b0e33cbf6b79fbfcba42" - url: "https://pub.dev" - source: hosted + path: "." + ref: HEAD + resolved-ref: a1c96618483ace4596af085891f04a4d463725b4 + url: "https://github.com/ente-io/wallpaper_manager_fork.git" + source: git version: "0.0.2" watcher: dependency: "direct overridden" description: name: watcher - sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" + sha256: "69da27e49efa56a15f8afe8f4438c4ec02eff0a117df1b22ea4aad194fe1c104" url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" web: dependency: transitive description: name: web - sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" web_socket: dependency: transitive description: @@ -2827,10 +2886,10 @@ packages: dependency: transitive description: name: web_socket_channel - sha256: "9f187088ed104edd8662ca07af4b124465893caf063ba29758f97af57e61da8f" + sha256: "0b8e2457400d8a859b7b2030786835a28a8e80836ef64402abef392ff4f1d0e5" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" webdriver: dependency: transitive description: @@ -2864,13 +2923,13 @@ packages: source: hosted version: "0.0.3" win32: - dependency: "direct overridden" + dependency: "direct main" description: name: win32 - sha256: "4d45dc9069dba4619dc0ebd93c7cec5e66d8482cb625a370ac806dcc8165f2ec" + sha256: daf97c9d80197ed7b619040e86c8ab9a9dad285e7671ee7390f9180cc828a51e url: "https://pub.dev" source: hosted - version: "5.5.5" + version: "5.10.1" win32_registry: dependency: transitive description: @@ -2923,10 +2982,10 @@ packages: dependency: transitive description: name: yaml - sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce url: "https://pub.dev" source: hosted - version: "3.1.2" + version: "3.1.3" sdks: dart: ">=3.5.0 <4.0.0" flutter: ">=3.24.0" diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index fa330d7cf8..bbb94254cf 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: 0.9.98+999 +version: 0.9.99+1013 publish_to: none environment: @@ -25,13 +25,15 @@ dependencies: animated_list_plus: ^0.5.2 archive: ^3.6.1 background_fetch: ^1.2.1 - battery_info: ^1.1.1 + battery_info: # update source if there is any update + git: + url: https://github.com/ente-io/battery_info bip39: ^1.0.6 cached_network_image: ^3.0.0 chewie: git: url: https://github.com/ente-io/chewie.git - ref: forked_video_player_plus + ref: mybranched collection: # dart computer: git: "https://github.com/ente-io/computer.git" @@ -42,7 +44,7 @@ dependencies: dart_ui_isolate: ^1.1.1 defer_pointer: ^0.0.2 device_info_plus: ^9.0.3 - dio: ^4.0.6 + dio: ^5.8.0+1 dots_indicator: ^2.0.0 dotted_border: ^2.1.0 dropdown_button2: ^2.0.0 @@ -53,6 +55,8 @@ dependencies: path: plugins/ente_cast ente_cast_normal: path: plugins/ente_cast_normal + ente_crypto: + path: plugins/ente_crypto ente_feature_flag: path: plugins/ente_feature_flag equatable: ^2.0.5 @@ -64,11 +68,11 @@ dependencies: fade_indexed_stack: ^0.2.2 fast_base58: ^0.2.1 ffmpeg_kit_flutter_full_gpl: ^6.0.3 - figma_squircle: 0.5.3 - file_saver: - # Use forked version till this PR is merged: https://github.com/incrediblezayed/file_saver/pull/87 - git: https://github.com/jesims/file_saver.git - fk_user_agent: ^2.0.1 + figma_squircle: # update source if there is any update + git: + url: https://github.com/aloisdeniel/figma_squircle.git + ref: 7cc383b30e96c07acd4e484c1d6731d054f7f6ec + file_saver: ^0.2.14 flutter: sdk: flutter flutter_animate: ^4.1.0 @@ -76,8 +80,8 @@ dependencies: flutter_datetime_picker_bdaya: ^3.0.2 flutter_displaymode: ^0.6.0 flutter_easyloading: ^3.0.0 - flutter_email_sender: ^5.2.0 - flutter_image_compress: ^1.1.0 + flutter_email_sender: ^7.0.0 + flutter_image_compress: ^2.4.0 flutter_inappwebview: ^6.1.4 flutter_launcher_icons: ^0.13.1 flutter_local_notifications: ^17.2.2 @@ -87,8 +91,8 @@ dependencies: flutter_map_marker_cluster: ^1.3.6 flutter_native_splash: ^2.2.0+1 flutter_password_strength: ^0.1.6 - flutter_secure_storage: ^8.0.0 - flutter_sodium: ^0.2.0 + flutter_secure_storage: ^9.2.4 + flutter_sodium: flutter_staggered_grid_view: ^0.6.2 flutter_svg: ^2.0.10+1 fluttertoast: ^8.0.6 @@ -109,37 +113,37 @@ dependencies: local_auth: ^2.1.5 local_auth_android: local_auth_ios: - logging: ^1.0.1 + logging: ^1.3.0 lottie: ^1.2.2 - maps_launcher: ^2.2.1 + maps_launcher: ^3.0.0+1 media_extension: git: url: "https://github.com/ente-io/media_extension.git" - ref: deeplink_fixes - media_kit: ^1.1.10+1 - media_kit_libs_ios_video: ^1.1.4 - media_kit_libs_video: ^1.0.4 - media_kit_video: ^1.2.4 + media_kit: + media_kit_libs_ios_video: + media_kit_libs_video: + media_kit_video: ml_linalg: ^13.11.31 - modal_bottom_sheet: ^3.0.0-pre + modal_bottom_sheet: ^3.0.0 motion_photos: git: "https://github.com/ente-io/motion_photo.git" motionphoto: git: "https://github.com/ente-io/motionphoto.git" - move_to_background: ^1.0.2 + move_to_background: # update source if there is any update + git: "https://github.com/ente-io/move_to_background.git" nanoid: ^1.0.0 - native_video_player: - git: - url: https://github.com/ashilkn/native_video_player.git - ref: notify_if_video_isnt_playable + native_dio_adapter: ^1.4.0 + native_video_player: ^3.0.0-dev.4 onnx_dart: path: plugins/onnx_dart onnxruntime: git: url: https://github.com/ente-io/onnxruntime.git ref: ios_only - open_mail_app: ^0.4.5 - package_info_plus: ^4.1.0 + open_mail_app: # update source if there is any update + git: + url: https://github.com/ente-io/open-mail-app-flutter.git + package_info_plus: ^8.2.1 page_transition: ^2.0.2 panorama: git: @@ -155,15 +159,20 @@ dependencies: pinput: ^5.0.0 pointycastle: ^3.7.3 pool: ^1.5.1 - privacy_screen: ^0.0.6 + privacy_screen: # update source if there is any update + git: + url: https://github.com/ente-io/privacy_screen.git + ref: pb/FIX-obsolete-android-plugin protobuf: ^3.1.0 provider: ^6.0.0 quiver: ^3.0.1 - receive_sharing_intent: ^1.7.0 + receive_sharing_intent: # update source if there is any update + git: + url: https://github.com/KasemJaffer/receive_sharing_intent.git screenshot: ^3.0.0 scrollable_positioned_list: ^0.3.5 - sentry: ^8.0.0 - sentry_flutter: ^8.0.0 + sentry: ^8.13.2 + sentry_flutter: ^8.13.2 share_plus: ^10.0.2 shared_preferences: ^2.0.5 simple_cluster: ^0.3.0 @@ -176,27 +185,32 @@ dependencies: syncfusion_flutter_core: ^25.2.5 syncfusion_flutter_sliders: ^25.2.5 synchronized: ^3.1.0 - system_info_plus: ^0.0.5 + system_info_plus: ^0.0.6 tuple: ^2.0.0 ua_client_hints: ^1.4.0 - uni_links: ^0.5.1 + uni_links: # update to app_links + git: + url: https://github.com/ente-io/uni_links.git + path: uni_links url_launcher: ^6.3.0 uuid: ^4.5.0 video_editor: git: - url: https://github.com/prateekmedia/video_editor.git + url: https://github.com/ente-io/video_editor_fork.git video_player: git: url: https://github.com/ente-io/packages.git ref: android_video_roation_fix path: packages/video_player/video_player/ - video_player_media_kit: ^1.0.5 - video_thumbnail: ^0.5.3 + video_thumbnail: visibility_detector: ^0.3.3 wakelock_plus: ^1.1.1 - wallpaper_manager_flutter: ^0.0.2 + wallpaper_manager_flutter: # update source if there is any update + git: + url: https://github.com/ente-io/wallpaper_manager_fork.git wechat_assets_picker: ^8.6.3 widgets_to_image: ^0.0.2 + win32: ^5.10.1 xml: ^6.3.0 xmp: ^1.0.3 @@ -204,14 +218,37 @@ dependency_overrides: # Remove this after removing dependency from flutter_sodium. # Newer flutter packages depends on ffi > 2.0.0 while flutter_sodium depends on ffi < 2.0.0 ffi: 2.1.0 + flutter_sodium: # update source if there is any update + git: + url: https://github.com/ente-io/flutter_sodium intl: 0.18.1 - video_player: + js: ^0.6.7 + media_kit: # update media_kit* if there is any update + git: + url: https://github.com/media-kit/media-kit + path: media_kit + media_kit_libs_ios_video: + git: + url: https://github.com/media-kit/media-kit + path: libs/ios/media_kit_libs_ios_video + media_kit_libs_video: + git: + url: https://github.com/media-kit/media-kit + path: libs/universal/media_kit_libs_video + media_kit_video: + git: + url: https://github.com/media-kit/media-kit + path: media_kit_video + video_player: # remove this dep as soon as we move to one player for all git: url: https://github.com/ente-io/packages.git ref: android_video_roation_fix path: packages/video_player/video_player/ + video_thumbnail: # update source if there is any update + git: + url: https://github.com/ente-io/video_thumbnail_fork.git watcher: ^1.1.0 - win32: ^5.5.4 + win32: "5.10.1" flutter_intl: enabled: true diff --git a/mobile/test/utils/date_time_util_test.dart b/mobile/test/utils/date_time_util_test.dart index 5025299a1c..ac89231ee8 100644 --- a/mobile/test/utils/date_time_util_test.dart +++ b/mobile/test/utils/date_time_util_test.dart @@ -1,6 +1,6 @@ import 'package:flutter/foundation.dart'; import 'package:photos/core/constants.dart'; -import 'package:photos/utils/date_time_util.dart'; +import "package:photos/utils/standalone/date_time.dart"; import 'package:test/test.dart'; void main() { diff --git a/server/RUNNING.md b/server/RUNNING.md index 0f2dc4fe3d..00c1298677 100644 --- a/server/RUNNING.md +++ b/server/RUNNING.md @@ -43,10 +43,10 @@ Or interact with the other services in the cluster, e.g. connect to the DB Or interact with the MinIO S3 API - AWS_ACCESS_KEY_ID=test AWS_SECRET_ACCESS_KEY=testtest \ - aws s3 --endpoint-url http://localhost:3200 ls s3://test + AWS_ACCESS_KEY_ID=changeme AWS_SECRET_ACCESS_KEY=changeme1234 \ + aws s3 --endpoint-url http://localhost:3200 ls s3://b2-eu-cen -Or open the MinIO dashboard at (user: test/password: testtest). +Or open the MinIO dashboard at (user: changeme/password: changeme1234). > [!NOTE] > diff --git a/server/cmd/museum/main.go b/server/cmd/museum/main.go index e1a3622011..f3a92cf210 100644 --- a/server/cmd/museum/main.go +++ b/server/cmd/museum/main.go @@ -5,6 +5,7 @@ import ( "database/sql" b64 "encoding/base64" "fmt" + "github.com/ente-io/museum/pkg/controller/collections" "net/http" "os" "os/signal" @@ -242,13 +243,14 @@ func main() { ) usageController := &controller.UsageController{ - BillingCtrl: billingController, - StorageBonusCtrl: storageBonusCtrl, - UserCacheCtrl: userCacheCtrl, - UsageRepo: usageRepo, - UserRepo: userRepo, - FamilyRepo: familyRepo, - FileRepo: fileRepo, + BillingCtrl: billingController, + StorageBonusCtrl: storageBonusCtrl, + UserCacheCtrl: userCacheCtrl, + UsageRepo: usageRepo, + UserRepo: userRepo, + FamilyRepo: familyRepo, + FileRepo: fileRepo, + UploadResultCache: make(map[int64]bool), } accessCtrl := access.NewAccessController(collectionRepo, fileRepo) @@ -293,6 +295,7 @@ func main() { BillingCtrl: billingController, UserRepo: userRepo, UserCacheCtrl: userCacheCtrl, + UsageRepo: usageRepo, } publicCollectionCtrl := &controller.PublicCollectionController{ @@ -304,7 +307,7 @@ func main() { JwtSecret: jwtSecretBytes, } - collectionController := &controller.CollectionController{ + collectionController := &collections.CollectionController{ CollectionRepo: collectionRepo, EmailCtrl: emailNotificationCtrl, AccessCtrl: accessCtrl, @@ -536,6 +539,7 @@ func main() { //lint:ignore SA1019 Deprecated API will be removed in the future privateAPI.GET("/collections", collectionHandler.Get) privateAPI.GET("/collections/v2", collectionHandler.GetV2) + privateAPI.GET("/collections/v3", collectionHandler.GetWithLimit) privateAPI.POST("/collections/share", collectionHandler.Share) privateAPI.POST("/collections/join-link", collectionHandler.JoinLink) privateAPI.POST("/collections/share-url", collectionHandler.ShareURL) @@ -621,6 +625,7 @@ func main() { familiesJwtAuthAPI.GET("/family/members", familyHandler.FetchMembers) familiesJwtAuthAPI.DELETE("/family/remove-member/:id", familyHandler.RemoveMember) familiesJwtAuthAPI.DELETE("/family/revoke-invite/:id", familyHandler.RevokeInvite) + familiesJwtAuthAPI.POST("/family/modify-storage", familyHandler.ModifyStorageLimit) emergencyHandler := &api.EmergencyHandler{ Controller: emergencyCtrl, @@ -750,14 +755,7 @@ func main() { pushHandler := &api.PushHandler{PushController: pushController} privateAPI.POST("/push/token", pushHandler.AddToken) - embeddingController := embeddingCtrl.New(embeddingRepo, accessCtrl, objectCleanupController, s3Config, queueRepo, taskLockingRepo, fileRepo, collectionRepo, hostName) - embeddingHandler := &api.EmbeddingHandler{Controller: embeddingController} - - privateAPI.PUT("/embeddings", embeddingHandler.InsertOrUpdate) - privateAPI.GET("/embeddings/diff", embeddingHandler.GetDiff) - privateAPI.GET("/embeddings/indexed-files", embeddingHandler.GetIndexedFiles) - privateAPI.POST("/embeddings/files", embeddingHandler.GetFilesEmbedding) - privateAPI.DELETE("/embeddings", embeddingHandler.DeleteAll) + embeddingController := embeddingCtrl.New(embeddingRepo, objectCleanupController, queueRepo, taskLockingRepo, fileRepo, hostName) offerHandler := &api.OfferHandler{Controller: offerController} publicAPI.GET("/offers/black-friday", offerHandler.GetBlackFridayOffers) @@ -943,7 +941,7 @@ func setupAndStartCrons(userAuthRepo *repo.UserAuthRepository, publicCollectionR } }) - schedule(c, "@every 10m", func() { + schedule(c, "@every 8m", func() { fileController.CleanupDeletedFiles() }) schedule(c, "@every 101s", func() { diff --git a/server/compose.yaml b/server/compose.yaml index 250feb40a0..727d1f8d77 100644 --- a/server/compose.yaml +++ b/server/compose.yaml @@ -1,3 +1,5 @@ +# Please note that the below docker compose is NOT directly +# meant to be copy pasted and deployed to production. services: museum: build: @@ -31,6 +33,7 @@ services: postgres: image: postgres:15 + # Change the postgres port here ports: - 5432:5432 environment: @@ -64,8 +67,9 @@ services: - 3200:3200 # API - 3201:3201 # Console environment: - MINIO_ROOT_USER: test - MINIO_ROOT_PASSWORD: testtest + MINIO_ROOT_USER: changeme + MINIO_ROOT_PASSWORD: changeme1234 + # Tweak this command to match the above port changes. command: server /data --address ":3200" --console-address ":3201" volumes: - minio-data:/data diff --git a/server/docs/publish.md b/server/docs/publish.md index 7da3b27c60..0c6ee2e4f5 100644 --- a/server/docs/publish.md +++ b/server/docs/publish.md @@ -21,26 +21,23 @@ commit to the GitHub Container Registry (GHCR) so that it can be used by folks without needing to clone our repository just for building an image. For more details about the use case, see [docker.md](docker.md). -To publish such an external image, firstly find the commit of the currently -running production instance. +These images are published automatically by the "Publish (server)" workflow on +the 15th of every month. If needed, the workflow can also be manually triggered +invoked to publish out of schedule. It can be triggered on the GitHub UI, or by - curl -s https://api.ente.io/ping | jq -r '.id' +```sh +gh workflow run server-publish.yml +``` -> We can publish from any arbitrary commit really, but by using the commit -> that's already seen production for a few days, we avoid externally publishing -> images with issues. - -Then, trigger the "Publish (server)" workflow, providing it the commit. You can -trigger it either from GitHub's UI or using the `gh cli`. With the CLI, we can -combine both these steps too. - - gh workflow run server-publish.yml -F commit=`curl -s https://api.ente.io/ping | jq -r '.id'` +> It uses the commit that is deployed on production museum instances. We can +> publish from any arbitrary commit really, but by using the commit that's +> already seen production, we avoid externally publishing images with issues. Once the workflow completes, the resultant image will be available at `ghcr.io/ente-io/server`. The image will be tagged by the commit SHA. The latest image will also be tagged, well, "latest". The workflow will also update the branch `server/ghcr` to point to the commit it -used to build the image. This branch will be overwritten on each publish, and -thus it `server/ghcr` points to the code from which the most recent ghcr docker -image for museum has been built. +used to build the image. This branch will be overwritten on each publish; thus +`server/ghcr` will always points to the code from which the most recent ghcr +docker image for museum has been built. diff --git a/server/ente/errors.go b/server/ente/errors.go index aac78f6f73..56d341571a 100644 --- a/server/ente/errors.go +++ b/server/ente/errors.go @@ -24,6 +24,12 @@ var ErrIncorrectTOTP = errors.New("incorrect TOTP") // ErrNotFound is returned when the requested resource was not found var ErrNotFound = errors.New("not found") +var ErrCollectionDeleted = &ApiError{ + Code: "COLLECTION_DELETED", + Message: "", + HttpStatusCode: http.StatusNotFound, +} + var ErrFileLimitReached = errors.New("file limit reached") // ErrBadRequest is returned when a bad request is encountered @@ -153,6 +159,12 @@ var ErrNotFoundError = ApiError{ HttpStatusCode: http.StatusNotFound, } +var ErrObjSizeFetchFailed = &ApiError{ + Code: "OBJECT_SIZE_FETCH_FAILED", + Message: "", + HttpStatusCode: http.StatusServiceUnavailable, +} + var ErrUserNotFound = &ApiError{ Code: "USER_NOT_FOUND", Message: "User is either deleted or not found", diff --git a/server/ente/family.go b/server/ente/family.go index 004067007c..b02b6cab7b 100644 --- a/server/ente/family.go +++ b/server/ente/family.go @@ -49,6 +49,11 @@ type FamilyMember struct { AdminUserID int64 `json:"-"` // for internal use only, ignore from json response } +type ModifyMemberStorage struct { + ID uuid.UUID `json:"id" binding:"required"` + StorageLimit *int64 `json:"storageLimit"` +} + type FamilyMemberResponse struct { Members []FamilyMember `json:"members" binding:"required"` // Family admin subscription storage capacity. This excludes add-on and any other bonus storage diff --git a/server/ente/public_collection.go b/server/ente/public_collection.go index ef22302b89..eb1bd8c385 100644 --- a/server/ente/public_collection.go +++ b/server/ente/public_collection.go @@ -12,7 +12,7 @@ import ( type CreatePublicAccessTokenRequest struct { CollectionID int64 `json:"collectionID" binding:"required"` EnableCollect bool `json:"enableCollect"` - // defaults to false + // defaults to true EnableJoin *bool `json:"enableJoin"` ValidTill int64 `json:"validTill"` DeviceLimit int `json:"deviceLimit"` diff --git a/server/migrations/97_file_data_user_update_idx.down.sql b/server/migrations/97_file_data_user_update_idx.down.sql new file mode 100644 index 0000000000..848321602e --- /dev/null +++ b/server/migrations/97_file_data_user_update_idx.down.sql @@ -0,0 +1 @@ +DROP INDEX CONCURRENTLY IF EXISTS idx_file_data_user_updated; diff --git a/server/migrations/97_file_data_user_update_idx.up.sql b/server/migrations/97_file_data_user_update_idx.up.sql new file mode 100644 index 0000000000..b0dc8ae140 --- /dev/null +++ b/server/migrations/97_file_data_user_update_idx.up.sql @@ -0,0 +1,2 @@ +CREATE INDEX CONCURRENTLY idx_file_data_user_updated + ON file_data (user_id, updated_at); diff --git a/server/pkg/api/cast.go b/server/pkg/api/cast.go index 9012624d32..0fcac8a364 100644 --- a/server/pkg/api/cast.go +++ b/server/pkg/api/cast.go @@ -5,6 +5,7 @@ import ( entity "github.com/ente-io/museum/ente/cast" "github.com/ente-io/museum/pkg/controller" "github.com/ente-io/museum/pkg/controller/cast" + "github.com/ente-io/museum/pkg/controller/collections" "github.com/ente-io/museum/pkg/utils/handler" "github.com/ente-io/stacktrace" "github.com/gin-gonic/gin" @@ -16,7 +17,7 @@ import ( // CastHandler exposes request handlers for publicly accessible collections type CastHandler struct { FileCtrl *controller.FileController - CollectionCtrl *controller.CollectionController + CollectionCtrl *collections.CollectionController Ctrl *cast.Controller } diff --git a/server/pkg/api/collection.go b/server/pkg/api/collection.go index 57cc8c4f28..9318f5c329 100644 --- a/server/pkg/api/collection.go +++ b/server/pkg/api/collection.go @@ -2,6 +2,7 @@ package api import ( "fmt" + "github.com/ente-io/museum/pkg/controller/collections" "net/http" "strconv" @@ -18,7 +19,7 @@ import ( // CollectionHandler exposes request handlers for all collection related requests type CollectionHandler struct { - Controller *controller.CollectionController + Controller *collections.CollectionController } // Create creates a collection @@ -64,18 +65,20 @@ func (h *CollectionHandler) GetCollectionByID(c *gin.Context) { // Deprecated: Remove once rps goes to 0. // Get returns the list of collections accessible to a user. func (h *CollectionHandler) Get(c *gin.Context) { + h.GetV2(c) +} + +// GetV2 returns the list of collections accessible to a user +func (h *CollectionHandler) GetV2(c *gin.Context) { userID := auth.GetUserID(c.Request.Header) sinceTime, _ := strconv.ParseInt(c.Query("sinceTime"), 10, 64) - app := auth.GetApp(c) - - // TODO: Compute both with a single query - ownedCollections, err := h.Controller.GetOwned(userID, sinceTime, app) + ownedCollections, err := h.Controller.GetOwnedV2(userID, sinceTime, app, nil) if err != nil { handler.Error(c, stacktrace.Propagate(err, "Failed to get owned collections")) return } - sharedCollections, err := h.Controller.GetSharedWith(userID, sinceTime, app) + sharedCollections, err := h.Controller.GetSharedWith(userID, sinceTime, app, nil) if err != nil { handler.Error(c, stacktrace.Propagate(err, "Failed to get shared collections")) return @@ -85,23 +88,32 @@ func (h *CollectionHandler) Get(c *gin.Context) { }) } -// GetV2 returns the list of collections accessible to a user -func (h *CollectionHandler) GetV2(c *gin.Context) { +// GetWithLimit returns owned and shared collections accessible to a user +func (h *CollectionHandler) GetWithLimit(c *gin.Context) { userID := auth.GetUserID(c.Request.Header) sinceTime, _ := strconv.ParseInt(c.Query("sinceTime"), 10, 64) + sharedSinceTime, _ := strconv.ParseInt(c.Query("sharedSinceTime"), 10, 64) + limit := int64(1000) + if c.Query("limit") != "" { + limit, _ = strconv.ParseInt(c.Query("limit"), 10, 64) + if limit > 1000 { + limit = 1000 + } + } app := auth.GetApp(c) - ownedCollections, err := h.Controller.GetOwnedV2(userID, sinceTime, app) + ownedCollections, err := h.Controller.GetOwnedV2(userID, sinceTime, app, &limit) if err != nil { handler.Error(c, stacktrace.Propagate(err, "Failed to get owned collections")) return } - sharedCollections, err := h.Controller.GetSharedWith(userID, sinceTime, app) + sharedCollections, err := h.Controller.GetSharedWith(userID, sharedSinceTime, app, &limit) if err != nil { handler.Error(c, stacktrace.Propagate(err, "Failed to get shared collections")) return } c.JSON(http.StatusOK, gin.H{ - "collections": append(ownedCollections, sharedCollections...), + "owned": ownedCollections, + "shared": sharedCollections, }) } diff --git a/server/pkg/api/embedding.go b/server/pkg/api/embedding.go deleted file mode 100644 index d95158dd8d..0000000000 --- a/server/pkg/api/embedding.go +++ /dev/null @@ -1,95 +0,0 @@ -package api - -import ( - "fmt" - "net/http" - - "github.com/ente-io/museum/ente" - "github.com/ente-io/museum/pkg/controller/embedding" - "github.com/ente-io/museum/pkg/utils/handler" - - "github.com/ente-io/stacktrace" - "github.com/gin-gonic/gin" -) - -type EmbeddingHandler struct { - Controller *embedding.Controller -} - -// InsertOrUpdate handler for inserting or updating embedding -func (h *EmbeddingHandler) InsertOrUpdate(c *gin.Context) { - var request ente.InsertOrUpdateEmbeddingRequest - if err := c.ShouldBindJSON(&request); err != nil { - handler.Error(c, - stacktrace.Propagate(ente.ErrBadRequest, fmt.Sprintf("Request binding failed %s", err))) - return - } - embedding, err := h.Controller.InsertOrUpdate(c, request) - if err != nil { - handler.Error(c, stacktrace.Propagate(err, "")) - return - } - c.JSON(http.StatusOK, embedding) -} - -// GetDiff handler for getting diff of embedding -func (h *EmbeddingHandler) GetDiff(c *gin.Context) { - var request ente.GetEmbeddingDiffRequest - if err := c.ShouldBindQuery(&request); err != nil { - handler.Error(c, - stacktrace.Propagate(ente.ErrBadRequest, fmt.Sprintf("Request binding failed %s", err))) - return - } - embeddings, err := h.Controller.GetDiff(c, request) - if err != nil { - handler.Error(c, stacktrace.Propagate(err, "")) - return - } - c.JSON(http.StatusOK, gin.H{ - "diff": embeddings, - }) -} - -// GetIndexedFiles returns the fileIDs that has been indexed or updated for given user -func (h *EmbeddingHandler) GetIndexedFiles(c *gin.Context) { - var request ente.GetIndexedFiles - if err := c.ShouldBindQuery(&request); err != nil { - handler.Error(c, - stacktrace.Propagate(ente.ErrBadRequest, fmt.Sprintf("Request binding failed %s", err))) - return - } - embeddings, err := h.Controller.GetIndexedFiles(c, request) - if err != nil { - handler.Error(c, stacktrace.Propagate(err, "")) - return - } - c.JSON(http.StatusOK, gin.H{ - "diff": embeddings, - }) -} - -// GetFilesEmbedding returns the embeddings for the files -func (h *EmbeddingHandler) GetFilesEmbedding(c *gin.Context) { - var request ente.GetFilesEmbeddingRequest - if err := c.ShouldBindJSON(&request); err != nil { - handler.Error(c, - stacktrace.Propagate(ente.ErrBadRequest, fmt.Sprintf("Request binding failed %s", err))) - return - } - resp, err := h.Controller.GetFilesEmbedding(c, request) - if err != nil { - handler.Error(c, stacktrace.Propagate(err, "")) - return - } - c.JSON(http.StatusOK, resp) -} - -// DeleteAll handler for deleting all embeddings for the user -func (h *EmbeddingHandler) DeleteAll(c *gin.Context) { - err := h.Controller.DeleteAll(c) - if err != nil { - handler.Error(c, stacktrace.Propagate(err, "")) - return - } - c.Status(http.StatusOK) -} diff --git a/server/pkg/api/family.go b/server/pkg/api/family.go index e4ca403ac4..aa0d2f6c9f 100644 --- a/server/pkg/api/family.go +++ b/server/pkg/api/family.go @@ -120,6 +120,22 @@ func (h *FamilyHandler) AcceptInvite(c *gin.Context) { c.JSON(http.StatusOK, response) } +// ModifyStorageLimit allows adminUser to Modify the storage for a member in the Family. +func (h *FamilyHandler) ModifyStorageLimit(c *gin.Context) { + var request ente.ModifyMemberStorage + if err := c.ShouldBindJSON(&request); err != nil { + handler.Error(c, stacktrace.Propagate(err, "Could not bind request params")) + return + } + + err := h.Controller.ModifyMemberStorage(c, auth.GetUserID(c.Request.Header), request.ID, request.StorageLimit) + if err != nil { + handler.Error(c, stacktrace.Propagate(err, "")) + return + } + c.Status(http.StatusOK) +} + // GetInviteInfo returns basic information about invitor/admin as long as the invite is valid func (h *FamilyHandler) GetInviteInfo(c *gin.Context) { inviteToken := c.Param("token") diff --git a/server/pkg/api/public_collection.go b/server/pkg/api/public_collection.go index b6d2d79a94..d66c1ff2c1 100644 --- a/server/pkg/api/public_collection.go +++ b/server/pkg/api/public_collection.go @@ -2,6 +2,7 @@ package api import ( "fmt" + "github.com/ente-io/museum/pkg/controller/collections" "net/http" "strconv" @@ -19,7 +20,7 @@ import ( type PublicCollectionHandler struct { Controller *controller.PublicCollectionController FileCtrl *controller.FileController - CollectionCtrl *controller.CollectionController + CollectionCtrl *collections.CollectionController StorageBonusController *storagebonus.Controller } diff --git a/server/pkg/controller/collection.go b/server/pkg/controller/collection.go deleted file mode 100644 index 52c6a32418..0000000000 --- a/server/pkg/controller/collection.go +++ /dev/null @@ -1,829 +0,0 @@ -package controller - -import ( - "context" - "encoding/json" - "fmt" - "runtime/debug" - "strings" - - "github.com/ente-io/museum/pkg/repo/cast" - - "github.com/ente-io/museum/pkg/controller/access" - "github.com/ente-io/museum/pkg/controller/email" - "github.com/gin-contrib/requestid" - "github.com/google/go-cmp/cmp" - "github.com/google/go-cmp/cmp/cmpopts" - - "github.com/ente-io/museum/pkg/utils/array" - "github.com/ente-io/museum/pkg/utils/auth" - "github.com/gin-gonic/gin" - - "github.com/ente-io/museum/ente" - "github.com/ente-io/museum/pkg/repo" - "github.com/ente-io/museum/pkg/utils/time" - "github.com/ente-io/stacktrace" - log "github.com/sirupsen/logrus" -) - -const ( - CollectionDiffLimit = 2500 -) - -// CollectionController encapsulates logic that deals with collections -type CollectionController struct { - PublicCollectionCtrl *PublicCollectionController - EmailCtrl *email.EmailNotificationController - AccessCtrl access.Controller - BillingCtrl *BillingController - CollectionRepo *repo.CollectionRepository - UserRepo *repo.UserRepository - FileRepo *repo.FileRepository - QueueRepo *repo.QueueRepository - CastRepo *cast.Repository - TaskRepo *repo.TaskLockRepository -} - -// Create creates a collection -func (c *CollectionController) Create(collection ente.Collection, ownerID int64) (ente.Collection, error) { - // The key attribute check is to ensure that user does not end up uploading any files before actually setting the key attributes. - if _, keyErr := c.UserRepo.GetKeyAttributes(ownerID); keyErr != nil { - return ente.Collection{}, stacktrace.Propagate(keyErr, "Unable to get keyAttributes") - } - collectionType := collection.Type - collection.Owner.ID = ownerID - collection.UpdationTime = time.Microseconds() - // [20th Dec 2022] Patch on server side untill majority of the existing mobile clients upgrade to a version higher > 0.7.0 - // https://github.com/ente-io/photos-app/pull/725 - if collection.Type == "CollectionType.album" { - collection.Type = "album" - } - if !array.StringInList(collection.Type, ente.ValidCollectionTypes) { - return ente.Collection{}, stacktrace.Propagate(fmt.Errorf("unexpected collection type %s", collection.Type), "") - } - collection, err := c.CollectionRepo.Create(collection) - if err != nil { - if err == ente.ErrUncategorizeCollectionAlreadyExists || err == ente.ErrFavoriteCollectionAlreadyExist { - dbCollection, err := c.CollectionRepo.GetCollectionByType(ownerID, collectionType) - if err != nil { - return ente.Collection{}, stacktrace.Propagate(err, "") - } - if dbCollection.IsDeleted { - return ente.Collection{}, stacktrace.Propagate(fmt.Errorf("special collection of type : %s is deleted", collectionType), "") - } - return dbCollection, nil - } - return ente.Collection{}, stacktrace.Propagate(err, "") - } - return collection, nil -} - -// GetOwned returns the list of collections owned by a user -func (c *CollectionController) GetOwned(userID int64, sinceTime int64, app ente.App) ([]ente.Collection, error) { - collections, err := c.CollectionRepo.GetCollectionsOwnedByUser(userID, sinceTime, app) - if err != nil { - return nil, stacktrace.Propagate(err, "") - } - go func() { - defer func() { - if r := recover(); r != nil { - log.Errorf("Panic caught: %s, stack: %s", r, string(debug.Stack())) - } - }() - collectionsV2, errV2 := c.CollectionRepo.GetCollectionsOwnedByUserV2(userID, sinceTime, app) - if errV2 != nil { - log.WithError(errV2).Error("failed to fetch collections using v2") - } - isEqual := cmp.Equal(collections, collectionsV2, cmpopts.SortSlices(func(a, b ente.Collection) bool { return a.ID < b.ID })) - if !isEqual { - jsonV1, _ := json.Marshal(collections) - jsonV2, _ := json.Marshal(collectionsV2) - log.WithFields(log.Fields{ - "v1": string(jsonV1), - "v2": string(jsonV2), - }).Error("collections diff didn't match") - } else { - log.Info("collections diff matched") - } - }() - return collections, nil -} - -// GetOwnedV2 returns the list of collections owned by a user using optimized query -func (c *CollectionController) GetOwnedV2(userID int64, sinceTime int64, app ente.App) ([]ente.Collection, error) { - collections, err := c.CollectionRepo.GetCollectionsOwnedByUserV2(userID, sinceTime, app) - if err != nil { - return nil, stacktrace.Propagate(err, "") - } - return collections, nil -} - -// GetCollection returns the collection for given collectionID -func (c *CollectionController) GetCollection(ctx *gin.Context, userID int64, cID int64) (ente.Collection, error) { - resp, err := c.AccessCtrl.GetCollection(ctx, &access.GetCollectionParams{ - CollectionID: cID, - ActorUserID: userID, - IncludeDeleted: true, - }) - if err != nil { - return ente.Collection{}, stacktrace.Propagate(err, "") - } - return resp.Collection, nil -} - -// GetSharedWith returns the list of collections that are shared with a user -func (c *CollectionController) GetSharedWith(userID int64, sinceTime int64, app ente.App) ([]ente.Collection, error) { - collections, err := c.CollectionRepo.GetCollectionsSharedWithUser(userID, sinceTime, app) - if err != nil { - return nil, stacktrace.Propagate(err, "") - } - return collections, nil -} - -// Share shares a collection with a user -func (c *CollectionController) Share(ctx *gin.Context, req ente.AlterShareRequest) ([]ente.CollectionUser, error) { - fromUserID := auth.GetUserID(ctx.Request.Header) - cID := req.CollectionID - encryptedKey := req.EncryptedKey - toUserEmail := strings.ToLower(strings.TrimSpace(req.Email)) - // default role type - role := ente.VIEWER - if req.Role != nil { - role = *req.Role - } - - toUserID, err := c.UserRepo.GetUserIDWithEmail(toUserEmail) - if err != nil { - return nil, stacktrace.Propagate(err, "") - } - if toUserID == fromUserID { - return nil, stacktrace.Propagate(ente.ErrBadRequest, "Can not share collection with self") - } - collection, err := c.CollectionRepo.Get(cID) - if err != nil { - return nil, stacktrace.Propagate(err, "") - } - if !collection.AllowSharing() { - return nil, stacktrace.Propagate(ente.ErrBadRequest, fmt.Sprintf("sharing %s is not allowed", collection.Type)) - } - if fromUserID != collection.Owner.ID { - return nil, stacktrace.Propagate(ente.ErrPermissionDenied, "") - } - err = c.BillingCtrl.HasActiveSelfOrFamilySubscription(fromUserID, true) - if err != nil { - return nil, stacktrace.Propagate(err, "") - } - err = c.CollectionRepo.Share(cID, fromUserID, toUserID, encryptedKey, role, time.Microseconds()) - if err != nil { - return nil, stacktrace.Propagate(err, "") - } - sharees, err := c.GetSharees(ctx, cID, fromUserID) - if err != nil { - return nil, stacktrace.Propagate(err, "") - } - return sharees, nil -} - -func (c *CollectionController) JoinViaLink(ctx *gin.Context, req ente.JoinCollectionViaLinkRequest) error { - userID := auth.GetUserID(ctx.Request.Header) - collection, err := c.CollectionRepo.Get(req.CollectionID) - if err != nil { - return stacktrace.Propagate(err, "") - } - if collection.Owner.ID == userID { - return stacktrace.Propagate(ente.ErrBadRequest, "owner can not join via link") - } - if !collection.AllowSharing() { - return stacktrace.Propagate(ente.ErrBadRequest, fmt.Sprintf("joining %s is not allowed", collection.Type)) - } - publicCollectionToken, err := c.PublicCollectionCtrl.GetActivePublicCollectionToken(ctx, req.CollectionID) - if err != nil { - return stacktrace.Propagate(err, "") - } - - if canJoin := publicCollectionToken.CanJoin(); canJoin != nil { - return stacktrace.Propagate(ente.ErrBadRequest, fmt.Sprintf("can not join collection: %s", canJoin.Error())) - } - accessToken := auth.GetAccessToken(ctx) - if publicCollectionToken.Token != accessToken { - return stacktrace.Propagate(ente.ErrPermissionDenied, "token doesn't match collection") - } - if publicCollectionToken.PassHash != nil && *publicCollectionToken.PassHash != "" { - accessTokenJWT := auth.GetAccessTokenJWT(ctx) - if passCheckErr := c.PublicCollectionCtrl.ValidateJWTToken(ctx, accessTokenJWT, *publicCollectionToken.PassHash); passCheckErr != nil { - return stacktrace.Propagate(passCheckErr, "") - } - } - err = c.BillingCtrl.HasActiveSelfOrFamilySubscription(collection.Owner.ID, true) - if err != nil { - return stacktrace.Propagate(err, "") - } - role := ente.VIEWER - if publicCollectionToken.EnableCollect { - role = ente.COLLABORATOR - } - joinErr := c.CollectionRepo.Share(req.CollectionID, collection.Owner.ID, userID, req.EncryptedKey, role, time.Microseconds()) - if joinErr != nil { - return stacktrace.Propagate(joinErr, "") - } - go c.EmailCtrl.OnLinkJoined(collection.Owner.ID, userID, role) - return nil -} - -// UnShare unshares a collection with a user -func (c *CollectionController) UnShare(ctx *gin.Context, cID int64, fromUserID int64, toUserEmail string) ([]ente.CollectionUser, error) { - toUserID, err := c.UserRepo.GetUserIDWithEmail(toUserEmail) - if err != nil { - return nil, stacktrace.Propagate(ente.ErrNotFound, "") - } - collection, err := c.CollectionRepo.Get(cID) - if err != nil { - return nil, stacktrace.Propagate(err, "") - } - isLeavingCollection := toUserID == fromUserID - if fromUserID != collection.Owner.ID || isLeavingCollection { - return nil, stacktrace.Propagate(ente.ErrPermissionDenied, "") - } - err = c.CollectionRepo.UnShare(cID, toUserID) - if err != nil { - return nil, stacktrace.Propagate(err, "") - } - err = c.CastRepo.RevokeForGivenUserAndCollection(ctx, cID, toUserID) - if err != nil { - return nil, stacktrace.Propagate(err, "") - } - sharees, err := c.GetSharees(ctx, cID, fromUserID) - if err != nil { - return nil, stacktrace.Propagate(err, "") - } - return sharees, nil -} - -// Leave leaves the collection owned by someone else, -func (c *CollectionController) Leave(ctx *gin.Context, cID int64) error { - userID := auth.GetUserID(ctx.Request.Header) - collection, err := c.CollectionRepo.Get(cID) - if err != nil { - return stacktrace.Propagate(err, "") - } - if userID == collection.Owner.ID { - return stacktrace.Propagate(ente.ErrPermissionDenied, "can not leave collection owned by self") - } - sharedCollectionIDs, err := c.CollectionRepo.GetCollectionIDsSharedWithUser(userID) - if err != nil { - return stacktrace.Propagate(err, "") - } - if !array.Int64InList(cID, sharedCollectionIDs) { - return nil - } - err = c.CastRepo.RevokeForGivenUserAndCollection(ctx, cID, userID) - if err != nil { - return stacktrace.Propagate(err, "") - } - err = c.CollectionRepo.UnShare(cID, userID) - if err != nil { - return stacktrace.Propagate(err, "") - } - return nil -} - -func (c *CollectionController) UpdateShareeMagicMetadata(ctx *gin.Context, req ente.UpdateCollectionMagicMetadata) error { - actorUserId := auth.GetUserID(ctx.Request.Header) - resp, err := c.AccessCtrl.GetCollection(ctx, &access.GetCollectionParams{ - CollectionID: req.ID, - ActorUserID: actorUserId, - }) - if err != nil { - return stacktrace.Propagate(err, "") - } - if resp.Collection.Owner.ID == actorUserId { - return stacktrace.Propagate(ente.NewBadRequestWithMessage("owner can not update sharee magic metadata"), "") - } - err = c.CollectionRepo.UpdateShareeMetadata(req.ID, resp.Collection.Owner.ID, actorUserId, req.MagicMetadata, time.Microseconds()) - if err != nil { - return stacktrace.Propagate(err, "failed to update sharee magic metadata") - } - return nil -} - -// ShareURL generates a public auth-token for the given collectionID -func (c *CollectionController) ShareURL(ctx context.Context, userID int64, req ente.CreatePublicAccessTokenRequest) ( - ente.PublicURL, error) { - collection, err := c.CollectionRepo.Get(req.CollectionID) - if err != nil { - return ente.PublicURL{}, stacktrace.Propagate(err, "") - } - if !collection.AllowSharing() { - return ente.PublicURL{}, stacktrace.Propagate(ente.ErrBadRequest, fmt.Sprintf("sharing %s is not allowed", collection.Type)) - } - if userID != collection.Owner.ID { - return ente.PublicURL{}, stacktrace.Propagate(ente.ErrPermissionDenied, "") - } - err = c.BillingCtrl.HasActiveSelfOrFamilySubscription(userID, true) - if err != nil { - return ente.PublicURL{}, stacktrace.Propagate(err, "") - } - response, err := c.PublicCollectionCtrl.CreateAccessToken(ctx, req) - if err != nil { - return ente.PublicURL{}, stacktrace.Propagate(err, "") - } - return response, nil -} - -// UpdateShareURL updates the shared url configuration -func (c *CollectionController) UpdateShareURL(ctx context.Context, userID int64, req ente.UpdatePublicAccessTokenRequest) ( - ente.PublicURL, error) { - if err := c.verifyOwnership(req.CollectionID, userID); err != nil { - return ente.PublicURL{}, stacktrace.Propagate(err, "") - } - err := c.BillingCtrl.HasActiveSelfOrFamilySubscription(userID, true) - if err != nil { - return ente.PublicURL{}, stacktrace.Propagate(err, "") - } - response, err := c.PublicCollectionCtrl.UpdateSharedUrl(ctx, req) - if err != nil { - return ente.PublicURL{}, stacktrace.Propagate(err, "") - } - return response, nil -} - -// DisableSharedURL disable a public auth-token for the given collectionID -func (c *CollectionController) DisableSharedURL(ctx context.Context, userID int64, cID int64) error { - if err := c.verifyOwnership(cID, userID); err != nil { - return stacktrace.Propagate(err, "") - } - err := c.PublicCollectionCtrl.Disable(ctx, cID) - return stacktrace.Propagate(err, "") -} - -// AddFiles adds files to a collection -func (c *CollectionController) AddFiles(ctx *gin.Context, userID int64, files []ente.CollectionFileItem, cID int64) error { - - resp, err := c.AccessCtrl.GetCollection(ctx, &access.GetCollectionParams{ - CollectionID: cID, - ActorUserID: userID, - IncludeDeleted: false, - }) - if err != nil { - return stacktrace.Propagate(err, "failed to verify collection access") - } - if !resp.Role.CanAdd() { - return stacktrace.Propagate(ente.ErrPermissionDenied, fmt.Sprintf("user %d with role %s can not add files", userID, *resp.Role)) - } - - collectionOwnerID := resp.Collection.Owner.ID - filesOwnerID := userID - // Verify that the user owns each file - fileIDs := make([]int64, 0) - for _, file := range files { - fileIDs = append(fileIDs, file.ID) - } - err = c.AccessCtrl.VerifyFileOwnership(ctx, &access.VerifyFileOwnershipParams{ - ActorUserId: userID, - FileIDs: fileIDs, - }) - - if err != nil { - return stacktrace.Propagate(err, "Failed to verify fileOwnership") - } - err = c.CollectionRepo.AddFiles(cID, collectionOwnerID, files, filesOwnerID) - if err != nil { - return stacktrace.Propagate(err, "") - } - return nil -} - -// RestoreFiles restore files from trash and add to the collection -func (c *CollectionController) RestoreFiles(ctx *gin.Context, userID int64, cID int64, files []ente.CollectionFileItem) error { - _, err := c.AccessCtrl.GetCollection(ctx, &access.GetCollectionParams{ - CollectionID: cID, - ActorUserID: userID, - IncludeDeleted: false, - VerifyOwner: true, - }) - if err != nil { - return stacktrace.Propagate(err, "failed to verify collection access") - } - // Verify that the user owns each file - for _, file := range files { - // todo #perf find owners of all files - ownerID, err := c.FileRepo.GetOwnerID(file.ID) - if err != nil { - return stacktrace.Propagate(err, "") - } - if ownerID != userID { - log.WithFields(log.Fields{ - "file_id": file.ID, - "owner_id": ownerID, - "user_id": userID, - }).Error("invalid ops: can't add file which isn't owned by user") - return stacktrace.Propagate(ente.ErrPermissionDenied, "") - } - } - err = c.CollectionRepo.RestoreFiles(ctx, userID, cID, files) - if err != nil { - return stacktrace.Propagate(err, "") - } - return nil -} - -// MoveFiles from one collection to another collection. Both the collections and files should belong to -// single user -func (c *CollectionController) MoveFiles(ctx *gin.Context, req ente.MoveFilesRequest) error { - userID := auth.GetUserID(ctx.Request.Header) - _, err := c.AccessCtrl.GetCollection(ctx, &access.GetCollectionParams{ - CollectionID: req.FromCollectionID, - ActorUserID: userID, - IncludeDeleted: false, - VerifyOwner: true, - }) - if err != nil { - return stacktrace.Propagate(err, "failed to verify if actor owns fromCollection") - } - - _, err = c.AccessCtrl.GetCollection(ctx, &access.GetCollectionParams{ - CollectionID: req.ToCollectionID, - ActorUserID: userID, - IncludeDeleted: false, - VerifyOwner: true, - }) - if err != nil { - return stacktrace.Propagate(err, "failed to verify if actor owns toCollection") - } - - // Verify that the user owns each file - fileIDs := make([]int64, 0) - for _, file := range req.Files { - fileIDs = append(fileIDs, file.ID) - } - err = c.AccessCtrl.VerifyFileOwnership(ctx, &access.VerifyFileOwnershipParams{ - ActorUserId: userID, - FileIDs: fileIDs, - }) - if err != nil { - stacktrace.Propagate(err, "Failed to verify fileOwnership") - } - err = c.CollectionRepo.MoveFiles(ctx, req.ToCollectionID, req.FromCollectionID, req.Files, userID, userID) - return stacktrace.Propagate(err, "") // return nil if err is nil -} - -// RemoveFilesV3 removes files from a collection as long as owner(s) of the file is different from collection owner -func (c *CollectionController) RemoveFilesV3(ctx *gin.Context, req ente.RemoveFilesV3Request) error { - actorUserID := auth.GetUserID(ctx.Request.Header) - resp, err := c.AccessCtrl.GetCollection(ctx, &access.GetCollectionParams{ - CollectionID: req.CollectionID, - ActorUserID: actorUserID, - VerifyOwner: false, - }) - if err != nil { - return stacktrace.Propagate(err, "failed to verify collection access") - } - err = c.isRemoveAllowed(ctx, actorUserID, resp.Collection.Owner.ID, req.FileIDs) - if err != nil { - return stacktrace.Propagate(err, "file removal check failed") - } - err = c.CollectionRepo.RemoveFilesV3(ctx, req.CollectionID, req.FileIDs) - if err != nil { - return stacktrace.Propagate(err, "failed to remove files") - } - return nil -} - -// isRemoveAllowed verifies that given set of files can be removed from the collection or not -func (c *CollectionController) isRemoveAllowed(ctx *gin.Context, actorUserID int64, collectionOwnerID int64, fileIDs []int64) error { - ownerToFilesMap, err := c.FileRepo.GetOwnerToFileIDsMap(ctx, fileIDs) - if err != nil { - return stacktrace.Propagate(err, "failed to get owner to fileIDs map") - } - // verify that none of the file belongs to the collection owner - if _, ok := ownerToFilesMap[collectionOwnerID]; ok { - return ente.NewBadRequestWithMessage("can not remove files owned by album owner") - } - - if collectionOwnerID != actorUserID { - // verify that user is only trying to remove files owned by them - if len(ownerToFilesMap) > 1 { - return stacktrace.Propagate(ente.ErrPermissionDenied, "can not remove files owned by others") - } - // verify that user is only trying to remove files owned by them - if _, ok := ownerToFilesMap[actorUserID]; !ok { - return stacktrace.Propagate(ente.ErrPermissionDenied, "can not remove files owned by others") - } - } - return nil -} - -func (c *CollectionController) IsCopyAllowed(ctx *gin.Context, actorUserID int64, req ente.CopyFileSyncRequest) error { - // verify that srcCollectionID is accessible by actorUserID - if _, err := c.AccessCtrl.GetCollection(ctx, &access.GetCollectionParams{ - CollectionID: req.SrcCollectionID, - ActorUserID: actorUserID, - }); err != nil { - return stacktrace.Propagate(err, "failed to verify srcCollection access") - } - // verify that dstCollectionID is owned by actorUserID - if _, err := c.AccessCtrl.GetCollection(ctx, &access.GetCollectionParams{ - CollectionID: req.DstCollection, - ActorUserID: actorUserID, - VerifyOwner: true, - }); err != nil { - return stacktrace.Propagate(err, "failed to ownership of the dstCollection access") - } - // verify that all FileIDs exists in the srcCollection - fileIDs := make([]int64, len(req.CollectionFileItems)) - for idx, file := range req.CollectionFileItems { - fileIDs[idx] = file.ID - } - if err := c.CollectionRepo.VerifyAllFileIDsExistsInCollection(ctx, req.SrcCollectionID, fileIDs); err != nil { - return stacktrace.Propagate(err, "failed to verify fileIDs in srcCollection") - } - dsMap, err := c.FileRepo.GetOwnerToFileIDsMap(ctx, fileIDs) - if err != nil { - return err - } - // verify that none of the file belongs to actorUserID - if _, ok := dsMap[actorUserID]; ok { - return ente.NewBadRequestWithMessage("can not copy files owned by actor") - } - return nil -} - -// GetDiffV2 returns the changes in user's collections since a timestamp, along with hasMore bool flag. -func (c *CollectionController) GetDiffV2(ctx *gin.Context, cID int64, userID int64, sinceTime int64) ([]ente.File, bool, error) { - reqContextLogger := log.WithFields(log.Fields{ - "user_id": userID, - "collection_id": cID, - "since_time": sinceTime, - "req_id": requestid.Get(ctx), - }) - _, err := c.AccessCtrl.GetCollection(ctx, &access.GetCollectionParams{ - CollectionID: cID, - ActorUserID: userID, - }) - if err != nil { - return nil, false, stacktrace.Propagate(err, "failed to verify access") - } - diff, hasMore, err := c.getDiff(cID, sinceTime, CollectionDiffLimit, reqContextLogger) - if err != nil { - return nil, false, stacktrace.Propagate(err, "") - } - // hide private metadata before returning files info in diff - for idx := range diff { - if diff[idx].OwnerID != userID { - diff[idx].MagicMetadata = nil - } - if diff[idx].Metadata.EncryptedData == "-" && !diff[idx].IsDeleted { - // This indicates that the file is deleted, but we still have a stale entry in the collection - log.WithFields(log.Fields{ - "file_id": diff[idx].ID, - "collection_id": cID, - "updated_at": diff[idx].UpdationTime, - }).Warning("stale collection_file found") - diff[idx].IsDeleted = true - } - } - return diff, hasMore, nil -} - -func (c *CollectionController) GetFile(ctx *gin.Context, collectionID int64, fileID int64) (*ente.File, error) { - userID := auth.GetUserID(ctx.Request.Header) - files, err := c.CollectionRepo.GetFile(collectionID, fileID) - if err != nil { - return nil, stacktrace.Propagate(err, "") - } - if len(files) == 0 { - return nil, stacktrace.Propagate(&ente.ErrFileNotFoundInAlbum, "") - } - - file := files[0] - if file.OwnerID != userID { - cIDs, err := c.CollectionRepo.GetCollectionIDsSharedWithUser(userID) - if err != nil { - return nil, stacktrace.Propagate(err, "") - } - if !array.Int64InList(collectionID, cIDs) { - return nil, stacktrace.Propagate(ente.ErrPermissionDenied, "") - } - } - if file.IsDeleted { - return nil, stacktrace.Propagate(&ente.ErrFileNotFoundInAlbum, "") - } - return &file, nil -} - -// GetPublicDiff returns the changes in the collections since a timestamp, along with hasMore bool flag. -func (c *CollectionController) GetPublicDiff(ctx *gin.Context, sinceTime int64) ([]ente.File, bool, error) { - accessContext := auth.MustGetPublicAccessContext(ctx) - reqContextLogger := log.WithFields(log.Fields{ - "public_id": accessContext.ID, - "collection_id": accessContext.CollectionID, - "since_time": sinceTime, - "req_id": requestid.Get(ctx), - }) - diff, hasMore, err := c.getDiff(accessContext.CollectionID, sinceTime, CollectionDiffLimit, reqContextLogger) - if err != nil { - return nil, false, stacktrace.Propagate(err, "") - } - // hide private metadata before returning files info in diff - for idx := range diff { - if diff[idx].MagicMetadata != nil { - diff[idx].MagicMetadata = nil - } - } - return diff, hasMore, nil -} - -// getDiff returns the diff in user's collection since a timestamp, along with hasMore bool flag. -// The function will never return partial result for a version. To maintain this promise, it will not be able to honor -// the limit parameter. Based on the db state, compared to the limit, the diff length can be -// less (case 1), more (case 2), or same (case 3, 4) -// Example: Assume we have 11 files with following versions: v0, v1, v1, v1, v1, v1, v1, v1, v2, v2, v2 (count = 7 v1, 3 v2) -// client has synced up till version v0. -// case 1: ( sinceTime: v0, limit = 8): -// The method will discard the entries with version v2 and return only 7 entries with version v1. -// case 2: (sinceTime: v0, limit 5): -// Instead of returning 5 entries with version V1, method will return all 7 entries with version v1. -// case 3: (sinceTime: v0, limit 7): -// The method will return all 7 entries with version V1. -// case 4: (sinceTime: v0, limit >=10): -// The method will all 10 entries in the diff -func (c *CollectionController) getDiff(cID int64, sinceTime int64, limit int, logger *log.Entry) ([]ente.File, bool, error) { - // request for limit +1 files - diffLimitPlusOne, err := c.CollectionRepo.GetDiff(cID, sinceTime, limit+1) - if err != nil { - return nil, false, stacktrace.Propagate(err, "") - } - if len(diffLimitPlusOne) <= limit { - // case 4: all files changed after sinceTime are included. - return diffLimitPlusOne, false, nil - } - lastFileVersion := diffLimitPlusOne[limit].UpdationTime - filteredDiffs := c.removeFilesWithVersion(diffLimitPlusOne, lastFileVersion) - filteredDiffLen := len(filteredDiffs) - - if filteredDiffLen > 0 { // case 1 or case 3 - if filteredDiffLen < limit { - // logging case 1 - logger. - WithField("last_file_version", lastFileVersion). - WithField("filtered_diff_len", filteredDiffLen). - Info(fmt.Sprintf("less than limit (%d) files in diff", limit)) - } - return filteredDiffs, true, nil - } - // case 2 - diff, err := c.CollectionRepo.GetFilesWithVersion(cID, lastFileVersion) - logger. - WithField("last_file_version", lastFileVersion). - WithField("count", len(diff)). - Info(fmt.Sprintf("more than limit (%d) files with same version", limit)) - if err != nil { - return nil, false, stacktrace.Propagate(err, "") - } - return diff, true, nil -} - -// removeFilesWithVersion returns filtered list of files are removing all files with given version. -// Important: The method assumes that files are sorted by increasing order of File.UpdationTime -func (c *CollectionController) removeFilesWithVersion(files []ente.File, version int64) []ente.File { - var i = len(files) - 1 - for ; i >= 0; i-- { - if files[i].UpdationTime != version { - // found index (from end) where file's version is different from given version - break - } - } - return files[0 : i+1] -} - -// GetSharees returns the list of users a collection has been shared with -func (c *CollectionController) GetSharees(ctx *gin.Context, cID int64, userID int64) ([]ente.CollectionUser, error) { - _, err := c.AccessCtrl.GetCollection(ctx, &access.GetCollectionParams{ - CollectionID: cID, - ActorUserID: userID, - }) - if err != nil { - return nil, stacktrace.Propagate(err, "Access check failed") - } - sharees, err := c.CollectionRepo.GetSharees(cID) - if err != nil { - return nil, stacktrace.Propagate(err, "") - } - return sharees, nil -} - -// TrashV3 deletes a given collection and based on user input (TrashCollectionV3Request.KeepFiles as FALSE) , it will move all files present in the underlying collection -// to trash. -func (c *CollectionController) TrashV3(ctx *gin.Context, req ente.TrashCollectionV3Request) error { - if req.KeepFiles == nil { - return ente.ErrBadRequest - } - userID := auth.GetUserID(ctx.Request.Header) - cID := req.CollectionID - resp, err := c.AccessCtrl.GetCollection(ctx, &access.GetCollectionParams{ - CollectionID: cID, - ActorUserID: userID, - IncludeDeleted: true, - VerifyOwner: true, - }) - if err != nil { - return stacktrace.Propagate(err, "") - } - if !resp.Collection.AllowDelete() { - return stacktrace.Propagate(ente.ErrBadRequest, fmt.Sprintf("deleting albums of type %s is not allowed", resp.Collection.Type)) - } - if resp.Collection.IsDeleted { - log.WithFields(log.Fields{ - "c_id": cID, - "user_id": userID, - }).Warning("Collection is already deleted") - return nil - } - - if *req.KeepFiles { - // Verify that all files from this particular collections have been removed. - count, err := c.CollectionRepo.GetCollectionsFilesCount(cID) - if err != nil { - return stacktrace.Propagate(err, "") - } - if count != 0 { - return stacktrace.Propagate(&ente.ErrCollectionNotEmpty, fmt.Sprintf("Collection file count %d", count)) - } - - } - err = c.PublicCollectionCtrl.Disable(ctx, cID) - if err != nil { - return stacktrace.Propagate(err, "failed to disabled public share url") - } - err = c.CastRepo.RevokeTokenForCollection(ctx, cID) - if err != nil { - return stacktrace.Propagate(err, "failed to revoke cast token") - } - // Continue with current delete flow till. This disables sharing for this collection and then queue it up for deletion - err = c.CollectionRepo.ScheduleDelete(cID) - if err != nil { - return stacktrace.Propagate(err, "") - } - return nil -} - -// Rename updates the collection's name -func (c *CollectionController) Rename(userID int64, cID int64, encryptedName string, nameDecryptionNonce string) error { - if err := c.verifyOwnership(cID, userID); err != nil { - return stacktrace.Propagate(err, "") - } - err := c.CollectionRepo.Rename(cID, encryptedName, nameDecryptionNonce) - if err != nil { - return stacktrace.Propagate(err, "") - } - return nil -} - -// UpdateMagicMetadata updates the magic metadata for given collection -func (c *CollectionController) UpdateMagicMetadata(ctx *gin.Context, request ente.UpdateCollectionMagicMetadata, isPublicMetadata bool) error { - userID := auth.GetUserID(ctx.Request.Header) - if err := c.verifyOwnership(request.ID, userID); err != nil { - return stacktrace.Propagate(err, "") - } - // todo: verify version mismatch later. We are not planning to resync collection on clients, - // so ignore that check until then. Ideally, after file size info sync, we should enable - err := c.CollectionRepo.UpdateMagicMetadata(ctx, request.ID, request.MagicMetadata, isPublicMetadata) - if err != nil { - return stacktrace.Propagate(err, "") - } - return nil -} - -func (c *CollectionController) HandleAccountDeletion(ctx context.Context, userID int64, logger *log.Entry) error { - logger.Info("disabling shared collections with or by the user") - sharedCollections, err := c.CollectionRepo.GetAllSharedCollections(ctx, userID) - if err != nil { - return stacktrace.Propagate(err, "") - } - logger.Info(fmt.Sprintf("shared collections count: %d", len(sharedCollections))) - for _, shareCollection := range sharedCollections { - logger.WithField("shared_collection", shareCollection).Info("disable shared collection") - err = c.CollectionRepo.UnShare(shareCollection.CollectionID, shareCollection.ToUserID) - if err != nil { - return stacktrace.Propagate(err, "") - } - } - err = c.CastRepo.RevokeTokenForUser(ctx, userID) - if err != nil { - return stacktrace.Propagate(err, "failed to revoke cast token for user") - } - err = c.PublicCollectionCtrl.HandleAccountDeletion(ctx, userID, logger) - return stacktrace.Propagate(err, "") -} - -// Verify that user owns the collection -func (c *CollectionController) verifyOwnership(cID int64, userID int64) error { - collection, err := c.CollectionRepo.Get(cID) - if err != nil { - return stacktrace.Propagate(err, "") - } - if userID != collection.Owner.ID { - return stacktrace.Propagate(ente.ErrPermissionDenied, "") - } - return nil -} diff --git a/server/pkg/controller/collection_cast.go b/server/pkg/controller/collections/cast.go similarity index 98% rename from server/pkg/controller/collection_cast.go rename to server/pkg/controller/collections/cast.go index 9b975279b2..e29452e2fb 100644 --- a/server/pkg/controller/collection_cast.go +++ b/server/pkg/controller/collections/cast.go @@ -1,4 +1,4 @@ -package controller +package collections import ( "github.com/ente-io/museum/ente" diff --git a/server/pkg/controller/collections/collection.go b/server/pkg/controller/collections/collection.go new file mode 100644 index 0000000000..5f096bc133 --- /dev/null +++ b/server/pkg/controller/collections/collection.go @@ -0,0 +1,226 @@ +package collections + +import ( + "context" + "fmt" + "github.com/ente-io/museum/pkg/controller" + "github.com/ente-io/museum/pkg/controller/access" + "github.com/ente-io/museum/pkg/controller/email" + "github.com/ente-io/museum/pkg/repo/cast" + "github.com/ente-io/museum/pkg/utils/array" + "github.com/ente-io/museum/pkg/utils/auth" + "github.com/gin-gonic/gin" + + "github.com/ente-io/museum/ente" + "github.com/ente-io/museum/pkg/repo" + "github.com/ente-io/museum/pkg/utils/time" + "github.com/ente-io/stacktrace" + log "github.com/sirupsen/logrus" +) + +const ( + CollectionDiffLimit = 2500 +) + +// CollectionController encapsulates logic that deals with collections +type CollectionController struct { + PublicCollectionCtrl *controller.PublicCollectionController + EmailCtrl *email.EmailNotificationController + AccessCtrl access.Controller + BillingCtrl *controller.BillingController + CollectionRepo *repo.CollectionRepository + UserRepo *repo.UserRepository + FileRepo *repo.FileRepository + QueueRepo *repo.QueueRepository + CastRepo *cast.Repository + TaskRepo *repo.TaskLockRepository +} + +// Create creates a collection +func (c *CollectionController) Create(collection ente.Collection, ownerID int64) (ente.Collection, error) { + // The key attribute check is to ensure that user does not end up uploading any files before actually setting the key attributes. + if _, keyErr := c.UserRepo.GetKeyAttributes(ownerID); keyErr != nil { + return ente.Collection{}, stacktrace.Propagate(keyErr, "Unable to get keyAttributes") + } + collectionType := collection.Type + collection.Owner.ID = ownerID + collection.UpdationTime = time.Microseconds() + // [20th Dec 2022] Patch on server side untill majority of the existing mobile clients upgrade to a version higher > 0.7.0 + // https://github.com/ente-io/photos-app/pull/725 + if collection.Type == "CollectionType.album" { + collection.Type = "album" + } + if !array.StringInList(collection.Type, ente.ValidCollectionTypes) { + return ente.Collection{}, stacktrace.Propagate(fmt.Errorf("unexpected collection type %s", collection.Type), "") + } + collection, err := c.CollectionRepo.Create(collection) + if err != nil { + if err == ente.ErrUncategorizeCollectionAlreadyExists || err == ente.ErrFavoriteCollectionAlreadyExist { + dbCollection, err := c.CollectionRepo.GetCollectionByType(ownerID, collectionType) + if err != nil { + return ente.Collection{}, stacktrace.Propagate(err, "") + } + if dbCollection.IsDeleted { + return ente.Collection{}, stacktrace.Propagate(fmt.Errorf("special collection of type : %s is deleted", collectionType), "") + } + return dbCollection, nil + } + return ente.Collection{}, stacktrace.Propagate(err, "") + } + return collection, nil +} + +// GetCollection returns the collection for given collectionID +func (c *CollectionController) GetCollection(ctx *gin.Context, userID int64, cID int64) (ente.Collection, error) { + resp, err := c.AccessCtrl.GetCollection(ctx, &access.GetCollectionParams{ + CollectionID: cID, + ActorUserID: userID, + IncludeDeleted: true, + }) + if err != nil { + return ente.Collection{}, stacktrace.Propagate(err, "") + } + return resp.Collection, nil +} + +func (c *CollectionController) GetFile(ctx *gin.Context, collectionID int64, fileID int64) (*ente.File, error) { + userID := auth.GetUserID(ctx.Request.Header) + files, err := c.CollectionRepo.GetFile(collectionID, fileID) + if err != nil { + return nil, stacktrace.Propagate(err, "") + } + if len(files) == 0 { + return nil, stacktrace.Propagate(&ente.ErrFileNotFoundInAlbum, "") + } + + file := files[0] + if file.OwnerID != userID { + cIDs, err := c.CollectionRepo.GetCollectionIDsSharedWithUser(userID) + if err != nil { + return nil, stacktrace.Propagate(err, "") + } + if !array.Int64InList(collectionID, cIDs) { + return nil, stacktrace.Propagate(ente.ErrPermissionDenied, "") + } + } + if file.IsDeleted { + return nil, stacktrace.Propagate(&ente.ErrFileNotFoundInAlbum, "") + } + return &file, nil +} + +// TrashV3 deletes a given collection and based on user input (TrashCollectionV3Request.KeepFiles as FALSE) , it will move all files present in the underlying collection +// to trash. +func (c *CollectionController) TrashV3(ctx *gin.Context, req ente.TrashCollectionV3Request) error { + if req.KeepFiles == nil { + return ente.ErrBadRequest + } + userID := auth.GetUserID(ctx.Request.Header) + cID := req.CollectionID + resp, err := c.AccessCtrl.GetCollection(ctx, &access.GetCollectionParams{ + CollectionID: cID, + ActorUserID: userID, + IncludeDeleted: true, + VerifyOwner: true, + }) + if err != nil { + return stacktrace.Propagate(err, "") + } + if !resp.Collection.AllowDelete() { + return stacktrace.Propagate(ente.ErrBadRequest, fmt.Sprintf("deleting albums of type %s is not allowed", resp.Collection.Type)) + } + if resp.Collection.IsDeleted { + log.WithFields(log.Fields{ + "c_id": cID, + "user_id": userID, + }).Warning("Collection is already deleted") + return nil + } + + if *req.KeepFiles { + // Verify that all files from this particular collections have been removed. + count, err := c.CollectionRepo.GetCollectionsFilesCount(cID) + if err != nil { + return stacktrace.Propagate(err, "") + } + if count != 0 { + return stacktrace.Propagate(&ente.ErrCollectionNotEmpty, fmt.Sprintf("Collection file count %d", count)) + } + + } + err = c.PublicCollectionCtrl.Disable(ctx, cID) + if err != nil { + return stacktrace.Propagate(err, "failed to disabled public share url") + } + err = c.CastRepo.RevokeTokenForCollection(ctx, cID) + if err != nil { + return stacktrace.Propagate(err, "failed to revoke cast token") + } + // Continue with current delete flow till. This disables sharing for this collection and then queue it up for deletion + err = c.CollectionRepo.ScheduleDelete(cID) + if err != nil { + return stacktrace.Propagate(err, "") + } + return nil +} + +// Rename updates the collection's name +func (c *CollectionController) Rename(userID int64, cID int64, encryptedName string, nameDecryptionNonce string) error { + if err := c.verifyOwnership(cID, userID); err != nil { + return stacktrace.Propagate(err, "") + } + err := c.CollectionRepo.Rename(cID, encryptedName, nameDecryptionNonce) + if err != nil { + return stacktrace.Propagate(err, "") + } + return nil +} + +// UpdateMagicMetadata updates the magic metadata for given collection +func (c *CollectionController) UpdateMagicMetadata(ctx *gin.Context, request ente.UpdateCollectionMagicMetadata, isPublicMetadata bool) error { + userID := auth.GetUserID(ctx.Request.Header) + if err := c.verifyOwnership(request.ID, userID); err != nil { + return stacktrace.Propagate(err, "") + } + // todo: verify version mismatch later. We are not planning to resync collection on clients, + // so ignore that check until then. Ideally, after file size info sync, we should enable + err := c.CollectionRepo.UpdateMagicMetadata(ctx, request.ID, request.MagicMetadata, isPublicMetadata) + if err != nil { + return stacktrace.Propagate(err, "") + } + return nil +} + +func (c *CollectionController) HandleAccountDeletion(ctx context.Context, userID int64, logger *log.Entry) error { + logger.Info("disabling shared collections with or by the user") + sharedCollections, err := c.CollectionRepo.GetAllSharedCollections(ctx, userID) + if err != nil { + return stacktrace.Propagate(err, "") + } + logger.Info(fmt.Sprintf("shared collections count: %d", len(sharedCollections))) + for _, shareCollection := range sharedCollections { + logger.WithField("shared_collection", shareCollection).Info("disable shared collection") + err = c.CollectionRepo.UnShare(shareCollection.CollectionID, shareCollection.ToUserID) + if err != nil { + return stacktrace.Propagate(err, "") + } + } + err = c.CastRepo.RevokeTokenForUser(ctx, userID) + if err != nil { + return stacktrace.Propagate(err, "failed to revoke cast token for user") + } + err = c.PublicCollectionCtrl.HandleAccountDeletion(ctx, userID, logger) + return stacktrace.Propagate(err, "") +} + +// Verify that user owns the collection +func (c *CollectionController) verifyOwnership(cID int64, userID int64) error { + collection, err := c.CollectionRepo.Get(cID) + if err != nil { + return stacktrace.Propagate(err, "") + } + if userID != collection.Owner.ID { + return stacktrace.Propagate(ente.ErrPermissionDenied, "") + } + return nil +} diff --git a/server/pkg/controller/collections/diff.go b/server/pkg/controller/collections/diff.go new file mode 100644 index 0000000000..ad4190cfc3 --- /dev/null +++ b/server/pkg/controller/collections/diff.go @@ -0,0 +1,24 @@ +package collections + +import ( + "github.com/ente-io/museum/ente" + "github.com/ente-io/stacktrace" +) + +// GetOwnedV2 returns the list of collections owned by a user using optimized query +func (c *CollectionController) GetOwnedV2(userID int64, sinceTime int64, app ente.App, limit *int64) ([]ente.Collection, error) { + collections, err := c.CollectionRepo.GetCollectionsOwnedByUserV2(userID, sinceTime, app, limit) + if err != nil { + return nil, stacktrace.Propagate(err, "") + } + return collections, nil +} + +// GetSharedWith returns the list of collections that are shared with a user +func (c *CollectionController) GetSharedWith(userID int64, sinceTime int64, app ente.App, limit *int64) ([]ente.Collection, error) { + collections, err := c.CollectionRepo.GetCollectionsSharedWithUser(userID, sinceTime, app, limit) + if err != nil { + return nil, stacktrace.Propagate(err, "") + } + return collections, nil +} diff --git a/server/pkg/controller/collections/file_action.go b/server/pkg/controller/collections/file_action.go new file mode 100644 index 0000000000..5a85381374 --- /dev/null +++ b/server/pkg/controller/collections/file_action.go @@ -0,0 +1,203 @@ +package collections + +import ( + "fmt" + "github.com/ente-io/museum/ente" + "github.com/ente-io/museum/pkg/controller/access" + "github.com/ente-io/museum/pkg/utils/auth" + "github.com/ente-io/stacktrace" + "github.com/gin-gonic/gin" + log "github.com/sirupsen/logrus" +) + +// AddFiles adds files to a collection +func (c *CollectionController) AddFiles(ctx *gin.Context, userID int64, files []ente.CollectionFileItem, cID int64) error { + + resp, err := c.AccessCtrl.GetCollection(ctx, &access.GetCollectionParams{ + CollectionID: cID, + ActorUserID: userID, + IncludeDeleted: false, + }) + if err != nil { + return stacktrace.Propagate(err, "failed to verify collection access") + } + if !resp.Role.CanAdd() { + return stacktrace.Propagate(ente.ErrPermissionDenied, fmt.Sprintf("user %d with role %s can not add files", userID, *resp.Role)) + } + + collectionOwnerID := resp.Collection.Owner.ID + filesOwnerID := userID + // Verify that the user owns each file + fileIDs := make([]int64, 0) + for _, file := range files { + fileIDs = append(fileIDs, file.ID) + } + err = c.AccessCtrl.VerifyFileOwnership(ctx, &access.VerifyFileOwnershipParams{ + ActorUserId: userID, + FileIDs: fileIDs, + }) + + if err != nil { + return stacktrace.Propagate(err, "Failed to verify fileOwnership") + } + err = c.CollectionRepo.AddFiles(cID, collectionOwnerID, files, filesOwnerID) + if err != nil { + return stacktrace.Propagate(err, "") + } + return nil +} + +// RestoreFiles restore files from trash and add to the collection +func (c *CollectionController) RestoreFiles(ctx *gin.Context, userID int64, cID int64, files []ente.CollectionFileItem) error { + _, err := c.AccessCtrl.GetCollection(ctx, &access.GetCollectionParams{ + CollectionID: cID, + ActorUserID: userID, + IncludeDeleted: false, + VerifyOwner: true, + }) + if err != nil { + return stacktrace.Propagate(err, "failed to verify collection access") + } + // Verify that the user owns each file + for _, file := range files { + // todo #perf find owners of all files + ownerID, err := c.FileRepo.GetOwnerID(file.ID) + if err != nil { + return stacktrace.Propagate(err, "") + } + if ownerID != userID { + log.WithFields(log.Fields{ + "file_id": file.ID, + "owner_id": ownerID, + "user_id": userID, + }).Error("invalid ops: can't add file which isn't owned by user") + return stacktrace.Propagate(ente.ErrPermissionDenied, "") + } + } + err = c.CollectionRepo.RestoreFiles(ctx, userID, cID, files) + if err != nil { + return stacktrace.Propagate(err, "") + } + return nil +} + +// MoveFiles from one collection to another collection. Both the collections and files should belong to +// single user +func (c *CollectionController) MoveFiles(ctx *gin.Context, req ente.MoveFilesRequest) error { + userID := auth.GetUserID(ctx.Request.Header) + _, err := c.AccessCtrl.GetCollection(ctx, &access.GetCollectionParams{ + CollectionID: req.FromCollectionID, + ActorUserID: userID, + IncludeDeleted: false, + VerifyOwner: true, + }) + if err != nil { + return stacktrace.Propagate(err, "failed to verify if actor owns fromCollection") + } + + _, err = c.AccessCtrl.GetCollection(ctx, &access.GetCollectionParams{ + CollectionID: req.ToCollectionID, + ActorUserID: userID, + IncludeDeleted: false, + VerifyOwner: true, + }) + if err != nil { + return stacktrace.Propagate(err, "failed to verify if actor owns toCollection") + } + + // Verify that the user owns each file + fileIDs := make([]int64, 0) + for _, file := range req.Files { + fileIDs = append(fileIDs, file.ID) + } + err = c.AccessCtrl.VerifyFileOwnership(ctx, &access.VerifyFileOwnershipParams{ + ActorUserId: userID, + FileIDs: fileIDs, + }) + if err != nil { + stacktrace.Propagate(err, "Failed to verify fileOwnership") + } + err = c.CollectionRepo.MoveFiles(ctx, req.ToCollectionID, req.FromCollectionID, req.Files, userID, userID) + return stacktrace.Propagate(err, "") // return nil if err is nil +} + +// RemoveFilesV3 removes files from a collection as long as owner(s) of the file is different from collection owner +func (c *CollectionController) RemoveFilesV3(ctx *gin.Context, req ente.RemoveFilesV3Request) error { + actorUserID := auth.GetUserID(ctx.Request.Header) + resp, err := c.AccessCtrl.GetCollection(ctx, &access.GetCollectionParams{ + CollectionID: req.CollectionID, + ActorUserID: actorUserID, + VerifyOwner: false, + }) + if err != nil { + return stacktrace.Propagate(err, "failed to verify collection access") + } + err = c.isRemoveAllowed(ctx, actorUserID, resp.Collection.Owner.ID, req.FileIDs) + if err != nil { + return stacktrace.Propagate(err, "file removal check failed") + } + err = c.CollectionRepo.RemoveFilesV3(ctx, req.CollectionID, req.FileIDs) + if err != nil { + return stacktrace.Propagate(err, "failed to remove files") + } + return nil +} + +// isRemoveAllowed verifies that given set of files can be removed from the collection or not +func (c *CollectionController) isRemoveAllowed(ctx *gin.Context, actorUserID int64, collectionOwnerID int64, fileIDs []int64) error { + ownerToFilesMap, err := c.FileRepo.GetOwnerToFileIDsMap(ctx, fileIDs) + if err != nil { + return stacktrace.Propagate(err, "failed to get owner to fileIDs map") + } + // verify that none of the file belongs to the collection owner + if _, ok := ownerToFilesMap[collectionOwnerID]; ok { + return ente.NewBadRequestWithMessage("can not remove files owned by album owner") + } + + if collectionOwnerID != actorUserID { + // verify that user is only trying to remove files owned by them + if len(ownerToFilesMap) > 1 { + return stacktrace.Propagate(ente.ErrPermissionDenied, "can not remove files owned by others") + } + // verify that user is only trying to remove files owned by them + if _, ok := ownerToFilesMap[actorUserID]; !ok { + return stacktrace.Propagate(ente.ErrPermissionDenied, "can not remove files owned by others") + } + } + return nil +} + +func (c *CollectionController) IsCopyAllowed(ctx *gin.Context, actorUserID int64, req ente.CopyFileSyncRequest) error { + // verify that srcCollectionID is accessible by actorUserID + if _, err := c.AccessCtrl.GetCollection(ctx, &access.GetCollectionParams{ + CollectionID: req.SrcCollectionID, + ActorUserID: actorUserID, + }); err != nil { + return stacktrace.Propagate(err, "failed to verify srcCollection access") + } + // verify that dstCollectionID is owned by actorUserID + if _, err := c.AccessCtrl.GetCollection(ctx, &access.GetCollectionParams{ + CollectionID: req.DstCollection, + ActorUserID: actorUserID, + VerifyOwner: true, + }); err != nil { + return stacktrace.Propagate(err, "failed to ownership of the dstCollection access") + } + // verify that all FileIDs exists in the srcCollection + fileIDs := make([]int64, len(req.CollectionFileItems)) + for idx, file := range req.CollectionFileItems { + fileIDs[idx] = file.ID + } + if err := c.CollectionRepo.VerifyAllFileIDsExistsInCollection(ctx, req.SrcCollectionID, fileIDs); err != nil { + return stacktrace.Propagate(err, "failed to verify fileIDs in srcCollection") + } + dsMap, err := c.FileRepo.GetOwnerToFileIDsMap(ctx, fileIDs) + if err != nil { + return err + } + // verify that none of the file belongs to actorUserID + if _, ok := dsMap[actorUserID]; ok { + return ente.NewBadRequestWithMessage("can not copy files owned by actor") + } + return nil +} diff --git a/server/pkg/controller/collections/files_diff.go b/server/pkg/controller/collections/files_diff.go new file mode 100644 index 0000000000..8d2215df47 --- /dev/null +++ b/server/pkg/controller/collections/files_diff.go @@ -0,0 +1,111 @@ +package collections + +import ( + "fmt" + "github.com/ente-io/museum/ente" + "github.com/ente-io/museum/pkg/controller/access" + "github.com/ente-io/stacktrace" + "github.com/gin-contrib/requestid" + "github.com/gin-gonic/gin" + log "github.com/sirupsen/logrus" +) + +// GetDiffV2 returns the changes in user's collections since a timestamp, along with hasMore bool flag. +func (c *CollectionController) GetDiffV2(ctx *gin.Context, cID int64, userID int64, sinceTime int64) ([]ente.File, bool, error) { + reqContextLogger := log.WithFields(log.Fields{ + "user_id": userID, + "collection_id": cID, + "since_time": sinceTime, + "req_id": requestid.Get(ctx), + }) + _, err := c.AccessCtrl.GetCollection(ctx, &access.GetCollectionParams{ + CollectionID: cID, + ActorUserID: userID, + }) + if err != nil { + return nil, false, stacktrace.Propagate(err, "failed to verify access") + } + diff, hasMore, err := c.getDiff(cID, sinceTime, CollectionDiffLimit, reqContextLogger) + if err != nil { + return nil, false, stacktrace.Propagate(err, "") + } + // hide private metadata before returning files info in diff + for idx := range diff { + if diff[idx].OwnerID != userID { + diff[idx].MagicMetadata = nil + } + if diff[idx].Metadata.EncryptedData == "-" && !diff[idx].IsDeleted { + // This indicates that the file is deleted, but we still have a stale entry in the collection + log.WithFields(log.Fields{ + "file_id": diff[idx].ID, + "collection_id": cID, + "updated_at": diff[idx].UpdationTime, + }).Warning("stale collection_file found") + diff[idx].IsDeleted = true + } + } + return diff, hasMore, nil +} + +// getDiff returns the diff in user's collection since a timestamp, along with hasMore bool flag. +// The function will never return partial result for a version. To maintain this promise, it will not be able to honor +// the limit parameter. Based on the db state, compared to the limit, the diff length can be +// less (case 1), more (case 2), or same (case 3, 4) +// Example: Assume we have 11 files with following versions: v0, v1, v1, v1, v1, v1, v1, v1, v2, v2, v2 (count = 7 v1, 3 v2) +// client has synced up till version v0. +// case 1: ( sinceTime: v0, limit = 8): +// The method will discard the entries with version v2 and return only 7 entries with version v1. +// case 2: (sinceTime: v0, limit 5): +// Instead of returning 5 entries with version V1, method will return all 7 entries with version v1. +// case 3: (sinceTime: v0, limit 7): +// The method will return all 7 entries with version V1. +// case 4: (sinceTime: v0, limit >=10): +// The method will all 10 entries in the diff +func (c *CollectionController) getDiff(cID int64, sinceTime int64, limit int, logger *log.Entry) ([]ente.File, bool, error) { + // request for limit +1 files + diffLimitPlusOne, err := c.CollectionRepo.GetDiff(cID, sinceTime, limit+1) + if err != nil { + return nil, false, stacktrace.Propagate(err, "") + } + if len(diffLimitPlusOne) <= limit { + // case 4: all files changed after sinceTime are included. + return diffLimitPlusOne, false, nil + } + lastFileVersion := diffLimitPlusOne[limit].UpdationTime + filteredDiffs := c.removeFilesWithVersion(diffLimitPlusOne, lastFileVersion) + filteredDiffLen := len(filteredDiffs) + + if filteredDiffLen > 0 { // case 1 or case 3 + if filteredDiffLen < limit { + // logging case 1 + logger. + WithField("last_file_version", lastFileVersion). + WithField("filtered_diff_len", filteredDiffLen). + Info(fmt.Sprintf("less than limit (%d) files in diff", limit)) + } + return filteredDiffs, true, nil + } + // case 2 + diff, err := c.CollectionRepo.GetFilesWithVersion(cID, lastFileVersion) + logger. + WithField("last_file_version", lastFileVersion). + WithField("count", len(diff)). + Info(fmt.Sprintf("more than limit (%d) files with same version", limit)) + if err != nil { + return nil, false, stacktrace.Propagate(err, "") + } + return diff, true, nil +} + +// removeFilesWithVersion returns filtered list of files are removing all files with given version. +// Important: The method assumes that files are sorted by increasing order of File.UpdationTime +func (c *CollectionController) removeFilesWithVersion(files []ente.File, version int64) []ente.File { + var i = len(files) - 1 + for ; i >= 0; i-- { + if files[i].UpdationTime != version { + // found index (from end) where file's version is different from given version + break + } + } + return files[0 : i+1] +} diff --git a/server/pkg/controller/collections/share.go b/server/pkg/controller/collections/share.go new file mode 100644 index 0000000000..ced64f0fdf --- /dev/null +++ b/server/pkg/controller/collections/share.go @@ -0,0 +1,269 @@ +package collections + +import ( + "context" + "fmt" + "github.com/ente-io/museum/ente" + "github.com/ente-io/museum/pkg/controller/access" + "github.com/ente-io/museum/pkg/utils/array" + "github.com/ente-io/museum/pkg/utils/auth" + "github.com/ente-io/museum/pkg/utils/time" + "github.com/ente-io/stacktrace" + "github.com/gin-contrib/requestid" + "github.com/gin-gonic/gin" + log "github.com/sirupsen/logrus" + "strings" +) + +func (c *CollectionController) Share(ctx *gin.Context, req ente.AlterShareRequest) ([]ente.CollectionUser, error) { + fromUserID := auth.GetUserID(ctx.Request.Header) + cID := req.CollectionID + encryptedKey := req.EncryptedKey + toUserEmail := strings.ToLower(strings.TrimSpace(req.Email)) + // default role type + role := ente.VIEWER + if req.Role != nil { + role = *req.Role + } + + toUserID, err := c.UserRepo.GetUserIDWithEmail(toUserEmail) + if err != nil { + return nil, stacktrace.Propagate(err, "") + } + if toUserID == fromUserID { + return nil, stacktrace.Propagate(ente.ErrBadRequest, "Can not share collection with self") + } + collection, err := c.CollectionRepo.Get(cID) + if err != nil { + return nil, stacktrace.Propagate(err, "") + } + if !collection.AllowSharing() { + return nil, stacktrace.Propagate(ente.ErrBadRequest, fmt.Sprintf("sharing %s is not allowed", collection.Type)) + } + if fromUserID != collection.Owner.ID { + return nil, stacktrace.Propagate(ente.ErrPermissionDenied, "") + } + err = c.BillingCtrl.HasActiveSelfOrFamilySubscription(fromUserID, true) + if err != nil { + return nil, stacktrace.Propagate(err, "") + } + err = c.CollectionRepo.Share(cID, fromUserID, toUserID, encryptedKey, role, time.Microseconds()) + if err != nil { + return nil, stacktrace.Propagate(err, "") + } + sharees, err := c.GetSharees(ctx, cID, fromUserID) + if err != nil { + return nil, stacktrace.Propagate(err, "") + } + return sharees, nil +} + +func (c *CollectionController) JoinViaLink(ctx *gin.Context, req ente.JoinCollectionViaLinkRequest) error { + userID := auth.GetUserID(ctx.Request.Header) + collection, err := c.CollectionRepo.Get(req.CollectionID) + if err != nil { + return stacktrace.Propagate(err, "") + } + if collection.Owner.ID == userID { + return stacktrace.Propagate(ente.ErrBadRequest, "owner can not join via link") + } + if !collection.AllowSharing() { + return stacktrace.Propagate(ente.ErrBadRequest, fmt.Sprintf("joining %s is not allowed", collection.Type)) + } + publicCollectionToken, err := c.PublicCollectionCtrl.GetActivePublicCollectionToken(ctx, req.CollectionID) + if err != nil { + return stacktrace.Propagate(err, "") + } + + if canJoin := publicCollectionToken.CanJoin(); canJoin != nil { + return stacktrace.Propagate(ente.ErrBadRequest, fmt.Sprintf("can not join collection: %s", canJoin.Error())) + } + accessToken := auth.GetAccessToken(ctx) + if publicCollectionToken.Token != accessToken { + return stacktrace.Propagate(ente.ErrPermissionDenied, "token doesn't match collection") + } + if publicCollectionToken.PassHash != nil && *publicCollectionToken.PassHash != "" { + accessTokenJWT := auth.GetAccessTokenJWT(ctx) + if passCheckErr := c.PublicCollectionCtrl.ValidateJWTToken(ctx, accessTokenJWT, *publicCollectionToken.PassHash); passCheckErr != nil { + return stacktrace.Propagate(passCheckErr, "") + } + } + err = c.BillingCtrl.HasActiveSelfOrFamilySubscription(collection.Owner.ID, true) + if err != nil { + return stacktrace.Propagate(err, "") + } + role := ente.VIEWER + if publicCollectionToken.EnableCollect { + role = ente.COLLABORATOR + } + joinErr := c.CollectionRepo.Share(req.CollectionID, collection.Owner.ID, userID, req.EncryptedKey, role, time.Microseconds()) + if joinErr != nil { + return stacktrace.Propagate(joinErr, "") + } + go c.EmailCtrl.OnLinkJoined(collection.Owner.ID, userID, role) + return nil +} + +// UnShare unshares a collection with a user +func (c *CollectionController) UnShare(ctx *gin.Context, cID int64, fromUserID int64, toUserEmail string) ([]ente.CollectionUser, error) { + toUserID, err := c.UserRepo.GetUserIDWithEmail(toUserEmail) + if err != nil { + return nil, stacktrace.Propagate(ente.ErrNotFound, "") + } + collection, err := c.CollectionRepo.Get(cID) + if err != nil { + return nil, stacktrace.Propagate(err, "") + } + isLeavingCollection := toUserID == fromUserID + if fromUserID != collection.Owner.ID || isLeavingCollection { + return nil, stacktrace.Propagate(ente.ErrPermissionDenied, "") + } + err = c.CollectionRepo.UnShare(cID, toUserID) + if err != nil { + return nil, stacktrace.Propagate(err, "") + } + err = c.CastRepo.RevokeForGivenUserAndCollection(ctx, cID, toUserID) + if err != nil { + return nil, stacktrace.Propagate(err, "") + } + sharees, err := c.GetSharees(ctx, cID, fromUserID) + if err != nil { + return nil, stacktrace.Propagate(err, "") + } + return sharees, nil +} + +// Leave leaves the collection owned by someone else, +func (c *CollectionController) Leave(ctx *gin.Context, cID int64) error { + userID := auth.GetUserID(ctx.Request.Header) + collection, err := c.CollectionRepo.Get(cID) + if err != nil { + return stacktrace.Propagate(err, "") + } + if userID == collection.Owner.ID { + return stacktrace.Propagate(ente.ErrPermissionDenied, "can not leave collection owned by self") + } + sharedCollectionIDs, err := c.CollectionRepo.GetCollectionIDsSharedWithUser(userID) + if err != nil { + return stacktrace.Propagate(err, "") + } + if !array.Int64InList(cID, sharedCollectionIDs) { + return nil + } + err = c.CastRepo.RevokeForGivenUserAndCollection(ctx, cID, userID) + if err != nil { + return stacktrace.Propagate(err, "") + } + err = c.CollectionRepo.UnShare(cID, userID) + if err != nil { + return stacktrace.Propagate(err, "") + } + return nil +} + +func (c *CollectionController) UpdateShareeMagicMetadata(ctx *gin.Context, req ente.UpdateCollectionMagicMetadata) error { + actorUserId := auth.GetUserID(ctx.Request.Header) + resp, err := c.AccessCtrl.GetCollection(ctx, &access.GetCollectionParams{ + CollectionID: req.ID, + ActorUserID: actorUserId, + }) + if err != nil { + return stacktrace.Propagate(err, "") + } + if resp.Collection.Owner.ID == actorUserId { + return stacktrace.Propagate(ente.NewBadRequestWithMessage("owner can not update sharee magic metadata"), "") + } + err = c.CollectionRepo.UpdateShareeMetadata(req.ID, resp.Collection.Owner.ID, actorUserId, req.MagicMetadata, time.Microseconds()) + if err != nil { + return stacktrace.Propagate(err, "failed to update sharee magic metadata") + } + return nil +} + +// ShareURL generates a public auth-token for the given collectionID +func (c *CollectionController) ShareURL(ctx context.Context, userID int64, req ente.CreatePublicAccessTokenRequest) ( + ente.PublicURL, error) { + collection, err := c.CollectionRepo.Get(req.CollectionID) + if err != nil { + return ente.PublicURL{}, stacktrace.Propagate(err, "") + } + if !collection.AllowSharing() { + return ente.PublicURL{}, stacktrace.Propagate(ente.ErrBadRequest, fmt.Sprintf("sharing %s is not allowed", collection.Type)) + } + if userID != collection.Owner.ID { + return ente.PublicURL{}, stacktrace.Propagate(ente.ErrPermissionDenied, "") + } + err = c.BillingCtrl.HasActiveSelfOrFamilySubscription(userID, true) + if err != nil { + return ente.PublicURL{}, stacktrace.Propagate(err, "") + } + response, err := c.PublicCollectionCtrl.CreateAccessToken(ctx, req) + if err != nil { + return ente.PublicURL{}, stacktrace.Propagate(err, "") + } + return response, nil +} + +// UpdateShareURL updates the shared url configuration +func (c *CollectionController) UpdateShareURL(ctx context.Context, userID int64, req ente.UpdatePublicAccessTokenRequest) ( + ente.PublicURL, error) { + if err := c.verifyOwnership(req.CollectionID, userID); err != nil { + return ente.PublicURL{}, stacktrace.Propagate(err, "") + } + err := c.BillingCtrl.HasActiveSelfOrFamilySubscription(userID, true) + if err != nil { + return ente.PublicURL{}, stacktrace.Propagate(err, "") + } + response, err := c.PublicCollectionCtrl.UpdateSharedUrl(ctx, req) + if err != nil { + return ente.PublicURL{}, stacktrace.Propagate(err, "") + } + return response, nil +} + +// DisableSharedURL disable a public auth-token for the given collectionID +func (c *CollectionController) DisableSharedURL(ctx context.Context, userID int64, cID int64) error { + if err := c.verifyOwnership(cID, userID); err != nil { + return stacktrace.Propagate(err, "") + } + err := c.PublicCollectionCtrl.Disable(ctx, cID) + return stacktrace.Propagate(err, "") +} + +// GetSharees returns the list of users a collection has been shared with +func (c *CollectionController) GetSharees(ctx *gin.Context, cID int64, userID int64) ([]ente.CollectionUser, error) { + _, err := c.AccessCtrl.GetCollection(ctx, &access.GetCollectionParams{ + CollectionID: cID, + ActorUserID: userID, + }) + if err != nil { + return nil, stacktrace.Propagate(err, "Access check failed") + } + sharees, err := c.CollectionRepo.GetSharees(cID) + if err != nil { + return nil, stacktrace.Propagate(err, "") + } + return sharees, nil +} + +// GetPublicDiff returns the changes in the collections since a timestamp, along with hasMore bool flag. +func (c *CollectionController) GetPublicDiff(ctx *gin.Context, sinceTime int64) ([]ente.File, bool, error) { + accessContext := auth.MustGetPublicAccessContext(ctx) + reqContextLogger := log.WithFields(log.Fields{ + "public_id": accessContext.ID, + "collection_id": accessContext.CollectionID, + "since_time": sinceTime, + "req_id": requestid.Get(ctx), + }) + diff, hasMore, err := c.getDiff(accessContext.CollectionID, sinceTime, CollectionDiffLimit, reqContextLogger) + if err != nil { + return nil, false, stacktrace.Propagate(err, "") + } + // hide private metadata before returning files info in diff + for idx := range diff { + if diff[idx].MagicMetadata != nil { + diff[idx].MagicMetadata = nil + } + } + return diff, hasMore, nil +} diff --git a/server/pkg/controller/embedding/controller.go b/server/pkg/controller/embedding/controller.go index 190f21d40a..e1f23fa50f 100644 --- a/server/pkg/controller/embedding/controller.go +++ b/server/pkg/controller/embedding/controller.go @@ -1,485 +1,33 @@ package embedding import ( - "bytes" - "context" - "encoding/json" - "errors" - "fmt" - "github.com/aws/aws-sdk-go/aws/awserr" - "github.com/ente-io/museum/pkg/utils/array" - "strconv" - "strings" - "sync" - gTime "time" - - "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/service/s3" - "github.com/aws/aws-sdk-go/service/s3/s3manager" - "github.com/ente-io/museum/ente" "github.com/ente-io/museum/pkg/controller" - "github.com/ente-io/museum/pkg/controller/access" "github.com/ente-io/museum/pkg/repo" "github.com/ente-io/museum/pkg/repo/embedding" - "github.com/ente-io/museum/pkg/utils/auth" - "github.com/ente-io/museum/pkg/utils/network" - "github.com/ente-io/museum/pkg/utils/s3config" - "github.com/ente-io/stacktrace" - "github.com/gin-gonic/gin" - log "github.com/sirupsen/logrus" + "strconv" ) -const ( - // maxEmbeddingDataSize is the min size of an embedding object in bytes - minEmbeddingDataSize = 2048 - embeddingFetchTimeout = 10 * gTime.Second -) - -// _fetchConfig is the configuration for the fetching objects from S3 -type _fetchConfig struct { - RetryCount int - InitialTimeout gTime.Duration - MaxTimeout gTime.Duration -} - -var _defaultFetchConfig = _fetchConfig{RetryCount: 3, InitialTimeout: 10 * gTime.Second, MaxTimeout: 30 * gTime.Second} -var _b2FetchConfig = _fetchConfig{RetryCount: 3, InitialTimeout: 15 * gTime.Second, MaxTimeout: 30 * gTime.Second} - type Controller struct { - Repo *embedding.Repository - AccessCtrl access.Controller - ObjectCleanupController *controller.ObjectCleanupController - S3Config *s3config.S3Config - QueueRepo *repo.QueueRepository - TaskLockingRepo *repo.TaskLockRepository - FileRepo *repo.FileRepository - CollectionRepo *repo.CollectionRepository - HostName string - cleanupCronRunning bool - derivedStorageDataCenter string - downloadManagerCache map[string]*s3manager.Downloader + Repo *embedding.Repository + ObjectCleanupController *controller.ObjectCleanupController + QueueRepo *repo.QueueRepository + TaskLockingRepo *repo.TaskLockRepository + FileRepo *repo.FileRepository + HostName string + cleanupCronRunning bool } -func New(repo *embedding.Repository, accessCtrl access.Controller, objectCleanupController *controller.ObjectCleanupController, s3Config *s3config.S3Config, queueRepo *repo.QueueRepository, taskLockingRepo *repo.TaskLockRepository, fileRepo *repo.FileRepository, collectionRepo *repo.CollectionRepository, hostName string) *Controller { - embeddingDcs := []string{s3Config.GetHotBackblazeDC(), s3Config.GetHotWasabiDC(), s3Config.GetWasabiDerivedDC(), s3Config.GetDerivedStorageDataCenter()} - cache := make(map[string]*s3manager.Downloader, len(embeddingDcs)) - for i := range embeddingDcs { - s3Client := s3Config.GetS3Client(embeddingDcs[i]) - cache[embeddingDcs[i]] = s3manager.NewDownloaderWithClient(&s3Client) - } +func New(repo *embedding.Repository, objectCleanupController *controller.ObjectCleanupController, queueRepo *repo.QueueRepository, taskLockingRepo *repo.TaskLockRepository, fileRepo *repo.FileRepository, hostName string) *Controller { return &Controller{ - Repo: repo, - AccessCtrl: accessCtrl, - ObjectCleanupController: objectCleanupController, - S3Config: s3Config, - QueueRepo: queueRepo, - TaskLockingRepo: taskLockingRepo, - FileRepo: fileRepo, - CollectionRepo: collectionRepo, - HostName: hostName, - derivedStorageDataCenter: s3Config.GetDerivedStorageDataCenter(), - downloadManagerCache: cache, + Repo: repo, + ObjectCleanupController: objectCleanupController, + QueueRepo: queueRepo, + TaskLockingRepo: taskLockingRepo, + FileRepo: fileRepo, + HostName: hostName, } } -func (c *Controller) InsertOrUpdate(ctx *gin.Context, req ente.InsertOrUpdateEmbeddingRequest) (*ente.Embedding, error) { - userID := auth.GetUserID(ctx.Request.Header) - - err := c.AccessCtrl.VerifyFileOwnership(ctx, &access.VerifyFileOwnershipParams{ - ActorUserId: userID, - FileIDs: []int64{req.FileID}, - }) - - if err != nil { - return nil, stacktrace.Propagate(err, "User does not own file") - } - - count, err := c.CollectionRepo.GetCollectionCount(req.FileID) - if err != nil { - return nil, stacktrace.Propagate(err, "") - } - if count < 1 { - return nil, stacktrace.Propagate(ente.ErrNotFound, "") - } - version := 1 - if req.Version != nil { - version = *req.Version - } - - obj := ente.EmbeddingObject{ - Version: version, - EncryptedEmbedding: req.EncryptedEmbedding, - DecryptionHeader: req.DecryptionHeader, - Client: network.GetClientInfo(ctx), - } - size, uploadErr := c.uploadObject(obj, c.getObjectKey(userID, req.FileID, req.Model), c.derivedStorageDataCenter) - if uploadErr != nil { - log.Error(uploadErr) - return nil, stacktrace.Propagate(uploadErr, "") - } - embedding, err := c.Repo.InsertOrUpdate(ctx, userID, req, size, version, c.derivedStorageDataCenter) - embedding.Version = &version - if err != nil { - return nil, stacktrace.Propagate(err, "") - } - return &embedding, nil -} - -func (c *Controller) GetIndexedFiles(ctx *gin.Context, req ente.GetIndexedFiles) ([]ente.IndexedFile, error) { - userID := auth.GetUserID(ctx.Request.Header) - updateSince := int64(0) - if req.SinceTime != nil { - updateSince = *req.SinceTime - } - indexedFiles, err := c.Repo.GetIndexedFiles(ctx, userID, req.Model, updateSince, req.Limit) - if err != nil { - return nil, stacktrace.Propagate(err, "") - } - return indexedFiles, nil -} - -func (c *Controller) GetDiff(ctx *gin.Context, req ente.GetEmbeddingDiffRequest) ([]ente.Embedding, error) { - userID := auth.GetUserID(ctx.Request.Header) - - if req.Model == "" { - req.Model = ente.GgmlClip - } - - embeddings, err := c.Repo.GetDiff(ctx, userID, req.Model, *req.SinceTime, req.Limit) - if err != nil { - return nil, stacktrace.Propagate(err, "") - } - - // Collect object keys for embeddings with missing data - var objectKeys []string - for i := range embeddings { - if embeddings[i].EncryptedEmbedding == "" { - objectKey := c.getObjectKey(userID, embeddings[i].FileID, embeddings[i].Model) - objectKeys = append(objectKeys, objectKey) - } - } - - // Fetch missing embeddings in parallel - if len(objectKeys) > 0 { - embeddingObjects, err := c.getEmbeddingObjectsParallel(objectKeys, c.derivedStorageDataCenter) - if err != nil { - return nil, stacktrace.Propagate(err, "") - } - - // Populate missing data in embeddings from fetched objects - for i, obj := range embeddingObjects { - for j := range embeddings { - if embeddings[j].EncryptedEmbedding == "" && c.getObjectKey(userID, embeddings[j].FileID, embeddings[j].Model) == objectKeys[i] { - embeddings[j].EncryptedEmbedding = obj.EncryptedEmbedding - embeddings[j].DecryptionHeader = obj.DecryptionHeader - } - } - } - } - - return embeddings, nil -} - -func (c *Controller) GetFilesEmbedding(ctx *gin.Context, req ente.GetFilesEmbeddingRequest) (*ente.GetFilesEmbeddingResponse, error) { - userID := auth.GetUserID(ctx.Request.Header) - if err := c._validateGetFileEmbeddingsRequest(ctx, userID, req); err != nil { - return nil, stacktrace.Propagate(err, "") - } - - userFileEmbeddings, err := c.Repo.GetFilesEmbedding(ctx, userID, req.Model, req.FileIDs) - if err != nil { - return nil, stacktrace.Propagate(err, "") - } - - embeddingsWithData := make([]ente.Embedding, 0) - noEmbeddingFileIds := make([]int64, 0) - dbFileIds := make([]int64, 0) - // fileIDs that were indexed, but they don't contain any embedding information - for i := range userFileEmbeddings { - dbFileIds = append(dbFileIds, userFileEmbeddings[i].FileID) - if userFileEmbeddings[i].Size != nil && *userFileEmbeddings[i].Size < minEmbeddingDataSize { - noEmbeddingFileIds = append(noEmbeddingFileIds, userFileEmbeddings[i].FileID) - } else { - embeddingsWithData = append(embeddingsWithData, userFileEmbeddings[i]) - } - } - pendingIndexFileIds := array.FindMissingElementsInSecondList(req.FileIDs, dbFileIds) - errFileIds := make([]int64, 0) - - // Fetch missing userFileEmbeddings in parallel - embeddingObjects, err := c.getEmbeddingObjectsParallelV2(userID, embeddingsWithData, c.derivedStorageDataCenter) - if err != nil { - return nil, stacktrace.Propagate(err, "") - } - fetchedEmbeddings := make([]ente.Embedding, 0) - - // Populate missing data in userFileEmbeddings from fetched objects - for _, obj := range embeddingObjects { - if obj.err != nil { - errFileIds = append(errFileIds, obj.dbEmbeddingRow.FileID) - } else { - fetchedEmbeddings = append(fetchedEmbeddings, ente.Embedding{ - FileID: obj.dbEmbeddingRow.FileID, - Model: obj.dbEmbeddingRow.Model, - EncryptedEmbedding: obj.embeddingObject.EncryptedEmbedding, - DecryptionHeader: obj.embeddingObject.DecryptionHeader, - UpdatedAt: obj.dbEmbeddingRow.UpdatedAt, - Version: obj.dbEmbeddingRow.Version, - }) - } - } - - return &ente.GetFilesEmbeddingResponse{ - Embeddings: fetchedEmbeddings, - PendingIndexFileIDs: pendingIndexFileIds, - ErrFileIDs: errFileIds, - NoEmbeddingFileIDs: noEmbeddingFileIds, - }, nil -} - -func (c *Controller) getObjectKey(userID int64, fileID int64, model string) string { - return c.getEmbeddingObjectPrefix(userID, fileID) + model + ".json" -} - func (c *Controller) getEmbeddingObjectPrefix(userID int64, fileID int64) string { return strconv.FormatInt(userID, 10) + "/ml-data/" + strconv.FormatInt(fileID, 10) + "/" } - -// Get userId, model and fileID from the object key -func (c *Controller) getEmbeddingObjectDetails(objectKey string) (userID int64, model string, fileID int64) { - split := strings.Split(objectKey, "/") - userID, _ = strconv.ParseInt(split[0], 10, 64) - fileID, _ = strconv.ParseInt(split[2], 10, 64) - model = strings.Split(split[3], ".")[0] - return userID, model, fileID -} - -// uploadObject uploads the embedding object to the object store and returns the object size -func (c *Controller) uploadObject(obj ente.EmbeddingObject, key string, dc string) (int, error) { - embeddingObj, _ := json.Marshal(obj) - s3Client := c.S3Config.GetS3Client(dc) - s3Bucket := c.S3Config.GetBucket(dc) - uploader := s3manager.NewUploaderWithClient(&s3Client) - up := s3manager.UploadInput{ - Bucket: s3Bucket, - Key: &key, - Body: bytes.NewReader(embeddingObj), - } - result, err := uploader.Upload(&up) - if err != nil { - log.Error(err) - return -1, stacktrace.Propagate(err, "") - } - - log.Infof("Uploaded to bucket %s", result.Location) - return len(embeddingObj), nil -} - -var globalDiffFetchSemaphore = make(chan struct{}, 300) - -var globalFileFetchSemaphore = make(chan struct{}, 400) - -func (c *Controller) getEmbeddingObjectsParallel(objectKeys []string, dc string) ([]ente.EmbeddingObject, error) { - var wg sync.WaitGroup - var errs []error - embeddingObjects := make([]ente.EmbeddingObject, len(objectKeys)) - for i, objectKey := range objectKeys { - wg.Add(1) - globalDiffFetchSemaphore <- struct{}{} // Acquire from global semaphore - go func(i int, objectKey string) { - defer wg.Done() - defer func() { <-globalDiffFetchSemaphore }() // Release back to global semaphore - - obj, err := c.getEmbeddingObject(context.Background(), objectKey, dc) - if err != nil { - errs = append(errs, err) - log.Error("error fetching embedding object: "+objectKey, err) - } else { - embeddingObjects[i] = obj - } - }(i, objectKey) - } - - wg.Wait() - - if len(errs) > 0 { - return nil, stacktrace.Propagate(errors.New("failed to fetch some objects"), "") - } - - return embeddingObjects, nil -} - -type embeddingObjectResult struct { - embeddingObject ente.EmbeddingObject - dbEmbeddingRow ente.Embedding - err error -} - -func (c *Controller) getEmbeddingObjectsParallelV2(userID int64, dbEmbeddingRows []ente.Embedding, dc string) ([]embeddingObjectResult, error) { - var wg sync.WaitGroup - embeddingObjects := make([]embeddingObjectResult, len(dbEmbeddingRows)) - - for i, dbEmbeddingRow := range dbEmbeddingRows { - wg.Add(1) - globalFileFetchSemaphore <- struct{}{} // Acquire from global semaphore - go func(i int, dbEmbeddingRow ente.Embedding) { - defer wg.Done() - defer func() { <-globalFileFetchSemaphore }() // Release back to global semaphore - objectKey := c.getObjectKey(userID, dbEmbeddingRow.FileID, dbEmbeddingRow.Model) - obj, err := c.getEmbeddingObject(context.Background(), objectKey, dc) - if err != nil { - log.Error("error fetching embedding object: "+objectKey, err) - embeddingObjects[i] = embeddingObjectResult{ - err: err, - dbEmbeddingRow: dbEmbeddingRow, - } - - } else { - embeddingObjects[i] = embeddingObjectResult{ - embeddingObject: obj, - dbEmbeddingRow: dbEmbeddingRow, - } - } - }(i, dbEmbeddingRow) - } - wg.Wait() - return embeddingObjects, nil -} - -func (c *Controller) getEmbeddingObject(ctx context.Context, objectKey string, dc string) (ente.EmbeddingObject, error) { - opt := _defaultFetchConfig - if dc == c.S3Config.GetHotBackblazeDC() { - opt = _b2FetchConfig - } - ctxLogger := log.WithField("objectKey", objectKey).WithField("dc", dc) - totalAttempts := opt.RetryCount + 1 - timeout := opt.InitialTimeout - for i := 0; i < totalAttempts; i++ { - if i > 0 { - timeout = timeout * 2 - if timeout > opt.MaxTimeout { - timeout = opt.MaxTimeout - } - } - fetchCtx, cancel := context.WithTimeout(ctx, timeout) - select { - case <-ctx.Done(): - cancel() - return ente.EmbeddingObject{}, stacktrace.Propagate(ctx.Err(), "") - default: - obj, err := c.downloadObject(fetchCtx, objectKey, dc) - cancel() // Ensure cancel is called to release resources - if err == nil { - if i > 0 { - ctxLogger.Infof("Fetched object after %d attempts", i) - } - return obj, nil - } - // Check if the error is due to context timeout or cancellation - if err == nil && fetchCtx.Err() != nil { - ctxLogger.Error("Fetch timed out or cancelled: ", fetchCtx.Err()) - } else { - // check if the error is due to object not found - if s3Err, ok := err.(awserr.RequestFailure); ok { - if s3Err.Code() == s3.ErrCodeNoSuchKey { - var srcDc, destDc string - destDc = c.S3Config.GetDerivedStorageDataCenter() - // todo:(neeraj) Refactor this later to get available the DC from the DB instead of - // querying the DB. This will help in case of multiple DCs and avoid querying the DB - // for each object. - // For initial migration, as we know that original DC was b2, and if the embedding is not found - // in the new derived DC, we can try to fetch it from the B2 DC. - if c.derivedStorageDataCenter != c.S3Config.GetHotBackblazeDC() { - // embeddings ideally should ideally be in the default hot bucket b2 - srcDc = c.S3Config.GetHotBackblazeDC() - } else { - _, modelName, fileID := c.getEmbeddingObjectDetails(objectKey) - activeDcs, err := c.Repo.GetOtherDCsForFileAndModel(context.Background(), fileID, modelName, c.derivedStorageDataCenter) - if err != nil { - return ente.EmbeddingObject{}, stacktrace.Propagate(err, "failed to get other dc") - } - if len(activeDcs) > 0 { - srcDc = activeDcs[0] - } else { - ctxLogger.Error("Object not found in any dc ", s3Err) - return ente.EmbeddingObject{}, stacktrace.Propagate(errors.New("object not found"), "") - } - } - copyEmbeddingObject, err := c.copyEmbeddingObject(ctx, objectKey, srcDc, destDc) - if err == nil { - ctxLogger.Infof("Got object from dc %s", srcDc) - return *copyEmbeddingObject, nil - } else { - ctxLogger.WithError(err).Errorf("Failed to get object from fallback dc %s", srcDc) - } - return ente.EmbeddingObject{}, stacktrace.Propagate(errors.New("object not found"), "") - } - } - ctxLogger.Error("Failed to fetch object: ", err) - } - } - } - return ente.EmbeddingObject{}, stacktrace.Propagate(errors.New("failed to fetch object"), "") -} - -func (c *Controller) downloadObject(ctx context.Context, objectKey string, dc string) (ente.EmbeddingObject, error) { - var obj ente.EmbeddingObject - buff := &aws.WriteAtBuffer{} - bucket := c.S3Config.GetBucket(dc) - downloader := c.downloadManagerCache[dc] - _, err := downloader.DownloadWithContext(ctx, buff, &s3.GetObjectInput{ - Bucket: bucket, - Key: &objectKey, - }) - if err != nil { - return obj, err - } - err = json.Unmarshal(buff.Bytes(), &obj) - if err != nil { - return obj, stacktrace.Propagate(err, "unmarshal failed") - } - return obj, nil -} - -// download the embedding object from hot bucket and upload to embeddings bucket -func (c *Controller) copyEmbeddingObject(ctx context.Context, objectKey string, srcDC, destDC string) (*ente.EmbeddingObject, error) { - if srcDC == destDC { - return nil, stacktrace.Propagate(errors.New("src and dest dc can not be same"), "") - } - obj, err := c.downloadObject(ctx, objectKey, srcDC) - if err != nil { - return nil, stacktrace.Propagate(err, fmt.Sprintf("failed to download object from %s", srcDC)) - } - go func() { - userID, modelName, fileID := c.getEmbeddingObjectDetails(objectKey) - size, uploadErr := c.uploadObject(obj, objectKey, c.derivedStorageDataCenter) - if uploadErr != nil { - log.WithField("object", objectKey).Error("Failed to copy to embeddings bucket: ", uploadErr) - } - updateDcErr := c.Repo.AddNewDC(context.Background(), fileID, ente.Model(modelName), userID, size, destDC) - if updateDcErr != nil { - log.WithField("object", objectKey).Error("Failed to update dc in db: ", updateDcErr) - return - } - }() - return &obj, nil -} - -func (c *Controller) _validateGetFileEmbeddingsRequest(ctx *gin.Context, userID int64, req ente.GetFilesEmbeddingRequest) error { - if req.Model == "" { - return ente.NewBadRequestWithMessage("model is required") - } - if len(req.FileIDs) == 0 { - return ente.NewBadRequestWithMessage("fileIDs are required") - } - if len(req.FileIDs) > 200 { - return ente.NewBadRequestWithMessage("fileIDs should be less than or equal to 200") - } - if err := c.AccessCtrl.VerifyFileOwnership(ctx, &access.VerifyFileOwnershipParams{ - ActorUserId: userID, - FileIDs: req.FileIDs, - }); err != nil { - return stacktrace.Propagate(err, "User does not own some file(s)") - } - return nil -} diff --git a/server/pkg/controller/embedding/delete.go b/server/pkg/controller/embedding/delete.go index 91a70963fe..134a5192dc 100644 --- a/server/pkg/controller/embedding/delete.go +++ b/server/pkg/controller/embedding/delete.go @@ -4,24 +4,11 @@ import ( "context" "fmt" "github.com/ente-io/museum/pkg/repo" - "github.com/ente-io/museum/pkg/utils/auth" "github.com/ente-io/museum/pkg/utils/time" - "github.com/ente-io/stacktrace" - "github.com/gin-gonic/gin" log "github.com/sirupsen/logrus" "strconv" ) -func (c *Controller) DeleteAll(ctx *gin.Context) error { - userID := auth.GetUserID(ctx.Request.Header) - - err := c.Repo.DeleteAll(ctx, userID) - if err != nil { - return stacktrace.Propagate(err, "") - } - return nil -} - // CleanupDeletedEmbeddings clears all embeddings for deleted files from the object store func (c *Controller) CleanupDeletedEmbeddings() { log.Info("Cleaning up deleted embeddings") diff --git a/server/pkg/controller/family/admin.go b/server/pkg/controller/family/admin.go index fbb3506b13..204ee3dee7 100644 --- a/server/pkg/controller/family/admin.go +++ b/server/pkg/controller/family/admin.go @@ -190,6 +190,56 @@ func (c *Controller) CloseFamily(ctx context.Context, adminID int64) error { return nil } +// ModifyMemberStorage allows admin user to update the storageLimit for a member in the family +func (c *Controller) ModifyMemberStorage(ctx context.Context, actorUserID int64, id uuid.UUID, storageLimit *int64) error { + member, err := c.FamilyRepo.GetMemberById(ctx, id) + if err != nil { + return stacktrace.Propagate(err, "Couldn't fetch Family Member") + } + + if member.AdminUserID != actorUserID { + return stacktrace.Propagate(ente.ErrPermissionDenied, "you do not have sufficient permission") + } + + if member.IsAdmin { + return stacktrace.Propagate(ente.NewBadRequestWithMessage("can not limit admin storage"), "cannot modify admin storage limit") + } + + if member.Status != ente.ACCEPTED { + return stacktrace.Propagate(ente.ErrBadRequest, "user is not a part of family") + } + + // gets admin subscription in order to get the size of total storage quota (including bonus) + if storageLimit != nil { + familyMembersData, err := c.FetchMembersForAdminID(ctx, member.AdminUserID) + if err != nil { + return stacktrace.Propagate(ente.ErrBadRequest, "couldn't get active subscription") + } + totalFamilyStorage := familyMembersData.Storage + familyMembersData.AdminBonus + if *storageLimit > totalFamilyStorage { + return stacktrace.Propagate(ente.ErrStorageLimitExceeded, "potential storage limit is more than subscription storage") + } + + // Handle if the admin user tries reducing the storage Limit + // and the members Usage is more than the potential storage Limit + memberUsage, memUsageErr := c.UsageRepo.GetUsage(member.MemberUserID) + if memUsageErr != nil { + return stacktrace.Propagate(memUsageErr, "Couldn't find members storage usage") + } + + if memberUsage > *storageLimit { + return stacktrace.Propagate(ente.NewBadRequestWithMessage("Failed to reduce storage"), "User's current usage is more") + } + } + + modifyStorageErr := c.FamilyRepo.ModifyMemberStorage(ctx, actorUserID, member.ID, storageLimit) + if modifyStorageErr != nil { + return stacktrace.Propagate(modifyStorageErr, "Failed to modify members storage") + } + + return nil +} + func (c *Controller) sendNotification(ctx context.Context, adminUserID int64, memberUserID int64, newStatus ente.MemberStatus, inviteToken *string) error { adminUser, err := c.UserRepo.Get(adminUserID) if err != nil { diff --git a/server/pkg/controller/family/family.go b/server/pkg/controller/family/family.go index 9cf2a768f0..c519e50692 100644 --- a/server/pkg/controller/family/family.go +++ b/server/pkg/controller/family/family.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "github.com/ente-io/museum/pkg/controller/usercache" "github.com/ente-io/museum/pkg/utils/time" @@ -26,6 +27,7 @@ type Controller struct { UserRepo *repo.UserRepository FamilyRepo *repo.FamilyRepository UserCacheCtrl *usercache.Controller + UsageRepo *repo.UsageRepository } // FetchMembers return list of members who are part of a family plan diff --git a/server/pkg/controller/file.go b/server/pkg/controller/file.go index 7f871cc098..4deb658fc6 100644 --- a/server/pkg/controller/file.go +++ b/server/pkg/controller/file.go @@ -6,11 +6,14 @@ import ( "encoding/json" "errors" "fmt" - "github.com/ente-io/museum/pkg/controller/discord" - "github.com/ente-io/museum/pkg/utils/network" "runtime/debug" "strconv" "strings" + "sync" + gTime "time" + + "github.com/ente-io/museum/pkg/controller/discord" + "github.com/ente-io/museum/pkg/utils/network" "github.com/ente-io/museum/pkg/controller/email" "github.com/ente-io/museum/pkg/controller/lock" @@ -94,7 +97,7 @@ func (c *FileController) validateFileCreateOrUpdateReq(userID int64, file ente.F return stacktrace.Propagate(ente.ErrPermissionDenied, "collection doesn't belong to user") } if collection.IsDeleted { - return stacktrace.Propagate(ente.ErrNotFound, "collection has been deleted") + return stacktrace.Propagate(ente.ErrCollectionDeleted, "collection has been deleted") } if file.OwnerID != userID { return stacktrace.Propagate(ente.ErrPermissionDenied, "file ownerID doesn't match with userID") @@ -104,20 +107,43 @@ func (c *FileController) validateFileCreateOrUpdateReq(userID int64, file ente.F return nil } +type sizeResult struct { + size int64 + err error +} + // Create adds an entry for a file in the respective tables func (c *FileController) Create(ctx *gin.Context, userID int64, file ente.File, userAgent string, app ente.App) (ente.File, error) { + fileChan := make(chan sizeResult) + thumbChan := make(chan sizeResult) + go func() { + size, err := c.sizeOf(file.File.ObjectKey) + fileChan <- sizeResult{size, err} + }() + go func() { + size, err := c.sizeOf(file.Thumbnail.ObjectKey) + thumbChan <- sizeResult{size, err} + }() err := c.validateFileCreateOrUpdateReq(userID, file) if err != nil { return file, stacktrace.Propagate(err, "") } + // Receive results from both operations + fileResult := <-fileChan + thumbResult := <-thumbChan + hotDC := c.S3Config.GetHotDataCenter() - // sizeOf will do also HEAD check to ensure that the object exists in the - // current hot DC - fileSize, err := c.sizeOf(file.File.ObjectKey) - if err != nil { + + if fileResult.err != nil { log.Error("Could not find size of file: " + file.File.ObjectKey) - return file, stacktrace.Propagate(err, "") + return file, stacktrace.Propagate(ente.ErrObjSizeFetchFailed, fileResult.err.Error()) } + if thumbResult.err != nil { + log.Error("Could not find size of thumbnail: " + file.Thumbnail.ObjectKey) + return file, stacktrace.Propagate(ente.ErrObjSizeFetchFailed, thumbResult.err.Error()) + } + fileSize := fileResult.size + thumbnailSize := thumbResult.size if fileSize > MaxFileSize { return file, stacktrace.Propagate(ente.ErrFileTooLarge, "") } @@ -125,7 +151,6 @@ func (c *FileController) Create(ctx *gin.Context, userID int64, file ente.File, return file, stacktrace.Propagate(ente.ErrBadRequest, "mismatch in file size") } file.File.Size = fileSize - thumbnailSize, err := c.sizeOf(file.Thumbnail.ObjectKey) if err != nil { log.Error("Could not find size of thumbnail: " + file.Thumbnail.ObjectKey) return file, stacktrace.Propagate(err, "") @@ -695,14 +720,38 @@ func (c *FileController) CleanupDeletedFiles() { defer func() { c.LockController.ReleaseLock(DeletedObjectQueueLock) }() - items, err := c.QueueRepo.GetItemsReadyForDeletion(repo.DeleteObjectQueue, 1500) + items, err := c.QueueRepo.GetItemsReadyForDeletion(repo.DeleteObjectQueue, 5000) if err != nil { log.WithError(err).Error("Failed to fetch items from queue") return } - for _, i := range items { - c.cleanupDeletedFile(i) + var wg sync.WaitGroup + itemChan := make(chan repo.QueueItem, len(items)) + + // Start worker goroutines + for w := 0; w < 4; w++ { + wg.Add(1) + go func() { + defer wg.Done() + for item := range itemChan { + func(item repo.QueueItem) { + defer func() { + if r := recover(); r != nil { + log.WithField("item", item.Item).Errorf("Recovered from panic: %v", r) + } + }() + c.cleanupDeletedFile(item) + }(item) + } + }() } + // Send items to the channel + for _, item := range items { + itemChan <- item + } + close(itemChan) + // Wait for all workers to finish + wg.Wait() } func (c *FileController) GetTotalFileCount() (int64, error) { @@ -780,14 +829,23 @@ func (c *FileController) getPreSignedURLForDC(objectKey string, dc string) (stri func (c *FileController) sizeOf(objectKey string) (int64, error) { s3Client := c.S3Config.GetHotS3Client() - head, err := s3Client.HeadObject(&s3.HeadObjectInput{ - Key: &objectKey, - Bucket: c.S3Config.GetHotBucket(), - }) - if err != nil { - return -1, stacktrace.Propagate(err, "") + bucket := c.S3Config.GetHotBucket() + var head *s3.HeadObjectOutput + var err error + // Retry twice with a delay of 500ms and 1000ms + for i := 0; i < 3; i++ { + head, err = s3Client.HeadObject(&s3.HeadObjectInput{ + Key: &objectKey, + Bucket: bucket, + }) + if err == nil { + return *head.ContentLength, nil + } + if i < 2 { + gTime.Sleep(gTime.Duration(500*(i+1)) * gTime.Millisecond) + } } - return *head.ContentLength, nil + return -1, stacktrace.Propagate(err, "") } func (c *FileController) onDuplicateObjectDetected(ctx *gin.Context, file ente.File, existing ente.File, hotDC string) (ente.File, error) { diff --git a/server/pkg/controller/file_copy/file_copy.go b/server/pkg/controller/file_copy/file_copy.go index 4f9267e2e9..0d718f4264 100644 --- a/server/pkg/controller/file_copy/file_copy.go +++ b/server/pkg/controller/file_copy/file_copy.go @@ -5,6 +5,7 @@ import ( "github.com/aws/aws-sdk-go/service/s3" "github.com/ente-io/museum/ente" "github.com/ente-io/museum/pkg/controller" + "github.com/ente-io/museum/pkg/controller/collections" "github.com/ente-io/museum/pkg/repo" "github.com/ente-io/museum/pkg/utils/auth" "github.com/ente-io/museum/pkg/utils/s3config" @@ -23,7 +24,7 @@ type FileCopyController struct { S3Config *s3config.S3Config FileController *controller.FileController FileRepo *repo.FileRepository - CollectionCtrl *controller.CollectionController + CollectionCtrl *collections.CollectionController ObjectRepo *repo.ObjectRepository } diff --git a/server/pkg/controller/filedata/controller.go b/server/pkg/controller/filedata/controller.go index cf779f814e..d97cf5e42c 100644 --- a/server/pkg/controller/filedata/controller.go +++ b/server/pkg/controller/filedata/controller.go @@ -17,6 +17,7 @@ import ( "github.com/ente-io/museum/pkg/utils/network" "github.com/ente-io/museum/pkg/utils/s3config" "github.com/ente-io/stacktrace" + "github.com/gin-contrib/requestid" "github.com/gin-gonic/gin" log "github.com/sirupsen/logrus" "sync" @@ -59,7 +60,7 @@ func New(repo *fileDataRepo.Repository, fileRepo *repo.FileRepository, collectionRepo *repo.CollectionRepository, ) *Controller { - embeddingDcs := []string{s3Config.GetHotBackblazeDC(), s3Config.GetHotWasabiDC(), s3Config.GetWasabiDerivedDC(), s3Config.GetDerivedStorageDataCenter(), "b5"} + embeddingDcs := []string{s3Config.GetHotBackblazeDC(), s3Config.GetHotWasabiDC(), s3Config.GetWasabiDerivedDC(), s3Config.GetDerivedStorageDataCenter(), "b5", "b6"} cache := make(map[string]*s3manager.Downloader, len(embeddingDcs)) for i := range embeddingDcs { s3Client := s3Config.GetS3Client(embeddingDcs[i]) @@ -104,27 +105,27 @@ func (c *Controller) InsertOrUpdateMetadata(ctx *gin.Context, req *fileData.PutF Client: network.GetClientInfo(ctx), } // Start a goroutine to handle the upload and insert operations - go func() { - logger := log.WithField("objectKey", objectKey).WithField("fileID", req.FileID).WithField("type", req.Type) - size, uploadErr := c.uploadObject(obj, objectKey, bucketID) - if uploadErr != nil { - logger.WithError(uploadErr).Error("upload failed") - return - } + //go func() { + logger := log.WithField("objectKey", objectKey).WithField("fileID", req.FileID).WithField("type", req.Type) + size, uploadErr := c.uploadObject(obj, objectKey, bucketID) + if uploadErr != nil { + logger.WithError(uploadErr).Error("upload failed") + return uploadErr + } - row := fileData.Row{ - FileID: req.FileID, - Type: req.Type, - UserID: fileOwnerID, - Size: size, - LatestBucket: bucketID, - } - dbInsertErr := c.Repo.InsertOrUpdate(context.Background(), row) - if dbInsertErr != nil { - logger.WithError(dbInsertErr).Error("insert or update failed") - return - } - }() + row := fileData.Row{ + FileID: req.FileID, + Type: req.Type, + UserID: fileOwnerID, + Size: size, + LatestBucket: bucketID, + } + dbInsertErr := c.Repo.InsertOrUpdate(context.Background(), row) + if dbInsertErr != nil { + logger.WithError(dbInsertErr).Error("insert or update failed") + return uploadErr + } + //}() return nil } @@ -141,9 +142,15 @@ func (c *Controller) GetFileData(ctx *gin.Context, req fileData.GetFileData) (*f return nil, stacktrace.Propagate(err, "") } if len(doRows) == 0 || doRows[0].IsDeleted { - return nil, stacktrace.Propagate(ente.ErrNotFound, "") + return nil, stacktrace.Propagate(&ente.ErrNotFoundError, "") } - s3MetaObject, err := c.fetchS3FileMetadata(context.Background(), doRows[0], doRows[0].LatestBucket) + ctxLogger := log.WithFields(log.Fields{ + "objectKey": doRows[0].S3FileMetadataObjectKey(), + "latest_bucket": doRows[0].LatestBucket, + "req_id": requestid.Get(ctx), + "file_id": req.FileID, + }) + s3MetaObject, err := c.fetchS3FileMetadata(context.Background(), doRows[0], ctxLogger) if err != nil { return nil, stacktrace.Propagate(err, "") } @@ -180,7 +187,7 @@ func (c *Controller) GetFilesData(ctx *gin.Context, req fileData.GetFilesData) ( } pendingIndexFileIds := array.FindMissingElementsInSecondList(req.FileIDs, dbFileIds) // Fetch missing doRows in parallel - s3MetaFetchResults, err := c.getS3FileMetadataParallel(activeRows) + s3MetaFetchResults, err := c.getS3FileMetadataParallel(ctx, activeRows) if err != nil { return nil, stacktrace.Propagate(err, "") } @@ -207,7 +214,7 @@ func (c *Controller) GetFilesData(ctx *gin.Context, req fileData.GetFilesData) ( }, nil } -func (c *Controller) getS3FileMetadataParallel(dbRows []fileData.Row) ([]bulkS3MetaFetchResult, error) { +func (c *Controller) getS3FileMetadataParallel(ctx *gin.Context, dbRows []fileData.Row) ([]bulkS3MetaFetchResult, error) { var wg sync.WaitGroup embeddingObjects := make([]bulkS3MetaFetchResult, len(dbRows)) for i := range dbRows { @@ -217,10 +224,17 @@ func (c *Controller) getS3FileMetadataParallel(dbRows []fileData.Row) ([]bulkS3M go func(i int, row fileData.Row) { defer wg.Done() defer func() { <-globalFileFetchSemaphore }() // Release back to global semaphore - dc := row.LatestBucket - s3FileMetadata, err := c.fetchS3FileMetadata(context.Background(), row, dc) + + ctxLogger := log.WithFields(log.Fields{ + "objectKey": row.S3FileMetadataObjectKey(), + "req_id": requestid.Get(ctx), + "latest_bucket": row.LatestBucket, + "file_id": row.FileID, + }) + + s3FileMetadata, err := c.fetchS3FileMetadata(context.Background(), row, ctxLogger) if err != nil { - log.WithField("bucket", dc). + ctxLogger. Error("error fetching object: "+row.S3FileMetadataObjectKey(), err) embeddingObjects[i] = bulkS3MetaFetchResult{ err: err, @@ -239,10 +253,18 @@ func (c *Controller) getS3FileMetadataParallel(dbRows []fileData.Row) ([]bulkS3M return embeddingObjects, nil } -func (c *Controller) fetchS3FileMetadata(ctx context.Context, row fileData.Row, dc string) (*fileData.S3FileMetadata, error) { +func (c *Controller) fetchS3FileMetadata(ctx context.Context, row fileData.Row, ctxLogger *log.Entry) (*fileData.S3FileMetadata, error) { + dc := row.LatestBucket + // :todo:neeraj make it configurable to + // specify preferred dc to read from + // and fallback logic to read from different bucket when we fail to read from preferred dc + if dc == "b6" { + if array.StringInList("b5", row.ReplicatedBuckets) { + dc = "b5" + } + } opt := _defaultFetchConfig objectKey := row.S3FileMetadataObjectKey() - ctxLogger := log.WithField("objectKey", objectKey).WithField("dc", row.LatestBucket) totalAttempts := opt.RetryCount + 1 timeout := opt.InitialTimeout for i := 0; i < totalAttempts; i++ { @@ -262,13 +284,13 @@ func (c *Controller) fetchS3FileMetadata(ctx context.Context, row fileData.Row, cancel() // Ensure cancel is called to release resources if err == nil { if i > 0 { - ctxLogger.Infof("Fetched object after %d attempts", i) + ctxLogger.WithField("dc", dc).Infof("Fetched object after %d attempts", i) } return &obj, nil } // Check if the error is due to context timeout or cancellation if err == nil && fetchCtx.Err() != nil { - ctxLogger.Error("Fetch timed out or cancelled: ", fetchCtx.Err()) + ctxLogger.WithField("dc", dc).Error("Fetch timed out or cancelled: ", fetchCtx.Err()) } else { // check if the error is due to object not found if s3Err, ok := err.(awserr.RequestFailure); ok { @@ -276,7 +298,7 @@ func (c *Controller) fetchS3FileMetadata(ctx context.Context, row fileData.Row, return nil, stacktrace.Propagate(errors.New("object not found"), "") } } - ctxLogger.Error("Failed to fetch object: ", err) + ctxLogger.WithField("dc", dc).Error("Failed to fetch object: ", err) } } } diff --git a/server/pkg/controller/filedata/replicate.go b/server/pkg/controller/filedata/replicate.go index a17323282b..2f4a7a8b8e 100644 --- a/server/pkg/controller/filedata/replicate.go +++ b/server/pkg/controller/filedata/replicate.go @@ -29,7 +29,7 @@ func (c *Controller) StartReplication() error { workerCount := viper.GetInt("replication.file-data.worker-count") if workerCount == 0 { - workerCount = 6 + workerCount = 10 } err := c.createTemporaryStorage() if err != nil { diff --git a/server/pkg/controller/usage.go b/server/pkg/controller/usage.go index 0e02febe6b..ff4e19256e 100644 --- a/server/pkg/controller/usage.go +++ b/server/pkg/controller/usage.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "sync" "github.com/ente-io/museum/ente" bonus "github.com/ente-io/museum/ente/storagebonus" @@ -15,20 +16,47 @@ import ( // UsageController exposes functions which can be used to check around storage type UsageController struct { - BillingCtrl *BillingController - StorageBonusCtrl *storagebonus.Controller - UserCacheCtrl *usercache.Controller - UsageRepo *repo.UsageRepository - UserRepo *repo.UserRepository - FamilyRepo *repo.FamilyRepository - FileRepo *repo.FileRepository + mu sync.Mutex + BillingCtrl *BillingController + StorageBonusCtrl *storagebonus.Controller + UserCacheCtrl *usercache.Controller + UsageRepo *repo.UsageRepository + UserRepo *repo.UserRepository + FamilyRepo *repo.FamilyRepository + FileRepo *repo.FileRepository + UploadResultCache map[int64]bool } const MaxLockerFiles = 10000 +const hundredMBInBytes = 100 * 1024 * 1024 // CanUploadFile returns error if the file of given size (with StorageOverflowAboveSubscriptionLimit buffer) can be // uploaded or not. If size is not passed, it validates if current usage is less than subscription storage. func (c *UsageController) CanUploadFile(ctx context.Context, userID int64, size *int64, app ente.App) error { + // check if size is nil or less than 100 MB + if app != ente.Locker && (size == nil || *size < hundredMBInBytes) { + c.mu.Lock() + canUpload, ok := c.UploadResultCache[userID] + c.mu.Unlock() + if ok && canUpload { + go func() { + _ = c.checkAndUpdateCache(ctx, userID, size, app) + }() + return nil + } + } + return c.checkAndUpdateCache(ctx, userID, size, app) +} + +func (c *UsageController) checkAndUpdateCache(ctx context.Context, userID int64, size *int64, app ente.App) error { + err := c.canUploadFile(ctx, userID, size, app) + c.mu.Lock() + c.UploadResultCache[userID] = err == nil + c.mu.Unlock() + return err +} + +func (c *UsageController) canUploadFile(ctx context.Context, userID int64, size *int64, app ente.App) error { // If app is Locker, limit to MaxLockerFiles files if app == ente.Locker { // Get file count @@ -113,7 +141,6 @@ func (c *UsageController) CanUploadFile(ctx context.Context, userID int64, size // Get particular member's storage and check if the file size is larger than the size of the storage allocated // to the Member and fail if its too large. - if subscriptionAdminID != userID && memberStorageLimit != nil { memberUsage, memberUsageErr := c.UsageRepo.GetUsage(userID) if memberUsageErr != nil { diff --git a/server/pkg/controller/user/user.go b/server/pkg/controller/user/user.go index 7f5d5d0374..c72d50546b 100644 --- a/server/pkg/controller/user/user.go +++ b/server/pkg/controller/user/user.go @@ -3,6 +3,7 @@ package user import ( "errors" "fmt" + "github.com/ente-io/museum/pkg/controller/collections" "github.com/ente-io/museum/pkg/repo/two_factor_recovery" "strings" @@ -38,7 +39,7 @@ type UserController struct { FileRepo *repo.FileRepository CollectionRepo *repo.CollectionRepository DataCleanupRepo *datacleanup.Repository - CollectionCtrl *controller.CollectionController + CollectionCtrl *collections.CollectionController BillingRepo *repo.BillingRepository BillingController *controller.BillingController FamilyController *family.Controller @@ -101,7 +102,7 @@ func NewUserController( passkeyRepo *passkey.Repository, storageBonusRepo *storageBonusRepo.Repository, fileRepo *repo.FileRepository, - collectionController *controller.CollectionController, + collectionController *collections.CollectionController, collectionRepo *repo.CollectionRepository, dataCleanupRepository *datacleanup.Repository, billingRepo *repo.BillingRepository, diff --git a/server/pkg/controller/user/userauth.go b/server/pkg/controller/user/userauth.go index e7934b8c6a..8d66023e98 100644 --- a/server/pkg/controller/user/userauth.go +++ b/server/pkg/controller/user/userauth.go @@ -346,7 +346,7 @@ func (c *UserController) AddTokenAndNotify(userID int64, app ente.App, token str return } emailSendErr := emailUtil.SendTemplatedEmail([]string{user.Email}, "Ente", "team@ente.io", emailCtrl.LoginSuccessSubject, emailCtrl.LoginSuccessTemplate, map[string]interface{}{ - "Date": t.Now().UTC().Format("02 Jan, 2006 15:04"), + "Date": t.Now().UTC().Format("02 Jan, 2006 15:04"), }, nil) if emailSendErr != nil { log.WithError(emailSendErr).Error("Failed to send email") @@ -479,7 +479,7 @@ func (c *UserController) onVerificationSuccess(context *gin.Context, email strin func convertStringToBytes(s string) []byte { b, err := base64.StdEncoding.DecodeString(s) if err != nil { - log.Fatal(err) + panic(fmt.Sprintf("failed to base64dDecode string %s", s)) } return b } diff --git a/server/pkg/repo/collection.go b/server/pkg/repo/collection.go index f7ddcfeced..3f9af70268 100644 --- a/server/pkg/repo/collection.go +++ b/server/pkg/repo/collection.go @@ -5,7 +5,6 @@ import ( "database/sql" "fmt" "strconv" - "strings" t "time" "github.com/prometheus/client_golang/prometheus" @@ -102,56 +101,8 @@ func (repo *CollectionRepository) GetCollectionByType(userID int64, collectionTy return c, nil } -// GetCollectionsOwnedByUser returns the list of collections that a user owns -// todo: refactor this method -func (repo *CollectionRepository) GetCollectionsOwnedByUser(userID int64, updationTime int64, app ente.App) ([]ente.Collection, error) { - rows, err := repo.DB.Query(` - SELECT collections.collection_id, collections.owner_id, collections.encrypted_key, collections.key_decryption_nonce, collections.name, collections.encrypted_name, collections.name_decryption_nonce, collections.type, collections.app, collections.attributes, collections.updation_time, collections.is_deleted, collections.magic_metadata, collections.pub_magic_metadata - FROM collections - WHERE collections.owner_id = $1 AND collections.updation_time > $2 AND app = $3`, userID, updationTime, strings.ToLower(string(app))) - if err != nil { - return nil, stacktrace.Propagate(err, "") - } - defer rows.Close() - collectionIDs := make([]int64, 0) - collections := make([]ente.Collection, 0) - result := make([]ente.Collection, 0) - for rows.Next() { - var c ente.Collection - var name, encryptedName, nameDecryptionNonce sql.NullString - if err := rows.Scan(&c.ID, &c.Owner.ID, &c.EncryptedKey, &c.KeyDecryptionNonce, &name, &encryptedName, &nameDecryptionNonce, &c.Type, &c.App, &c.Attributes, &c.UpdationTime, &c.IsDeleted, &c.MagicMetadata, &c.PublicMagicMetadata); err != nil { - return collections, stacktrace.Propagate(err, "") - } - if name.Valid && len(name.String) > 0 { - c.Name = name.String - } else { - c.EncryptedName = encryptedName.String - c.NameDecryptionNonce = nameDecryptionNonce.String - } - // TODO: Pull this information in the previous query - sharees, err := repo.GetSharees(c.ID) - if err != nil { - return collections, stacktrace.Propagate(err, "") - } - c.Sharees = sharees - collections = append(collections, c) - collectionIDs = append(collectionIDs, c.ID) - } - - urlMap, err := repo.PublicCollectionRepo.GetCollectionToActivePublicURLMap(context.Background(), collectionIDs) - if err != nil { - return nil, stacktrace.Propagate(err, "failed to get publicURL info") - } - for _, c := range collections { - c.PublicURLs = urlMap[c.ID] - result = append(result, c) - } - - return result, nil -} - -func (repo *CollectionRepository) GetCollectionsOwnedByUserV2(userID int64, updationTime int64, app ente.App) ([]ente.Collection, error) { - rows, err := repo.DB.Query(` +func (repo *CollectionRepository) GetCollectionsOwnedByUserV2(userID int64, updationTime int64, app ente.App, limit *int64) ([]ente.Collection, error) { + query := ` SELECT c.collection_id, c.owner_id, c.encrypted_key,c.key_decryption_nonce, c.name, c.encrypted_name, c.name_decryption_nonce, c.type, c.app, c.attributes, c.updation_time, c.is_deleted, c.magic_metadata, c.pub_magic_metadata, users.user_id, users.encrypted_email, users.email_decryption_nonce, cs.role_type, @@ -163,7 +114,14 @@ pct.access_token, pct.valid_till, pct.device_limit, pct.created_at, pct.updated_ ON (cs.to_user_id = users.user_id AND users.encrypted_email IS NOT NULL) LEFT JOIN public_collection_tokens pct ON (pct.collection_id = c.collection_id and pct.is_disabled=FALSE) - WHERE c.owner_id = $1 AND c.updation_time > $2 and c.app = $3`, userID, updationTime, string(app)) + WHERE c.owner_id = $1 AND c.updation_time > $2 and c.app = $3` + args := []interface{}{userID, updationTime, string(app)} + + if limit != nil { + query += " ORDER BY c.updation_time ASC LIMIT $4" + args = append(args, *limit) + } + rows, err := repo.DB.Query(query, args...) if err != nil { return nil, stacktrace.Propagate(err, "") } @@ -241,14 +199,21 @@ pct.access_token, pct.valid_till, pct.device_limit, pct.created_at, pct.updated_ // GetCollectionsSharedWithUser returns the list of collections that are shared // with a user -func (repo *CollectionRepository) GetCollectionsSharedWithUser(userID int64, updationTime int64, app ente.App) ([]ente.Collection, error) { - rows, err := repo.DB.Query(` +func (repo *CollectionRepository) GetCollectionsSharedWithUser(userID int64, updationTime int64, app ente.App, limit *int64) ([]ente.Collection, error) { + query := ` SELECT collections.collection_id, collections.owner_id, users.encrypted_email, users.email_decryption_nonce, collection_shares.encrypted_key, collections.name, collections.encrypted_name, collections.name_decryption_nonce, collections.type, collections.app, collections.pub_magic_metadata, collection_shares.magic_metadata, collections.updation_time, collection_shares.is_deleted FROM collections INNER JOIN users ON collections.owner_id = users.user_id INNER JOIN collection_shares - ON collections.collection_id = collection_shares.collection_id AND collection_shares.to_user_id = $1 AND (collection_shares.updation_time > $2 OR collections.updation_time > $2) AND users.encrypted_email IS NOT NULL AND app = $3`, userID, updationTime, string(app)) + ON collections.collection_id = collection_shares.collection_id AND collection_shares.to_user_id = $1 AND (collection_shares.updation_time > $2 OR collections.updation_time > $2) AND users.encrypted_email IS NOT NULL AND app = $3` + args := []interface{}{userID, updationTime, string(app)} + if limit != nil { + query += " ORDER BY collections.updation_time ASC LIMIT $4" + args = append(args, *limit) + } + + rows, err := repo.DB.Query(query, args...) if err != nil { return nil, stacktrace.Propagate(err, "") } @@ -389,85 +354,6 @@ func (repo *CollectionRepository) GetAllSharedCollections(ctx context.Context, u return result, nil } -// DoesFileExistInCollections returns true if the file exists in one of the -// provided collections -func (repo *CollectionRepository) DoesFileExistInCollections(fileID int64, cIDs []int64) (bool, error) { - var exists bool - err := repo.DB.QueryRow(`SELECT EXISTS (SELECT 1 FROM collection_files WHERE file_id = $1 AND is_deleted = $2 AND collection_id = ANY ($3))`, - fileID, false, pq.Array(cIDs)).Scan(&exists) - return exists, stacktrace.Propagate(err, "") -} - -func (repo *CollectionRepository) DoAllFilesExistInGivenCollections(fileIDs []int64, cIDs []int64) error { - // Query to get all distinct file_ids that exist in the collections - rows, err := repo.DB.Query(` - SELECT DISTINCT file_id - FROM collection_files - WHERE file_id = ANY ($1) - AND is_deleted = false - AND collection_id = ANY ($2)`, - pq.Array(fileIDs), pq.Array(cIDs)) - - if err != nil { - return stacktrace.Propagate(err, "") - } - defer rows.Close() - - // Create a map of input fileIDs for easy lookup - fileIDMap := make(map[int64]bool) - for _, id := range fileIDs { - fileIDMap[id] = false // false means not found yet - } - // Mark files that were found - for rows.Next() { - var fileID int64 - if err := rows.Scan(&fileID); err != nil { - return stacktrace.Propagate(err, "") - } - fileIDMap[fileID] = true // mark as found - } - - if err = rows.Err(); err != nil { - return stacktrace.Propagate(err, "") - } - - // Collect missing files - var missingFiles []int64 - for id, found := range fileIDMap { - if !found { - missingFiles = append(missingFiles, id) - } - } - if len(missingFiles) > 0 { - return stacktrace.Propagate(fmt.Errorf("missing files %v", missingFiles), "") - } - return nil -} - -// VerifyAllFileIDsExistsInCollection returns error if the fileIDs don't exist in the collection -func (repo *CollectionRepository) VerifyAllFileIDsExistsInCollection(ctx context.Context, cID int64, fileIDs []int64) error { - fileIdMap := make(map[int64]bool) - rows, err := repo.DB.QueryContext(ctx, `SELECT file_id FROM collection_files WHERE collection_id = $1 AND is_deleted = $2 AND file_id = ANY ($3)`, - cID, false, pq.Array(fileIDs)) - if err != nil { - return stacktrace.Propagate(err, "") - } - for rows.Next() { - var fileID int64 - if err := rows.Scan(&fileID); err != nil { - return stacktrace.Propagate(err, "") - } - fileIdMap[fileID] = true - } - // find fileIds that are not present in the collection - for _, fileID := range fileIDs { - if _, ok := fileIdMap[fileID]; !ok { - return stacktrace.Propagate(fmt.Errorf("fileID %d not found in collection %d", fileID, cID), "") - } - } - return nil -} - // GetCollectionShareeRole returns true if the collection is shared with the user func (repo *CollectionRepository) GetCollectionShareeRole(cID int64, userID int64) (*ente.CollectionParticipantRole, error) { var role *ente.CollectionParticipantRole @@ -483,17 +369,6 @@ func (repo *CollectionRepository) GetOwnerID(collectionID int64) (int64, error) return ownerID, stacktrace.Propagate(err, "failed to get collection owner") } -// GetCollectionsFilesCount returns the number of non-deleted files which are present in the given collection -func (repo *CollectionRepository) GetCollectionsFilesCount(collectionID int64) (int64, error) { - row := repo.DB.QueryRow(`SELECT count(*) FROM collection_files WHERE collection_id=$1 AND is_deleted = false`, collectionID) - var count int64 = 0 - err := row.Scan(&count) - if err != nil { - return -1, stacktrace.Propagate(err, "") - } - return count, nil -} - // Share shares a collection with a userID func (repo *CollectionRepository) Share( collectionID int64, @@ -869,21 +744,6 @@ func (repo *CollectionRepository) GetSharees(cID int64) ([]ente.CollectionUser, return users, nil } -// GetCollectionFileIDs return list of fileIDs are currently present in the given collection -// and fileIDs are owned by the collection owner -func (repo *CollectionRepository) GetCollectionFileIDs(collectionID int64, collectionOwnerID int64) ([]int64, error) { - // Collaboration Todo: Filter out files which are not owned by the collection owner - rows, err := repo.DB.Query( - `SELECT file_id - FROM collection_files - WHERE is_deleted=false - AND collection_id =$1 AND (f_owner_id is null or f_owner_id = $2)`, collectionID, collectionOwnerID) - if err != nil { - return make([]int64, 0), stacktrace.Propagate(err, "") - } - return convertRowsToFileId(rows) -} - func convertRowsToFileId(rows *sql.Rows) ([]int64, error) { fileIDs := make([]int64, 0) defer rows.Close() @@ -1064,13 +924,3 @@ func (repo *CollectionRepository) GetSharedCollectionsCount(userID int64) (int64 } return count, nil } - -func (repo *CollectionRepository) GetCollectionCount(fileID int64) (int64, error) { - row := repo.DB.QueryRow(`SELECT count(*) FROM collection_files WHERE file_id = $1 and is_deleted = false`, fileID) - var count int64 = 0 - err := row.Scan(&count) - if err != nil { - return -1, stacktrace.Propagate(err, "") - } - return count, nil -} diff --git a/server/pkg/repo/collection_files.go b/server/pkg/repo/collection_files.go new file mode 100644 index 0000000000..9bab3720cf --- /dev/null +++ b/server/pkg/repo/collection_files.go @@ -0,0 +1,123 @@ +package repo + +import ( + "context" + "fmt" + "github.com/ente-io/stacktrace" + "github.com/lib/pq" +) + +// GetCollectionFileIDs return list of fileIDs are currently present in the given collection +// and fileIDs are owned by the collection owner +func (repo *CollectionRepository) GetCollectionFileIDs(collectionID int64, collectionOwnerID int64) ([]int64, error) { + // Collaboration Todo: Filter out files which are not owned by the collection owner + rows, err := repo.DB.Query( + `SELECT file_id + FROM collection_files + WHERE is_deleted=false + AND collection_id =$1 AND (f_owner_id is null or f_owner_id = $2)`, collectionID, collectionOwnerID) + if err != nil { + return make([]int64, 0), stacktrace.Propagate(err, "") + } + return convertRowsToFileId(rows) +} + +// DoesFileExistInCollections returns true if the file exists in one of the +// provided collections +func (repo *CollectionRepository) DoesFileExistInCollections(fileID int64, cIDs []int64) (bool, error) { + var exists bool + err := repo.DB.QueryRow(`SELECT EXISTS (SELECT 1 FROM collection_files WHERE file_id = $1 AND is_deleted = $2 AND collection_id = ANY ($3))`, + fileID, false, pq.Array(cIDs)).Scan(&exists) + return exists, stacktrace.Propagate(err, "") +} + +func (repo *CollectionRepository) DoAllFilesExistInGivenCollections(fileIDs []int64, cIDs []int64) error { + // Query to get all distinct file_ids that exist in the collections + rows, err := repo.DB.Query(` + SELECT DISTINCT file_id + FROM collection_files + WHERE file_id = ANY ($1) + AND is_deleted = false + AND collection_id = ANY ($2)`, + pq.Array(fileIDs), pq.Array(cIDs)) + + if err != nil { + return stacktrace.Propagate(err, "") + } + defer rows.Close() + + // Create a map of input fileIDs for easy lookup + fileIDMap := make(map[int64]bool) + for _, id := range fileIDs { + fileIDMap[id] = false // false means not found yet + } + // Mark files that were found + for rows.Next() { + var fileID int64 + if err := rows.Scan(&fileID); err != nil { + return stacktrace.Propagate(err, "") + } + fileIDMap[fileID] = true // mark as found + } + + if err = rows.Err(); err != nil { + return stacktrace.Propagate(err, "") + } + + // Collect missing files + var missingFiles []int64 + for id, found := range fileIDMap { + if !found { + missingFiles = append(missingFiles, id) + } + } + if len(missingFiles) > 0 { + return stacktrace.Propagate(fmt.Errorf("missing files %v", missingFiles), "") + } + return nil +} + +// VerifyAllFileIDsExistsInCollection returns error if the fileIDs don't exist in the collection +func (repo *CollectionRepository) VerifyAllFileIDsExistsInCollection(ctx context.Context, cID int64, fileIDs []int64) error { + fileIdMap := make(map[int64]bool) + rows, err := repo.DB.QueryContext(ctx, `SELECT file_id FROM collection_files WHERE collection_id = $1 AND is_deleted = $2 AND file_id = ANY ($3)`, + cID, false, pq.Array(fileIDs)) + if err != nil { + return stacktrace.Propagate(err, "") + } + for rows.Next() { + var fileID int64 + if err := rows.Scan(&fileID); err != nil { + return stacktrace.Propagate(err, "") + } + fileIdMap[fileID] = true + } + // find fileIds that are not present in the collection + for _, fileID := range fileIDs { + if _, ok := fileIdMap[fileID]; !ok { + return stacktrace.Propagate(fmt.Errorf("fileID %d not found in collection %d", fileID, cID), "") + } + } + return nil +} + +// GetCollectionsFilesCount returns the number of non-deleted files which are present in the given collection +func (repo *CollectionRepository) GetCollectionsFilesCount(collectionID int64) (int64, error) { + row := repo.DB.QueryRow(`SELECT count(*) FROM collection_files WHERE collection_id=$1 AND is_deleted = false`, collectionID) + var count int64 = 0 + err := row.Scan(&count) + if err != nil { + return -1, stacktrace.Propagate(err, "") + } + return count, nil +} + +func (repo *CollectionRepository) GetCollectionCount(fileID int64) (int64, error) { + row := repo.DB.QueryRow(`SELECT count(*) FROM collection_files WHERE file_id = $1 and is_deleted = false`, fileID) + var count int64 = 0 + err := row.Scan(&count) + if err != nil { + return -1, stacktrace.Propagate(err, "") + } + return count, nil +} diff --git a/server/pkg/repo/embedding/repository.go b/server/pkg/repo/embedding/repository.go index eb570b4e74..22eb92e8e1 100644 --- a/server/pkg/repo/embedding/repository.go +++ b/server/pkg/repo/embedding/repository.go @@ -3,12 +3,8 @@ package embedding import ( "context" "database/sql" - "errors" - "fmt" - "github.com/ente-io/museum/ente" "github.com/ente-io/stacktrace" "github.com/lib/pq" - "github.com/sirupsen/logrus" ) // Repository defines the methods for inserting, updating and retrieving @@ -17,74 +13,6 @@ type Repository struct { DB *sql.DB } -// Create inserts a new embedding -func (r *Repository) InsertOrUpdate(ctx context.Context, ownerID int64, entry ente.InsertOrUpdateEmbeddingRequest, size int, version int, dc string) (ente.Embedding, error) { - var updatedAt int64 - err := r.DB.QueryRowContext(ctx, ` - INSERT INTO embeddings - (file_id, owner_id, model, size, version, datacenters) - VALUES - ($1, $2, $3, $4, $5, ARRAY[$6]::s3region[]) - ON CONFLICT ON CONSTRAINT unique_embeddings_file_id_model - DO UPDATE - SET - updated_at = now_utc_micro_seconds(), - size = $4, - version = $5, - datacenters = CASE - WHEN $6 = ANY(COALESCE(embeddings.datacenters, ARRAY['b2-eu-cen']::s3region[])) THEN embeddings.datacenters - ELSE array_append(COALESCE(embeddings.datacenters, ARRAY['b2-eu-cen']::s3region[]), $6::s3region) - END - RETURNING updated_at`, - entry.FileID, ownerID, entry.Model, size, version, dc).Scan(&updatedAt) - - if err != nil { - // check if error is due to model enum invalid value - if err.Error() == fmt.Sprintf("pq: invalid input value for enum model: \"%s\"", entry.Model) { - return ente.Embedding{}, stacktrace.Propagate(ente.ErrBadRequest, "invalid model value") - } - return ente.Embedding{}, stacktrace.Propagate(err, "") - } - return ente.Embedding{ - FileID: entry.FileID, - Model: entry.Model, - EncryptedEmbedding: entry.EncryptedEmbedding, - DecryptionHeader: entry.DecryptionHeader, - UpdatedAt: updatedAt, - }, nil -} - -// GetDiff returns the embeddings that have been updated since the given time -func (r *Repository) GetDiff(ctx context.Context, ownerID int64, model ente.Model, sinceTime int64, limit int16) ([]ente.Embedding, error) { - rows, err := r.DB.QueryContext(ctx, `SELECT file_id, model, encrypted_embedding, decryption_header, updated_at, version, size - FROM embeddings - WHERE owner_id = $1 AND model = $2 AND updated_at > $3 - ORDER BY updated_at ASC - LIMIT $4`, ownerID, model, sinceTime, limit) - if err != nil { - return nil, stacktrace.Propagate(err, "") - } - return convertRowsToEmbeddings(rows) -} - -func (r *Repository) GetFilesEmbedding(ctx context.Context, ownerID int64, model ente.Model, fileIDs []int64) ([]ente.Embedding, error) { - rows, err := r.DB.QueryContext(ctx, `SELECT file_id, model, encrypted_embedding, decryption_header, updated_at, version, size - FROM embeddings - WHERE owner_id = $1 AND model = $2 AND file_id = ANY($3)`, ownerID, model, pq.Array(fileIDs)) - if err != nil { - return nil, stacktrace.Propagate(err, "") - } - return convertRowsToEmbeddings(rows) -} - -func (r *Repository) DeleteAll(ctx context.Context, ownerID int64) error { - _, err := r.DB.ExecContext(ctx, "DELETE FROM embeddings WHERE owner_id = $1", ownerID) - if err != nil { - return stacktrace.Propagate(err, "") - } - return nil -} - func (r *Repository) Delete(fileID int64) error { _, err := r.DB.Exec("DELETE FROM embeddings WHERE file_id = $1", fileID) if err != nil { @@ -117,33 +45,6 @@ func (r *Repository) GetDatacenters(ctx context.Context, fileID int64) ([]string return datacenters, nil } -// GetOtherDCsForFileAndModel returns the list of datacenters where the embeddings are stored for a given file and model, excluding the ignoredDC -func (r *Repository) GetOtherDCsForFileAndModel(ctx context.Context, fileID int64, model string, ignoredDC string) ([]string, error) { - rows, err := r.DB.QueryContext(ctx, `SELECT datacenters FROM embeddings WHERE file_id = $1 AND model = $2`, fileID, model) - if err != nil { - return nil, stacktrace.Propagate(err, "") - } - uniqueDatacenters := make(map[string]bool) - for rows.Next() { - var datacenters []string - err = rows.Scan(pq.Array(&datacenters)) - if err != nil { - return nil, stacktrace.Propagate(err, "") - } - for _, dc := range datacenters { - // add to uniqueDatacenters if it is not the ignoredDC - if dc != ignoredDC { - uniqueDatacenters[dc] = true - } - } - } - datacenters := make([]string, 0, len(uniqueDatacenters)) - for dc := range uniqueDatacenters { - datacenters = append(datacenters, dc) - } - return datacenters, nil -} - // RemoveDatacenter removes the given datacenter from the list of datacenters func (r *Repository) RemoveDatacenter(ctx context.Context, fileID int64, dc string) error { _, err := r.DB.ExecContext(ctx, `UPDATE embeddings SET datacenters = array_remove(datacenters, $1) WHERE file_id = $2`, dc, fileID) @@ -152,87 +53,3 @@ func (r *Repository) RemoveDatacenter(ctx context.Context, fileID int64, dc stri } return nil } - -// AddNewDC adds the dc name to the list of datacenters, if it doesn't exist already, for a given file, model and user. It also updates the size of the embedding -func (r *Repository) AddNewDC(ctx context.Context, fileID int64, model ente.Model, userID int64, size int, dc string) error { - res, err := r.DB.ExecContext(ctx, ` - UPDATE embeddings - SET size = $1, - datacenters = CASE - WHEN $2::s3region = ANY(datacenters) THEN datacenters - ELSE array_append(datacenters, $2::s3region) - END - WHERE file_id = $3 AND model = $4 AND owner_id = $5`, size, dc, fileID, model, userID) - if err != nil { - return stacktrace.Propagate(err, "") - } - rowsAffected, err := res.RowsAffected() - if err != nil { - return stacktrace.Propagate(err, "") - } - if rowsAffected == 0 { - return stacktrace.Propagate(errors.New("no row got updated"), "") - } - return nil -} - -func (r *Repository) GetIndexedFiles(ctx context.Context, id int64, model ente.Model, since int64, limit *int64) ([]ente.IndexedFile, error) { - var rows *sql.Rows - var err error - if limit == nil { - rows, err = r.DB.QueryContext(ctx, `SELECT file_id, updated_at FROM embeddings WHERE owner_id = $1 AND model = $2 AND updated_at > $3`, id, model, since) - } else { - rows, err = r.DB.QueryContext(ctx, `SELECT file_id, updated_at FROM embeddings WHERE owner_id = $1 AND model = $2 AND updated_at > $3 LIMIT $4`, id, model, since, *limit) - } - if err != nil { - return nil, stacktrace.Propagate(err, "") - } - defer func() { - if err := rows.Close(); err != nil { - logrus.Error(err) - } - }() - result := make([]ente.IndexedFile, 0) - for rows.Next() { - var meta ente.IndexedFile - err := rows.Scan(&meta.FileID, &meta.UpdatedAt) - if err != nil { - return nil, stacktrace.Propagate(err, "") - } - result = append(result, meta) - } - return result, nil - -} - -func convertRowsToEmbeddings(rows *sql.Rows) ([]ente.Embedding, error) { - defer func() { - if err := rows.Close(); err != nil { - logrus.Error(err) - } - }() - - result := make([]ente.Embedding, 0) - for rows.Next() { - embedding := ente.Embedding{} - var encryptedEmbedding, decryptionHeader sql.NullString - var version sql.NullInt32 - err := rows.Scan(&embedding.FileID, &embedding.Model, &encryptedEmbedding, &decryptionHeader, &embedding.UpdatedAt, &version, &embedding.Size) - if encryptedEmbedding.Valid && len(encryptedEmbedding.String) > 0 { - embedding.EncryptedEmbedding = encryptedEmbedding.String - } - if decryptionHeader.Valid && len(decryptionHeader.String) > 0 { - embedding.DecryptionHeader = decryptionHeader.String - } - v := 1 - if version.Valid { - v = int(version.Int32) - } - embedding.Version = &v - if err != nil { - return nil, stacktrace.Propagate(err, "") - } - result = append(result, embedding) - } - return result, nil -} diff --git a/server/pkg/repo/family.go b/server/pkg/repo/family.go index 8043ebab1b..fad14d89aa 100644 --- a/server/pkg/repo/family.go +++ b/server/pkg/repo/family.go @@ -196,6 +196,16 @@ func (repo *FamilyRepository) RemoveMember(ctx context.Context, adminID int64, m return stacktrace.Propagate(tx.Commit(), "failed to commit") } +// UpdateStorage is used to set Pre-existing Members Storage Limit. +func (repo *FamilyRepository) ModifyMemberStorage(ctx context.Context, adminID int64, id uuid.UUID, storageLimit *int64) error { + _, err := repo.DB.Exec(`UPDATE families SET storage_limit=$1 where id=$2`, storageLimit, id) + if err != nil { + return stacktrace.Propagate(err, "Could not update Members Storage Limit") + } + + return stacktrace.Propagate(err, "Failed to Modify Members Storage Limit") +} + // RevokeInvite revokes the invitation invite func (repo *FamilyRepository) RevokeInvite(ctx context.Context, adminID int64, memberID int64) error { tx, err := repo.DB.BeginTx(ctx, nil) diff --git a/server/pkg/repo/public_collection.go b/server/pkg/repo/public_collection.go index 3b5aa6dbce..f5ae8f2d72 100644 --- a/server/pkg/repo/public_collection.go +++ b/server/pkg/repo/public_collection.go @@ -38,7 +38,7 @@ func (pcr *PublicCollectionRepository) GetAlbumUrl(token string) string { func (pcr *PublicCollectionRepository) Insert(ctx context.Context, cID int64, token string, validTill int64, deviceLimit int, enableCollect bool, enableJoin *bool) error { // default value for enableJoin is true - join := false + join := true if enableJoin != nil { join = *enableJoin } diff --git a/server/scripts/compose/credentials.yaml b/server/scripts/compose/credentials.yaml index d20532ec94..644743b657 100644 --- a/server/scripts/compose/credentials.yaml +++ b/server/scripts/compose/credentials.yaml @@ -8,21 +8,21 @@ db: s3: are_local_buckets: true b2-eu-cen: - key: test - secret: testtest + key: changeme + secret: changeme1234 endpoint: localhost:3200 region: eu-central-2 bucket: b2-eu-cen wasabi-eu-central-2-v3: - key: test - secret: testtest + key: changeme + secret: changeme1234 endpoint: localhost:3200 region: eu-central-2 bucket: wasabi-eu-central-2-v3 compliance: false scw-eu-fr-v3: - key: test - secret: testtest + key: changeme + secret: changeme1234 endpoint: localhost:3200 region: eu-central-2 bucket: scw-eu-fr-v3 diff --git a/server/scripts/compose/minio-provision.sh b/server/scripts/compose/minio-provision.sh index 34e3b98b88..ae60318804 100755 --- a/server/scripts/compose/minio-provision.sh +++ b/server/scripts/compose/minio-provision.sh @@ -3,7 +3,7 @@ # Script used to prepare the minio instance that runs as part of the development # Docker compose cluster. -while ! mc config host add h0 http://minio:3200 test testtest +while ! mc config host add h0 http://minio:3200 changeme changeme1234 do echo "waiting for minio..." sleep 0.5 diff --git a/web/.prettierignore b/web/.prettierignore index c94d62482f..c2ed1358c4 100644 --- a/web/.prettierignore +++ b/web/.prettierignore @@ -1,2 +1 @@ thirdparty/ -public/ diff --git a/web/apps/accounts/src/pages/_app.tsx b/web/apps/accounts/src/pages/_app.tsx index e510a5d75d..785150f50d 100644 --- a/web/apps/accounts/src/pages/_app.tsx +++ b/web/apps/accounts/src/pages/_app.tsx @@ -1,17 +1,18 @@ import { staticAppTitle } from "@/base/app"; +import { assertionFailed } from "@/base/assert"; import { CustomHead } from "@/base/components/Head"; import { LoadingIndicator } from "@/base/components/loaders"; import { AttributedMiniDialog } from "@/base/components/MiniDialog"; import { useAttributedMiniDialog } from "@/base/components/utils/dialog"; import { useSetupI18n, useSetupLogs } from "@/base/components/utils/hooks-app"; import { photosTheme } from "@/base/components/utils/theme"; +import { BaseContext, deriveBaseContext } from "@/base/context"; import "@fontsource-variable/inter"; import { CssBaseline } from "@mui/material"; import { ThemeProvider } from "@mui/material/styles"; import { t } from "i18next"; import type { AppProps } from "next/app"; import React, { useMemo } from "react"; -import { AppContext } from "../types/context"; const App: React.FC = ({ Component, pageProps }) => { useSetupLogs({ disableDiskLogs: true }); @@ -19,24 +20,30 @@ const App: React.FC = ({ Component, pageProps }) => { const isI18nReady = useSetupI18n(); const { showMiniDialog, miniDialogProps } = useAttributedMiniDialog(); - const appContext = useMemo(() => ({ showMiniDialog }), [showMiniDialog]); + // No code in the accounts app is currently expected to reach a code path + // where they would need to "logout". Also, the accounts app doesn't store + // any user specific persistent state that'd need to be cleared, so there + // really isn't anything to do here even if we needed to. + const logout = assertionFailed; + + const baseContext = useMemo( + () => deriveBaseContext({ logout, showMiniDialog }), + [logout, showMiniDialog], + ); const title = isI18nReady ? t("title_accounts") : staticAppTitle; return ( - <> + + + - - - - - - {!isI18nReady && } - {isI18nReady && } - - - + + {!isI18nReady && } + {isI18nReady && } + + ); }; diff --git a/web/apps/accounts/src/pages/passkeys/index.tsx b/web/apps/accounts/src/pages/passkeys/index.tsx index 597c319527..3e3a553907 100644 --- a/web/apps/accounts/src/pages/passkeys/index.tsx +++ b/web/apps/accounts/src/pages/passkeys/index.tsx @@ -10,9 +10,10 @@ import { SingleInputDialog } from "@/base/components/SingleInputDialog"; import { Titlebar } from "@/base/components/Titlebar"; import { errorDialogAttributes } from "@/base/components/utils/dialog"; import { useModalVisibility } from "@/base/components/utils/modal"; +import { useBaseContext } from "@/base/context"; +import { formattedDateTime } from "@/base/i18n-date"; import log from "@/base/log"; import SingleInputForm from "@ente/shared/components/SingleInputForm"; -import { formatDateTimeFull } from "@ente/shared/time/format"; import CalendarTodayIcon from "@mui/icons-material/CalendarToday"; import ChevronRightIcon from "@mui/icons-material/ChevronRight"; import DeleteIcon from "@mui/icons-material/Delete"; @@ -28,10 +29,9 @@ import { renamePasskey, type Passkey, } from "services/passkey"; -import { useAppContext } from "../../types/context"; const Page: React.FC = () => { - const { showMiniDialog } = useAppContext(); + const { showMiniDialog } = useBaseContext(); const [token, setToken] = useState(); const [passkeys, setPasskeys] = useState([]); @@ -252,7 +252,7 @@ const ManagePasskeyDrawer: React.FC = ({ passkey, onUpdateOrDeletePasskey, }) => { - const { showMiniDialog } = useAppContext(); + const { showMiniDialog } = useBaseContext(); const { show: showRenameDialog, props: renameDialogVisibilityProps } = useModalVisibility(); @@ -293,7 +293,7 @@ const ManagePasskeyDrawer: React.FC = ({ onRootClose={onClose} /> - {formatDateTimeFull(passkey.createdAt / 1000)} + {formattedDateTime(passkey.createdAt)} ; - -/** - * The React {@link Context} available to all nodes in the React tree. - */ -export const AppContext = createContext(undefined); - -/** - * Utility hook to get the {@link AppContextT} expected to be available to all - * React components in the Accounts app's React tree. - */ -export const useAppContext = (): AppContextT => useContext(AppContext)!; diff --git a/web/apps/accounts/tsconfig.json b/web/apps/accounts/tsconfig.json index 81071f5376..8e6cbbeecf 100644 --- a/web/apps/accounts/tsconfig.json +++ b/web/apps/accounts/tsconfig.json @@ -2,9 +2,7 @@ "extends": "@/build-config/tsconfig-next.json", "compilerOptions": { /* Set the base directory from which to resolve bare module names. */ - "baseUrl": "./src", - /* MUI doesn't work with exactOptionalPropertyTypes yet. */ - "exactOptionalPropertyTypes": false + "baseUrl": "./src" }, "include": [ "src", diff --git a/web/apps/auth/public/.well-known/assetlinks.json b/web/apps/auth/public/.well-known/assetlinks.json index cee8007fca..692be7e7a3 100644 --- a/web/apps/auth/public/.well-known/assetlinks.json +++ b/web/apps/auth/public/.well-known/assetlinks.json @@ -1,26 +1,20 @@ [ { - "relation": [ - "delegate_permission/common.get_login_creds" - ], + "relation": ["delegate_permission/common.get_login_creds"], "target": { "namespace": "photos-web", "site": "https://web.ente.io" } }, { - "relation": [ - "delegate_permission/common.get_login_creds" - ], + "relation": ["delegate_permission/common.get_login_creds"], "target": { "namespace": "auth-web", "site": "https://auth.ente.io" } }, { - "relation": [ - "delegate_permission/common.get_login_creds" - ], + "relation": ["delegate_permission/common.get_login_creds"], "target": { "namespace": "android_app", "package_name": "io.ente.photos.independent", @@ -30,9 +24,7 @@ } }, { - "relation": [ - "delegate_permission/common.get_login_creds" - ], + "relation": ["delegate_permission/common.get_login_creds"], "target": { "namespace": "android_app", "package_name": "io.ente.photos", @@ -42,9 +34,7 @@ } }, { - "relation": [ - "delegate_permission/common.get_login_creds" - ], + "relation": ["delegate_permission/common.get_login_creds"], "target": { "namespace": "android_app", "package_name": "io.ente.auth", diff --git a/web/apps/auth/src/pages/404.tsx b/web/apps/auth/src/pages/404.tsx index 55b8b0b860..201a7b79ff 100644 --- a/web/apps/auth/src/pages/404.tsx +++ b/web/apps/auth/src/pages/404.tsx @@ -1,3 +1 @@ -import Page from "@/base/components/pages/404"; - -export default Page; +export { default } from "@/base/components/pages/404"; diff --git a/web/apps/auth/src/pages/_app.tsx b/web/apps/auth/src/pages/_app.tsx index e3c92a117a..ba6939fb20 100644 --- a/web/apps/auth/src/pages/_app.tsx +++ b/web/apps/auth/src/pages/_app.tsx @@ -13,13 +13,10 @@ import { useSetupLogs, } from "@/base/components/utils/hooks-app"; import { authTheme } from "@/base/components/utils/theme"; +import { BaseContext, deriveBaseContext } from "@/base/context"; import { logStartupBanner } from "@/base/log-web"; import HTTPService from "@ente/shared/network/HTTPService"; -import { - LS_KEYS, - getData, - migrateKVToken, -} from "@ente/shared/storage/localStorage"; +import { LS_KEYS, getData } from "@ente/shared/storage/localStorage"; import type { User } from "@ente/shared/user/types"; import "@fontsource-variable/inter"; import { CssBaseline } from "@mui/material"; @@ -27,7 +24,6 @@ import { ThemeProvider } from "@mui/material/styles"; import { t } from "i18next"; import type { AppProps } from "next/app"; import React, { useCallback, useEffect, useMemo } from "react"; -import { AppContext } from "types/context"; const App: React.FC = ({ Component, pageProps }) => { useSetupLogs(); @@ -38,7 +34,6 @@ const App: React.FC = ({ Component, pageProps }) => { useEffect(() => { const user = getData(LS_KEYS.USER) as User | undefined | null; - void migrateKVToken(user); logStartupBanner(user?.id); HTTPService.setHeaders({ "X-Client-Package": clientPackageName }); }, []); @@ -47,37 +42,30 @@ const App: React.FC = ({ Component, pageProps }) => { void accountLogout().then(() => window.location.replace("/")); }, []); - const appContext = useMemo( - () => ({ - logout, - showMiniDialog, - }), + const baseContext = useMemo( + () => deriveBaseContext({ logout, showMiniDialog }), [logout, showMiniDialog], ); const title = isI18nReady ? t("title_auth") : staticAppTitle; return ( - <> + + + - - - - - - - {!isI18nReady ? ( - - ) : ( - <> - {isChangingRoute && } - - - )} - - - + + {!isI18nReady ? ( + + ) : ( + <> + {isChangingRoute && } + + + )} + + ); }; diff --git a/web/apps/auth/src/pages/auth.tsx b/web/apps/auth/src/pages/auth.tsx index cdf2195439..eb4ddee58a 100644 --- a/web/apps/auth/src/pages/auth.tsx +++ b/web/apps/auth/src/pages/auth.tsx @@ -8,9 +8,10 @@ import { OverflowMenu, OverflowMenuOption, } from "@/base/components/OverflowMenu"; +import { useBaseContext } from "@/base/context"; import { isHTTP401Error } from "@/base/http"; import log from "@/base/log"; -import { masterKeyFromSessionIfLoggedIn } from "@/base/session-store"; +import { masterKeyFromSessionIfLoggedIn } from "@/base/session"; import { AUTH_PAGES as PAGES } from "@ente/shared/constants/pages"; import LogoutOutlinedIcon from "@mui/icons-material/LogoutOutlined"; import { @@ -28,10 +29,9 @@ 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 { useAppContext } from "types/context"; const Page: React.FC = () => { - const { logout, showMiniDialog } = useAppContext(); + const { logout, showMiniDialog } = useBaseContext(); const router = useRouter(); const [codes, setCodes] = useState([]); @@ -57,7 +57,7 @@ const Page: React.FC = () => { setHasFetched(true); }; void fetchCodes(); - }, [router, showMiniDialog, logout]); + }, [router, logout, showMiniDialog]); const lcSearch = searchTerm.toLowerCase(); const filteredCodes = codes.filter( @@ -131,7 +131,7 @@ const Page: React.FC = () => { export default Page; const AuthNavbar: React.FC = () => { - const { logout } = useAppContext(); + const { logout } = useBaseContext(); return ( = ({ code }) => { ({ - backgroundColor: theme.vars.palette.fill.faint, - color: theme.vars.palette.primary.main, - backdropFilter: "blur(10px)", - }), + slotProps={{ + content: { + sx: { + backgroundColor: "fill.faint", + color: "primary.main", + backdropFilter: "blur(10px)", + }, + }, }} /> diff --git a/web/apps/auth/src/pages/change-password.tsx b/web/apps/auth/src/pages/change-password.tsx index 3402664eb7..bea6b02df4 100644 --- a/web/apps/auth/src/pages/change-password.tsx +++ b/web/apps/auth/src/pages/change-password.tsx @@ -1,3 +1 @@ -import Page from "@/accounts/pages/change-password"; - -export default Page; +export { default } from "@/accounts/pages/change-password"; diff --git a/web/apps/auth/src/pages/credentials.tsx b/web/apps/auth/src/pages/credentials.tsx index 17dd9fef70..9bb9307c79 100644 --- a/web/apps/auth/src/pages/credentials.tsx +++ b/web/apps/auth/src/pages/credentials.tsx @@ -1,6 +1 @@ -import Page_ from "@/accounts/pages/credentials"; -import { useAppContext } from "types/context"; - -const Page = () => ; - -export default Page; +export { default } from "@/accounts/pages/credentials"; diff --git a/web/apps/auth/src/pages/generate.tsx b/web/apps/auth/src/pages/generate.tsx index 12723d0b0a..bf45ab3f5a 100644 --- a/web/apps/auth/src/pages/generate.tsx +++ b/web/apps/auth/src/pages/generate.tsx @@ -1,6 +1 @@ -import Page_ from "@/accounts/pages/generate"; -import { useAppContext } from "types/context"; - -const Page = () => ; - -export default Page; +export { default } from "@/accounts/pages/generate"; diff --git a/web/apps/auth/src/pages/login.tsx b/web/apps/auth/src/pages/login.tsx index 46a8d69531..91d1d3fd41 100644 --- a/web/apps/auth/src/pages/login.tsx +++ b/web/apps/auth/src/pages/login.tsx @@ -1,3 +1 @@ -import Page from "@/accounts/pages/login"; - -export default Page; +export { default } from "@/accounts/pages/login"; diff --git a/web/apps/auth/src/pages/passkeys/finish.tsx b/web/apps/auth/src/pages/passkeys/finish.tsx index cc9baa5214..0601be631e 100644 --- a/web/apps/auth/src/pages/passkeys/finish.tsx +++ b/web/apps/auth/src/pages/passkeys/finish.tsx @@ -1,3 +1 @@ -import Page from "@/accounts/pages/passkeys/finish"; - -export default Page; +export { default } from "@/accounts/pages/passkeys/finish"; diff --git a/web/apps/auth/src/pages/passkeys/recover.tsx b/web/apps/auth/src/pages/passkeys/recover.tsx index a156314ed5..647a9d4aa2 100644 --- a/web/apps/auth/src/pages/passkeys/recover.tsx +++ b/web/apps/auth/src/pages/passkeys/recover.tsx @@ -1,8 +1,5 @@ import Page_ from "@/accounts/pages/two-factor/recover"; -import { useAppContext } from "types/context"; -const Page = () => ( - -); +const Page = () => ; export default Page; diff --git a/web/apps/auth/src/pages/recover.tsx b/web/apps/auth/src/pages/recover.tsx index 62ceb6a63a..e1398fabb4 100644 --- a/web/apps/auth/src/pages/recover.tsx +++ b/web/apps/auth/src/pages/recover.tsx @@ -1,6 +1 @@ -import Page_ from "@/accounts/pages/recover"; -import { useAppContext } from "types/context"; - -const Page = () => ; - -export default Page; +export { default } from "@/accounts/pages/recover"; diff --git a/web/apps/auth/src/pages/signup.tsx b/web/apps/auth/src/pages/signup.tsx index 23ed3fa02b..7ec71524d5 100644 --- a/web/apps/auth/src/pages/signup.tsx +++ b/web/apps/auth/src/pages/signup.tsx @@ -1,3 +1 @@ -import Page from "@/accounts/pages/signup"; - -export default Page; +export { default } from "@/accounts/pages/signup"; diff --git a/web/apps/auth/src/pages/two-factor/recover.tsx b/web/apps/auth/src/pages/two-factor/recover.tsx index dbf10270fb..fdda173088 100644 --- a/web/apps/auth/src/pages/two-factor/recover.tsx +++ b/web/apps/auth/src/pages/two-factor/recover.tsx @@ -1,6 +1,5 @@ import Page_ from "@/accounts/pages/two-factor/recover"; -import { useAppContext } from "types/context"; -const Page = () => ; +const Page = () => ; export default Page; diff --git a/web/apps/auth/src/pages/two-factor/setup.tsx b/web/apps/auth/src/pages/two-factor/setup.tsx index bab74ddd4b..ee74c573b6 100644 --- a/web/apps/auth/src/pages/two-factor/setup.tsx +++ b/web/apps/auth/src/pages/two-factor/setup.tsx @@ -1,3 +1 @@ -import Page from "@/accounts/pages/two-factor/setup"; - -export default Page; +export { default } from "@/accounts/pages/two-factor/setup"; diff --git a/web/apps/auth/src/pages/two-factor/verify.tsx b/web/apps/auth/src/pages/two-factor/verify.tsx index 0a449ad470..5f3fbef9ce 100644 --- a/web/apps/auth/src/pages/two-factor/verify.tsx +++ b/web/apps/auth/src/pages/two-factor/verify.tsx @@ -1,6 +1 @@ -import Page_ from "@/accounts/pages/two-factor/verify"; -import { useAppContext } from "types/context"; - -const Page = () => ; - -export default Page; +export { default } from "@/accounts/pages/two-factor/verify"; diff --git a/web/apps/auth/src/pages/verify.tsx b/web/apps/auth/src/pages/verify.tsx index 572f40aa99..b5860d3a53 100644 --- a/web/apps/auth/src/pages/verify.tsx +++ b/web/apps/auth/src/pages/verify.tsx @@ -1,6 +1 @@ -import Page_ from "@/accounts/pages/verify"; -import { useAppContext } from "types/context"; - -const Page = () => ; - -export default Page; +export { default } from "@/accounts/pages/verify"; diff --git a/web/apps/auth/src/services/remote.ts b/web/apps/auth/src/services/remote.ts index a1766c0265..6de83e9a2a 100644 --- a/web/apps/auth/src/services/remote.ts +++ b/web/apps/auth/src/services/remote.ts @@ -81,7 +81,7 @@ const RemoteAuthenticatorEntityChange = z.object({ */ isDeleted: z.boolean(), /** - * Epoch milliseconds when this entity was last updated. + * Epoch microseconds when this entity was last updated. * * This value is suitable for being passed as the `sinceTime` in the diff * requests to implement pagination. diff --git a/web/apps/auth/src/types/context.ts b/web/apps/auth/src/types/context.ts deleted file mode 100644 index e4cb892adf..0000000000 --- a/web/apps/auth/src/types/context.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { AccountsContextT } from "@/accounts/types/context"; -import { createContext, useContext } from "react"; - -/** - * Properties available via {@link AppContext} to the Auth app's React tree. - */ -type AppContextT = AccountsContextT; - -/** The React {@link Context} available to all pages. */ -export const AppContext = createContext(undefined); - -/** Utility hook to reduce amount of boilerplate in account related pages. */ -export const useAppContext = () => useContext(AppContext)!; diff --git a/web/apps/auth/tsconfig.json b/web/apps/auth/tsconfig.json index 81071f5376..8e6cbbeecf 100644 --- a/web/apps/auth/tsconfig.json +++ b/web/apps/auth/tsconfig.json @@ -2,9 +2,7 @@ "extends": "@/build-config/tsconfig-next.json", "compilerOptions": { /* Set the base directory from which to resolve bare module names. */ - "baseUrl": "./src", - /* MUI doesn't work with exactOptionalPropertyTypes yet. */ - "exactOptionalPropertyTypes": false + "baseUrl": "./src" }, "include": [ "src", diff --git a/web/apps/cast/src/pages/_app.tsx b/web/apps/cast/src/pages/_app.tsx index 61c0af1341..3f3b8198e6 100644 --- a/web/apps/cast/src/pages/_app.tsx +++ b/web/apps/cast/src/pages/_app.tsx @@ -10,15 +10,14 @@ import React from "react"; const App: React.FC = ({ Component, pageProps }) => { useSetupLogs({ disableDiskLogs: true }); - return ( - <> - + // We don't provide BaseContext. Nothing in the cast app needs it yet. - - - - - + return ( + + + + + ); }; diff --git a/web/apps/cast/tsconfig.json b/web/apps/cast/tsconfig.json index 81071f5376..8e6cbbeecf 100644 --- a/web/apps/cast/tsconfig.json +++ b/web/apps/cast/tsconfig.json @@ -2,9 +2,7 @@ "extends": "@/build-config/tsconfig-next.json", "compilerOptions": { /* Set the base directory from which to resolve bare module names. */ - "baseUrl": "./src", - /* MUI doesn't work with exactOptionalPropertyTypes yet. */ - "exactOptionalPropertyTypes": false + "baseUrl": "./src" }, "include": [ "src", diff --git a/web/apps/photos/eslint.config.mjs b/web/apps/photos/eslint.config.mjs index 2a735f1efe..c735044bf5 100644 --- a/web/apps/photos/eslint.config.mjs +++ b/web/apps/photos/eslint.config.mjs @@ -3,7 +3,7 @@ import config from "@/build-config/eslintrc-next-app.mjs"; export default [ ...config, { - ignores: ["thirdparty"], + ignores: ["thirdparty", ".next-desktop"], }, { rules: { diff --git a/web/apps/photos/package.json b/web/apps/photos/package.json index f8040461b7..97423cc2f4 100644 --- a/web/apps/photos/package.json +++ b/web/apps/photos/package.json @@ -9,14 +9,8 @@ "@/media": "*", "@/new": "*", "@ente/shared": "*", - "@stripe/stripe-js": "^1.13.2", - "bip39": "^3.0.4", "chrono-node": "^2.7.8", "debounce": "^2.2.0", - "exifreader": "^4.26.1", - "fast-srp-hap": "^2.0.4", - "leaflet": "^1.9.4", - "leaflet-defaulticon-compatibility": "^0.1.2", "localforage": "^1.9.0", "memoize-one": "^6.0.0", "ml-matrix": "^6.12.0", @@ -24,7 +18,6 @@ "photoswipe": "file:./thirdparty/photoswipe", "react": "^19.0.0", "react-dom": "^19.0.0", - "react-dropzone": "14.3.5", "react-select": "^5.10.0", "react-top-loading-bar": "^3.0.2", "react-virtualized-auto-sizer": "^1.0.25", @@ -35,7 +28,6 @@ }, "devDependencies": { "@/build-config": "*", - "@types/leaflet": "^1.9.16", "@types/node": "^20", "@types/photoswipe": "^4.1.1", "@types/react": "^19.0.8", diff --git a/web/apps/photos/public/.well-known/assetlinks.json b/web/apps/photos/public/.well-known/assetlinks.json index 292dbcfdaf..ec818a305b 100644 --- a/web/apps/photos/public/.well-known/assetlinks.json +++ b/web/apps/photos/public/.well-known/assetlinks.json @@ -1,26 +1,20 @@ [ { - "relation": [ - "delegate_permission/common.get_login_creds" - ], + "relation": ["delegate_permission/common.get_login_creds"], "target": { "namespace": "photos-web", "site": "https://web.ente.io" } }, - { - "relation": [ - "delegate_permission/common.get_login_creds" - ], + { + "relation": ["delegate_permission/common.get_login_creds"], "target": { "namespace": "auth-web", "site": "https://auth.ente.io" } }, { - "relation": [ - "delegate_permission/common.get_login_creds" - ], + "relation": ["delegate_permission/common.get_login_creds"], "target": { "namespace": "android_app", "package_name": "io.ente.photos.independent", @@ -30,9 +24,7 @@ } }, { - "relation": [ - "delegate_permission/common.get_login_creds" - ], + "relation": ["delegate_permission/common.get_login_creds"], "target": { "namespace": "android_app", "package_name": "io.ente.photos", @@ -44,9 +36,9 @@ { "relation": ["delegate_permission/common.handle_all_urls"], "target": { - "namespace": "android_app", - "package_name": "io.ente.photos", - "sha256_cert_fingerprints": [ + "namespace": "android_app", + "package_name": "io.ente.photos", + "sha256_cert_fingerprints": [ "37:D4:0B:10:3B:BF:86:43:EB:AE:23:B3:BB:73:F8:65:B4:E9:3A:BF:65:45:EF:37:12:8A:4C:EA:5B:C2:7E:2E" ] } @@ -54,17 +46,15 @@ { "relation": ["delegate_permission/common.handle_all_urls"], "target": { - "namespace": "android_app", - "package_name": "io.ente.photos.independent", - "sha256_cert_fingerprints": [ + "namespace": "android_app", + "package_name": "io.ente.photos.independent", + "sha256_cert_fingerprints": [ "37:D4:0B:10:3B:BF:86:43:EB:AE:23:B3:BB:73:F8:65:B4:E9:3A:BF:65:45:EF:37:12:8A:4C:EA:5B:C2:7E:2E" ] } }, { - "relation": [ - "delegate_permission/common.get_login_creds" - ], + "relation": ["delegate_permission/common.get_login_creds"], "target": { "namespace": "android_app", "package_name": "io.ente.auth", diff --git a/web/apps/photos/src/components/AuthenticateUserModal.tsx b/web/apps/photos/src/components/AuthenticateUser.tsx similarity index 84% rename from web/apps/photos/src/components/AuthenticateUserModal.tsx rename to web/apps/photos/src/components/AuthenticateUser.tsx index 28e20a839f..578bcb9a25 100644 --- a/web/apps/photos/src/components/AuthenticateUserModal.tsx +++ b/web/apps/photos/src/components/AuthenticateUser.tsx @@ -3,28 +3,36 @@ import { TitledMiniDialog, type MiniDialogAttributes, } from "@/base/components/MiniDialog"; +import type { ModalVisibilityProps } from "@/base/components/utils/modal"; +import { useBaseContext } from "@/base/context"; import log from "@/base/log"; -import { AppContext } from "@/new/photos/types/context"; import VerifyMasterPasswordForm, { type VerifyMasterPasswordFormProps, } from "@ente/shared/components/VerifyMasterPasswordForm"; import { LS_KEYS, getData } from "@ente/shared/storage/localStorage"; import type { KeyAttributes, User } from "@ente/shared/user/types"; import { t } from "i18next"; -import { useCallback, useContext, useEffect, useState } from "react"; +import { useCallback, useEffect, useState } from "react"; -interface Iprops { - open: boolean; - onClose: () => void; +type AuthenticateUserProps = ModalVisibilityProps & { + /** + * Called when the user successfully reauthenticates themselves. + */ onAuthenticate: () => void; -} +}; -export default function AuthenticateUserModal({ +/** + * A dialog for reauthenticating the logged in user by prompting them for their + * password. + * + * This is used as precursor to performing various sensitive or locked actions. + */ +export const AuthenticateUser: React.FC = ({ open, onClose, onAuthenticate, -}: Iprops) { - const { showMiniDialog, onGenericError, logout } = useContext(AppContext); +}) => { + const { logout, showMiniDialog, onGenericError } = useBaseContext(); const [user, setUser] = useState(); const [keyAttributes, setKeyAttributes] = useState(); @@ -47,7 +55,7 @@ export default function AuthenticateUserModal({ // potentially transient issues. log.warn("Ignoring error when determining session validity", e); } - }, [showMiniDialog, logout]); + }, [logout, showMiniDialog]); useEffect(() => { const main = async () => { @@ -104,7 +112,7 @@ export default function AuthenticateUserModal({ /> ); -} +}; /** * Attributes for a dialog box that informs the user that their password was diff --git a/web/apps/photos/src/components/Collections/CollectionHeader.tsx b/web/apps/photos/src/components/Collections/CollectionHeader.tsx index 86f3c61070..0e59cabb25 100644 --- a/web/apps/photos/src/components/Collections/CollectionHeader.tsx +++ b/web/apps/photos/src/components/Collections/CollectionHeader.tsx @@ -6,6 +6,11 @@ import { OverflowMenuOption, } from "@/base/components/OverflowMenu"; import { useModalVisibility } from "@/base/components/utils/modal"; +import { useBaseContext } from "@/base/context"; +import { + isArchivedCollection, + isPinnedCollection, +} from "@/gallery/services/magic-metadata"; import type { Collection } from "@/media/collection"; import { ItemVisibility } from "@/media/file-metadata"; import { @@ -22,11 +27,7 @@ import type { CollectionSummaryType, } from "@/new/photos/services/collection/ui"; import { clearLocalTrash, emptyTrash } from "@/new/photos/services/collections"; -import { - isArchivedCollection, - isPinnedCollection, -} from "@/new/photos/services/magic-metadata"; -import { useAppContext } from "@/new/photos/types/context"; +import { usePhotosAppContext } from "@/new/photos/types/context"; import ArchiveOutlinedIcon from "@mui/icons-material/ArchiveOutlined"; import DeleteOutlinedIcon from "@mui/icons-material/DeleteOutlined"; import EditIcon from "@mui/icons-material/Edit"; @@ -128,8 +129,8 @@ const CollectionOptions: React.FC = ({ setFilesDownloadProgressAttributesCreator, isActiveCollectionDownloadInProgress, }) => { - const { showLoadingBar, hideLoadingBar, onGenericError, showMiniDialog } = - useAppContext(); + const { showMiniDialog, onGenericError } = useBaseContext(); + const { showLoadingBar, hideLoadingBar } = usePhotosAppContext(); const { syncWithRemote } = useContext(GalleryContext); const overFlowMenuIconRef = useRef(null); @@ -716,9 +717,11 @@ const CollectionSortOrderMenu: React.FC = ({ anchorEl={overFlowMenuIconRef.current} open={open} onClose={onClose} - MenuListProps={{ - disablePadding: true, - "aria-labelledby": "collection-files-sort", + slotProps={{ + list: { + disablePadding: true, + "aria-labelledby": "collection-files-sort", + }, }} anchorOrigin={{ vertical: "bottom", diff --git a/web/apps/photos/src/components/Collections/CollectionShare.tsx b/web/apps/photos/src/components/Collections/CollectionShare.tsx index 69c0518212..4ed9f24342 100644 --- a/web/apps/photos/src/components/Collections/CollectionShare.tsx +++ b/web/apps/photos/src/components/Collections/CollectionShare.tsx @@ -15,7 +15,9 @@ import { } from "@/base/components/RowButton"; import { Titlebar } from "@/base/components/Titlebar"; import { useModalVisibility } from "@/base/components/utils/modal"; +import { useBaseContext } from "@/base/context"; import { sharedCryptoWorker } from "@/base/crypto"; +import { formattedDateTime } from "@/base/i18n-date"; import log from "@/base/log"; import { appendCollectionKeyToShareURL } from "@/gallery/services/share"; import type { @@ -27,13 +29,12 @@ import { COLLECTION_ROLE, type CollectionUser } from "@/media/collection"; import { PublicLinkCreated } from "@/new/photos/components/share/PublicLinkCreated"; import { avatarTextColor } from "@/new/photos/services/avatar"; import type { CollectionSummary } from "@/new/photos/services/collection/ui"; -import { AppContext, useAppContext } from "@/new/photos/types/context"; +import { usePhotosAppContext } from "@/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 { formatDateTime } from "@ente/shared/time/format"; import AddIcon from "@mui/icons-material/Add"; import AdminPanelSettingsIcon from "@mui/icons-material/AdminPanelSettings"; import BlockIcon from "@mui/icons-material/Block"; @@ -811,7 +812,7 @@ const ManageEmailShare: React.FC = ({ onRootClose, peopleCount, }) => { - const { showLoadingBar, hideLoadingBar } = useContext(AppContext); + const { showLoadingBar, hideLoadingBar } = usePhotosAppContext(); const galleryContext = useContext(GalleryContext); const [addParticipantView, setAddParticipantView] = useState(false); @@ -1007,7 +1008,7 @@ const ManageParticipant: React.FC = ({ selectedParticipant, collectionUnshare, }) => { - const { showMiniDialog } = useAppContext(); + const { showMiniDialog } = useBaseContext(); const galleryContext = useContext(GalleryContext); const handleRootClose = () => { @@ -1538,8 +1539,8 @@ const ManageLinkExpiry: React.FC = ({ isLinkExpired(publicShareProp?.validTill) ? t("link_expired") : publicShareProp?.validTill - ? formatDateTime( - publicShareProp?.validTill / 1000, + ? formattedDateTime( + publicShareProp.validTill / 1000, ) : t("never") } @@ -1710,7 +1711,7 @@ const ManageDownloadAccess: React.FC = ({ updatePublicShareURLHelper, collection, }) => { - const { showMiniDialog } = useAppContext(); + const { showMiniDialog } = useBaseContext(); const handleFileDownloadSetting = () => { if (publicShareProp.enableDownload) { @@ -1758,7 +1759,7 @@ const ManageLinkPassword: React.FC = ({ publicShareProp, updatePublicShareURLHelper, }) => { - const { showMiniDialog } = useAppContext(); + const { showMiniDialog } = useBaseContext(); const [changePasswordView, setChangePasswordView] = useState(false); const closeConfigurePassword = () => setChangePasswordView(false); diff --git a/web/apps/photos/src/components/DeleteAccountModal.tsx b/web/apps/photos/src/components/DeleteAccountModal.tsx deleted file mode 100644 index 896acee7c8..0000000000 --- a/web/apps/photos/src/components/DeleteAccountModal.tsx +++ /dev/null @@ -1,317 +0,0 @@ -import { assertionFailed } from "@/base/assert"; -import { TitledMiniDialog } from "@/base/components/MiniDialog"; -import { FocusVisibleButton } from "@/base/components/mui/FocusVisibleButton"; -import { LoadingButton } from "@/base/components/mui/LoadingButton"; -import { sharedCryptoWorker } from "@/base/crypto"; -import { - DropdownInput, - type DropdownOption, -} from "@/new/photos/components/DropdownInput"; -import { AppContext } from "@/new/photos/types/context"; -import { initiateEmail } from "@/new/photos/utils/web"; -import { getData, LS_KEYS } from "@ente/shared/storage/localStorage"; -import { getActualKey } from "@ente/shared/user"; -import { - Checkbox, - FormControlLabel, - FormGroup, - Link, - Stack, - TextField, - Typography, -} from "@mui/material"; -import { Formik, type FormikHelpers } from "formik"; -import { t } from "i18next"; -import { GalleryContext } from "pages/gallery"; -import React, { useContext, useRef, useState } from "react"; -import { Trans } from "react-i18next"; -import { deleteAccount, getAccountDeleteChallenge } from "services/userService"; -import * as Yup from "yup"; - -interface Iprops { - onClose: () => void; - open: boolean; -} - -interface FormValues { - reason: string; - feedback: string; -} - -const DeleteAccountModal = ({ open, onClose }: Iprops) => { - const { showMiniDialog, onGenericError, logout } = useContext(AppContext); - const { authenticateUser } = useContext(GalleryContext); - - const [loading, setLoading] = useState(false); - const deleteAccountChallenge = useRef(undefined); - - const [acceptDataDeletion, setAcceptDataDeletion] = useState(false); - const reasonAndFeedbackRef = useRef< - { reason: string; feedback: string } | undefined - >(undefined); - - const initiateDelete = async ( - { reason, feedback }: FormValues, - { setFieldError }: FormikHelpers, - ) => { - try { - feedback = feedback.trim(); - if (feedback.length === 0) { - switch (reason) { - case "found_another_service": - setFieldError( - "feedback", - t("feedback_required_found_another_service"), - ); - break; - default: - setFieldError("feedback", t("feedback_required")); - } - return; - } - setLoading(true); - reasonAndFeedbackRef.current = { reason, feedback }; - const deleteChallengeResponse = await getAccountDeleteChallenge(); - deleteAccountChallenge.current = - deleteChallengeResponse.encryptedChallenge; - if (deleteChallengeResponse.allowDelete) { - authenticateUser(confirmAccountDeletion); - } else { - askToMailForDeletion(); - } - } catch (e) { - onGenericError(e); - } finally { - setLoading(false); - } - }; - - const confirmAccountDeletion = () => - showMiniDialog({ - title: t("delete_account"), - message: , - continue: { - text: t("delete"), - color: "critical", - action: solveChallengeAndDeleteAccount, - }, - }); - - const askToMailForDeletion = () => { - const emailID = "account-deletion@ente.io"; - - showMiniDialog({ - title: t("delete_account"), - message: ( - }} - values={{ emailID }} - /> - ), - continue: { - text: t("delete"), - color: "critical", - action: () => initiateEmail(emailID), - }, - }); - }; - - const solveChallengeAndDeleteAccount = async () => { - if (!deleteAccountChallenge.current || !reasonAndFeedbackRef.current) { - assertionFailed(); - return; - } - const decryptedChallenge = await decryptDeleteAccountChallenge( - deleteAccountChallenge.current, - ); - const { reason, feedback } = reasonAndFeedbackRef.current; - await deleteAccount(decryptedChallenge, reason, feedback); - logout(); - }; - - return ( - - - initialValues={{ - reason: "", - feedback: "", - }} - validationSchema={Yup.object().shape({ - reason: Yup.string().required(t("required")), - })} - validateOnChange={false} - validateOnBlur={false} - onSubmit={initiateDelete} - > - {({ - values, - errors, - handleChange, - handleSubmit, - }): React.JSX.Element => ( -
- - - - {t("delete_account_reason_label")} - - - {errors.reason && ( - - {errors.reason} - - )} - - - - - - {t("delete_account_confirm")} - - - {t("cancel")} - - - -
- )} - -
- ); -}; - -export default DeleteAccountModal; - -/** - * All of these must have a corresponding localized string nested under the - * "delete_reason" key. - */ -const deleteReasons = [ - "missing_feature", - "behaviour", - "found_another_service", - "not_listed", -] as const; - -type DeleteReason = (typeof deleteReasons)[number]; - -const deleteReasonOptions = (): DropdownOption[] => - deleteReasons.map((reason) => ({ - label: t(`delete_reason.${reason}`), - value: reason, - })); - -interface FeedbackInputProps { - value: string; - errorMessage?: string; - onChange: (value: string) => void; -} - -const FeedbackInput: React.FC = ({ - value, - onChange, - errorMessage, -}) => ( - - {t("delete_account_feedback_label")} - onChange(e.target.value)} - placeholder={t("delete_account_feedback_placeholder")} - sx={{ - border: "1px solid", - borderColor: "stroke.faint", - borderRadius: "8px", - padding: "12px", - ".MuiInputBase-formControl": { - "::before, ::after": { - borderBottom: "none !important", - }, - }, - }} - /> - - {errorMessage} - - -); - -interface ConfirmationCheckboxInputProps { - checked: boolean; - onChange: (value: boolean) => void; -} - -const ConfirmationCheckboxInput: React.FC = ({ - checked, - onChange, -}) => ( - - onChange(e.target.checked)} - /> - } - label={ - - {t("delete_account_confirm_checkbox_label")} - - } - /> - -); - -async function decryptDeleteAccountChallenge(encryptedChallenge: string) { - const cryptoWorker = await sharedCryptoWorker(); - const masterKey = await getActualKey(); - const keyAttributes = getData(LS_KEYS.KEY_ATTRIBUTES); - const secretKey = await cryptoWorker.decryptB64( - keyAttributes.encryptedSecretKey, - keyAttributes.secretKeyDecryptionNonce, - masterKey, - ); - const b64DecryptedChallenge = await cryptoWorker.boxSealOpen( - encryptedChallenge, - keyAttributes.publicKey, - secretKey, - ); - const utf8DecryptedChallenge = atob(b64DecryptedChallenge); - return utf8DecryptedChallenge; -} diff --git a/web/apps/photos/src/components/Export.tsx b/web/apps/photos/src/components/Export.tsx index a10e855fad..1fe4708d79 100644 --- a/web/apps/photos/src/components/Export.tsx +++ b/web/apps/photos/src/components/Export.tsx @@ -7,12 +7,12 @@ import { } from "@/base/components/OverflowMenu"; import { EllipsizedTypography } from "@/base/components/Typography"; import type { ButtonishProps } from "@/base/components/mui"; +import { DialogCloseIconButton } from "@/base/components/mui/DialogCloseIconButton"; import type { ModalVisibilityProps } from "@/base/components/utils/modal"; +import { useBaseContext } from "@/base/context"; import { ensureElectron } from "@/base/electron"; import log from "@/base/log"; import { EnteFile } from "@/media/file"; -import { DialogCloseIconButton } from "@/new/photos/components/mui/Dialog"; -import { useAppContext } from "@/new/photos/types/context"; import { SpaceBetweenFlex } from "@ente/shared/components/Container"; import { CustomError } from "@ente/shared/error"; import FolderIcon from "@mui/icons-material/Folder"; @@ -42,15 +42,15 @@ import ExportInProgress from "./ExportInProgress"; import ExportInit from "./ExportInit"; type ExportProps = ModalVisibilityProps & { - collectionNameMap: Map; + allCollectionsNameByID: Map; }; export const Export: React.FC = ({ open, onClose, - collectionNameMap, + allCollectionsNameByID, }) => { - const { showMiniDialog } = useAppContext(); + const { showMiniDialog } = useBaseContext(); const [exportStage, setExportStage] = useState(ExportStage.INIT); const [exportFolder, setExportFolder] = useState(""); const [continuousExport, setContinuousExport] = useState(false); @@ -202,7 +202,7 @@ export const Export: React.FC = ({ lastExportTime={lastExportTime} exportProgress={exportProgress} pendingExports={pendingExports} - collectionNameMap={collectionNameMap} + allCollectionsNameByID={allCollectionsNameByID} /> ); @@ -270,7 +270,7 @@ function ContinuousExport({ continuousExport, toggleContinuousExport }) { return ( - {t("CONTINUOUS_EXPORT")} + {t("sync_continuously")} void; @@ -300,7 +300,7 @@ const ExportDynamicContent = ({ lastExportTime: number; exportProgress: ExportProgress; pendingExports: EnteFile[]; - collectionNameMap: Map; + allCollectionsNameByID: Map; }) => { switch (exportStage) { case ExportStage.INIT: @@ -326,7 +326,7 @@ const ExportDynamicContent = ({ onHide={onHide} lastExportTime={lastExportTime} pendingExports={pendingExports} - collectionNameMap={collectionNameMap} + allCollectionsNameByID={allCollectionsNameByID} onResync={() => startExport({ resync: true })} /> ); diff --git a/web/apps/photos/src/components/ExportFinished.tsx b/web/apps/photos/src/components/ExportFinished.tsx index 3f13a8c2a5..ae43775c54 100644 --- a/web/apps/photos/src/components/ExportFinished.tsx +++ b/web/apps/photos/src/components/ExportFinished.tsx @@ -1,9 +1,9 @@ import { LinkButton } from "@/base/components/LinkButton"; import { FocusVisibleButton } from "@/base/components/mui/FocusVisibleButton"; import { formattedNumber } from "@/base/i18n"; +import { formattedDateTime } from "@/base/i18n-date"; import { EnteFile } from "@/media/file"; import { SpaceBetweenFlex } from "@ente/shared/components/Container"; -import { formatDateTime } from "@ente/shared/time/format"; import { DialogActions, DialogContent, Stack, Typography } from "@mui/material"; import { t } from "i18next"; import { useState } from "react"; @@ -11,7 +11,7 @@ import ExportPendingList from "./ExportPendingList"; interface Props { pendingExports: EnteFile[]; - collectionNameMap: Map; + allCollectionsNameByID: Map; onHide: () => void; lastExportTime: number; /** Called when the user presses the "Resync" button. */ @@ -19,6 +19,8 @@ interface Props { } export default function ExportFinished(props: Props) { + const { lastExportTime } = props; + const [pendingFileListView, setPendingFileListView] = useState(false); @@ -35,7 +37,7 @@ export default function ExportFinished(props: Props) { - {t("PENDING_ITEMS")} + {t("pending_items")} {props.pendingExports.length ? ( @@ -52,8 +54,8 @@ export default function ExportFinished(props: Props) { {t("last_export_time")} - {props.lastExportTime - ? formatDateTime(props.lastExportTime) + {lastExportTime + ? formattedDateTime(new Date(lastExportTime)) : t("never")} @@ -73,7 +75,7 @@ export default function ExportFinished(props: Props) { diff --git a/web/apps/photos/src/components/ExportInProgress.tsx b/web/apps/photos/src/components/ExportInProgress.tsx index 1972e880d5..ffb74ddb07 100644 --- a/web/apps/photos/src/components/ExportInProgress.tsx +++ b/web/apps/photos/src/components/ExportInProgress.tsx @@ -36,25 +36,25 @@ export default function ExportInProgress(props: Props) { {props.exportStage === ExportStage.STARTING ? ( - t("EXPORT_STARTING") + t("export_starting") ) : props.exportStage === ExportStage.MIGRATION ? ( - t("MIGRATING_EXPORT") + t("preparing") ) : props.exportStage === ExportStage.RENAMING_COLLECTION_FOLDERS ? ( - t("RENAMING_COLLECTION_FOLDERS") + t("renaming_album_folders") ) : props.exportStage === ExportStage.TRASHING_DELETED_FILES ? ( - t("TRASHING_DELETED_FILES") + t("trashing_deleted_files") ) : props.exportStage === ExportStage.TRASHING_DELETED_COLLECTIONS ? ( - t("TRASHING_DELETED_COLLECTIONS") + t("trashing_deleted_albums") ) : ( - {t("STOP_EXPORT")} + {t("stop")} diff --git a/web/apps/photos/src/components/ExportPendingList.tsx b/web/apps/photos/src/components/ExportPendingList.tsx index c9e87afca8..da428c8540 100644 --- a/web/apps/photos/src/components/ExportPendingList.tsx +++ b/web/apps/photos/src/components/ExportPendingList.tsx @@ -10,7 +10,7 @@ import { t } from "i18next"; interface Iprops { isOpen: boolean; onClose: () => void; - collectionNameMap: Map; + allCollectionsNameByID: Map; pendingExports: EnteFile[]; } @@ -36,7 +36,7 @@ const ExportPendingList = (props: Iprops) => { /> - {`${props.collectionNameMap.get(file.collectionID)} / ${ + {`${props.allCollectionsNameByID.get(file.collectionID)} / ${ file.metadata.title }`} @@ -45,7 +45,7 @@ const ExportPendingList = (props: Iprops) => { }; const getItemTitle = (file: EnteFile) => { - return `${props.collectionNameMap.get(file.collectionID)} / ${ + return `${props.allCollectionsNameByID.get(file.collectionID)} / ${ file.metadata.title }`; }; @@ -59,7 +59,7 @@ const ExportPendingList = (props: Iprops) => { open={props.isOpen} onClose={props.onClose} paperMaxWidth="444px" - title={t("PENDING_ITEMS")} + title={t("pending_items")} > = ({ attributesList, setAttributesList, }) => { - const { showMiniDialog } = useAppContext(); + const { showMiniDialog } = useBaseContext(); const galleryContext = useContext(GalleryContext); if (!attributesList) { diff --git a/web/apps/photos/src/components/FixCreationTime.tsx b/web/apps/photos/src/components/FixCreationTime.tsx index 878a9d1c60..cf3028e434 100644 --- a/web/apps/photos/src/components/FixCreationTime.tsx +++ b/web/apps/photos/src/components/FixCreationTime.tsx @@ -2,6 +2,7 @@ import { FocusVisibleButton } from "@/base/components/mui/FocusVisibleButton"; import type { ModalVisibilityProps } from "@/base/components/utils/modal"; import log from "@/base/log"; import { downloadManager } from "@/gallery/services/download"; +import { extractExifDates } from "@/gallery/services/exif"; import { fileLogID, type EnteFile } from "@/media/file"; import { decryptPublicMagicMetadata, @@ -10,8 +11,7 @@ import { type ParsedMetadataDate, } from "@/media/file-metadata"; import { FileType } from "@/media/file-type"; -import { PhotoDateTimePicker } from "@/new/photos/components/PhotoDateTimePicker"; -import { extractExifDates } from "@/new/photos/services/exif"; +import { FileDateTimePicker } from "@/new/photos/components/FileDateTimePicker"; import { Dialog, DialogContent, @@ -205,7 +205,7 @@ const OptionsForm: React.FC = ({ /> {values.option == "custom" && ( - setValues({ option: "custom", customDate }) } diff --git a/web/apps/photos/src/components/PhotoFrame.tsx b/web/apps/photos/src/components/PhotoFrame.tsx index f197512807..582af17667 100644 --- a/web/apps/photos/src/components/PhotoFrame.tsx +++ b/web/apps/photos/src/components/PhotoFrame.tsx @@ -1,25 +1,38 @@ +import { useModalVisibility } from "@/base/components/utils/modal"; +import { isSameDay } from "@/base/date"; +import { formattedDate } from "@/base/i18n-date"; import log from "@/base/log"; +import type { FileInfoProps } from "@/gallery/components/FileInfo"; import { downloadManager, type LivePhotoSourceURL, type LoadedLivePhotoSourceURL, type RenderableSourceURLs, } from "@/gallery/services/download"; +import type { Collection } from "@/media/collection"; import { EnteFile } from "@/media/file"; import { FileType } from "@/media/file-type"; +import { FileViewer } from "@/new/photos/components/FileViewerComponents"; import type { GalleryBarMode } from "@/new/photos/components/gallery/reducer"; -import { TRASH_SECTION } from "@/new/photos/services/collection"; +import { moveToTrash, TRASH_SECTION } from "@/new/photos/services/collection"; import { styled } from "@mui/material"; -import { PhotoViewer, type PhotoViewerProps } from "components/PhotoViewer"; +import { PhotoViewer } from "components/PhotoViewer"; +import { t } from "i18next"; import { useRouter } from "next/router"; import { GalleryContext } from "pages/gallery"; import PhotoSwipe from "photoswipe"; -import { useContext, useEffect, useState } from "react"; +import { useCallback, useContext, useEffect, useMemo, useState } from "react"; import AutoSizer from "react-virtualized-auto-sizer"; +import { + addToFavorites, + removeFromFavorites, +} from "services/collectionService"; +import uploadManager from "services/upload/uploadManager"; import { SelectedState, SetFilesDownloadProgressAttributesCreator, } from "types/gallery"; +import { downloadSingleFile } from "utils/file"; import { handleSelectCreator } from "utils/photoFrame"; import { PhotoList } from "./PhotoList"; import PreviewCard from "./pages/gallery/PreviewCard"; @@ -64,9 +77,27 @@ export type DisplayFile = EnteFile & { isSourceLoaded?: boolean; conversionFailed?: boolean; canForceConvert?: boolean; + /** + * [Note: Timeline date string] + * + * The timeline date string is a formatted date string under which a + * particular file should be grouped in the gallery listing. e.g. "Today", + * "Yesterday", "Fri, 21 Feb" etc. + * + * All files which have the same timelineDateString will be grouped under a + * single section in the gallery listing, prefixed by the timelineDateString + * itself, and a checkbox to select all files on that date. + */ + timelineDateString?: string; }; -export interface PhotoFrameProps { +export type PhotoFrameProps = Pick< + FileInfoProps, + | "fileCollectionIDs" + | "allCollectionsNameByID" + | "onSelectCollection" + | "onSelectPerson" +> & { mode?: GalleryBarMode; /** * This is an experimental prop, to see if we can merge the separate @@ -75,7 +106,6 @@ export interface PhotoFrameProps { */ modePlus?: GalleryBarMode | "search"; files: EnteFile[]; - syncWithRemote: () => Promise; setSelected: ( selected: SelectedState | ((selected: SelectedState) => SelectedState), ) => void; @@ -95,7 +125,10 @@ export interface PhotoFrameProps { * * Not set in the context of the shared albums app. */ - markUnsyncedFavoriteUpdate?: (fileID: number, isFavorite: boolean) => void; + onMarkUnsyncedFavoriteUpdate?: ( + fileID: number, + isFavorite: boolean, + ) => void; /** * Called when the component wants to mark the given files as deleted in the * the in-memory, unsynced, state maintained by the top level gallery. @@ -105,45 +138,46 @@ export interface PhotoFrameProps { * * Not set in the context of the shared albums app. */ - markTempDeleted?: (files: EnteFile[]) => void; + onMarkTempDeleted?: (files: EnteFile[]) => void; /** This will be set if mode is not "people". */ activeCollectionID: number; /** This will be set if mode is "people". */ activePersonID?: string | undefined; enableDownload?: boolean; - fileToCollectionsMap: Map; - collectionNameMap: Map; showAppDownloadBanner?: boolean; setIsPhotoSwipeOpen?: (value: boolean) => void; + isInIncomingSharedCollection?: boolean; isInHiddenSection?: boolean; setFilesDownloadProgressAttributesCreator?: SetFilesDownloadProgressAttributesCreator; selectable?: boolean; - onSelectPerson?: PhotoViewerProps["onSelectPerson"]; -} + onSyncWithRemote: () => Promise; +}; /** - * TODO: Rename me to FileListWithViewer + * TODO: Rename me to FileListWithViewer (or Gallery?) */ const PhotoFrame = ({ mode, modePlus, files, - syncWithRemote, setSelected, selected, favoriteFileIDs, - markUnsyncedFavoriteUpdate, - markTempDeleted, + onMarkUnsyncedFavoriteUpdate, + onMarkTempDeleted, activeCollectionID, activePersonID, enableDownload, - fileToCollectionsMap, - collectionNameMap, + fileCollectionIDs, + allCollectionsNameByID, showAppDownloadBanner, setIsPhotoSwipeOpen, + isInIncomingSharedCollection, isInHiddenSection, setFilesDownloadProgressAttributesCreator, selectable, + onSyncWithRemote, + onSelectCollection, onSelectPerson, }: PhotoFrameProps) => { const [open, setOpen] = useState(false); @@ -158,6 +192,9 @@ const PhotoFrame = ({ const [isShiftKeyPressed, setIsShiftKeyPressed] = useState(false); const router = useRouter(); + const { show: showFileViewer, props: fileViewerVisibilityProps } = + useModalVisibility(); + const [displayFiles, setDisplayFiles] = useState( undefined, ); @@ -168,6 +205,7 @@ const PhotoFrame = ({ w: window.innerWidth, h: window.innerHeight, title: file.pubMagicMetadata?.data.caption, + timelineDateString: fileTimelineDateString(file), })); setDisplayFiles(result); setFetching({}); @@ -230,6 +268,53 @@ const PhotoFrame = ({ } }, [selected]); + const handleTriggerSyncWithRemote = useCallback( + () => void onSyncWithRemote(), + [onSyncWithRemote], + ); + + const handleToggleFavorite = useMemo(() => { + return favoriteFileIDs && onMarkUnsyncedFavoriteUpdate + ? async (file: EnteFile) => { + const isFavorite = favoriteFileIDs!.has(file.id); + await (isFavorite ? removeFromFavorites : addToFavorites)( + file, + true, + ); + // See: [Note: File viewer update and dispatch] + onMarkUnsyncedFavoriteUpdate(file.id, !isFavorite); + } + : undefined; + }, [favoriteFileIDs, onMarkUnsyncedFavoriteUpdate]); + + const handleDownload = useCallback( + (file: EnteFile) => { + const setSingleFileDownloadProgress = + setFilesDownloadProgressAttributesCreator!(file.metadata.title); + void downloadSingleFile(file, setSingleFileDownloadProgress); + }, + [setFilesDownloadProgressAttributesCreator], + ); + + const handleDelete = useMemo(() => { + return onMarkTempDeleted + ? async (file: EnteFile) => { + await moveToTrash([file]); + // See: [Note: File viewer update and dispatch] + onMarkTempDeleted?.([file]); + } + : undefined; + }, [onMarkTempDeleted]); + + const handleSaveEditedImageCopy = useCallback( + (editedFile: File, collection: Collection, enteFile: EnteFile) => { + uploadManager.prepareForNewUpload(); + uploadManager.showUploadProgressDialog(); + uploadManager.uploadFile(editedFile, collection, enteFile); + }, + [], + ); + if (!displayFiles) { return
; } @@ -254,20 +339,29 @@ const PhotoFrame = ({ }; const handleClose = (needUpdate) => { - setOpen(false); - needUpdate && syncWithRemote(); - setIsPhotoSwipeOpen?.(false); + if (process.env.NEXT_PUBLIC_ENTE_WIP_PS5) { + throw new Error("Not implemented"); + } else { + setOpen(false); + needUpdate && onSyncWithRemote(); + setIsPhotoSwipeOpen?.(false); + } }; const onThumbnailClick = (index: number) => () => { setCurrentIndex(index); - setOpen(true); - setIsPhotoSwipeOpen?.(true); + if (process.env.NEXT_PUBLIC_ENTE_WIP_PS5) { + showFileViewer(); + } else { + setOpen(true); + setIsPhotoSwipeOpen?.(true); + } }; const handleSelect = handleSelectCreator( setSelected, mode, + galleryContext.user?.id, activeCollectionID, activePersonID, setRangeStart, @@ -294,16 +388,9 @@ const PhotoFrame = ({ (index - i) * direction > 0; i += direction ) { - handleSelect( - displayFiles[i].id, - displayFiles[i].ownerID === galleryContext.user?.id, - )(!checked); + handleSelect(displayFiles[i])(!checked); } - handleSelect( - displayFiles[index].id, - displayFiles[index].ownerID === galleryContext.user?.id, - index, - )(!checked); + handleSelect(displayFiles[index], index)(!checked); } }; @@ -318,11 +405,7 @@ const PhotoFrame = ({ updateURL={updateThumbURL(index)} onClick={onThumbnailClick(index)} selectable={selectable} - onSelect={handleSelect( - item.id, - item.ownerID === galleryContext.user?.id, - index, - )} + onSelect={handleSelect(item, index)} selected={ (!mode ? selected.collectionID === activeCollectionID @@ -502,6 +585,32 @@ const PhotoFrame = ({ return ( + {process.env.NEXT_PUBLIC_ENTE_WIP_PS5 && ( + + )} {({ height, width }) => ( @@ -628,3 +738,15 @@ const updateDisplayFileSource = ( file.src = url as string; } }; + +/** + * See: [Note: Timeline date string] + */ +const fileTimelineDateString = (item: EnteFile) => { + const date = new Date(item.metadata.creationTime / 1000); + return isSameDay(date, new Date()) + ? t("today") + : isSameDay(date, new Date(Date.now() - 24 * 60 * 60 * 1000)) + ? t("yesterday") + : formattedDate(date); +}; diff --git a/web/apps/photos/src/components/PhotoList/index.tsx b/web/apps/photos/src/components/PhotoList/index.tsx index 669abdb948..a16ac826b8 100644 --- a/web/apps/photos/src/components/PhotoList/index.tsx +++ b/web/apps/photos/src/components/PhotoList/index.tsx @@ -1,4 +1,5 @@ import { assertionFailed } from "@/base/assert"; +import { isSameDay } from "@/base/date"; import { EnteFile } from "@/media/file"; import { GAP_BTW_TILES, @@ -7,7 +8,6 @@ import { MIN_COLUMNS, } from "@/new/photos/components/PhotoList"; import { FlexWrapper } from "@ente/shared/components/Container"; -import { formatDate } from "@ente/shared/time/format"; import { Box, Checkbox, Link, Typography, styled } from "@mui/material"; import type { PhotoFrameProps } from "components/PhotoFrame"; import { t } from "i18next"; @@ -20,7 +20,7 @@ import { ListChildComponentProps, areEqual, } from "react-window"; -import { handleSelectCreator } from "utils/photoFrame"; +import { handleSelectCreatorMulti } from "utils/photoFrame"; import { PublicCollectionGalleryContext } from "utils/publicCollectionGallery"; export const DATE_CONTAINER_HEIGHT = 48; @@ -185,7 +185,9 @@ const NothingContainer = styled(ListItemContainer)` type Props = Pick & { height: number; width: number; - displayFiles: EnteFile[]; + displayFiles: (EnteFile & { + timelineDateString?: string; + })[]; showAppDownloadBanner: boolean; getThumbnail: ( file: EnteFile, @@ -271,7 +273,11 @@ export function PhotoList({ const shouldRefresh = useRef(false); const listRef = useRef(null); - const [checkedDates, setCheckedDates] = useState({}); + // Timeline date strings for which all photos have been selected. + // + // See: [Note: Timeline date string] + const [checkedTimelineDateStrings, setCheckedTimelineDateStrings] = + useState(new Set()); const fittableColumns = getFractionFittableColumns(width); let columns = Math.floor(fittableColumns); @@ -434,14 +440,7 @@ export function PhotoList({ timeStampList.push({ itemType: ITEM_TYPE.TIME, - date: isSameDay(new Date(currentDate), new Date()) - ? t("TODAY") - : isSameDay( - new Date(currentDate), - new Date(Date.now() - A_DAY), - ) - ? t("YESTERDAY") - : formatDate(currentDate), + date: item.timelineDateString, id: currentDate.toString(), }); timeStampList.push({ @@ -743,61 +742,60 @@ export function PhotoList({ // Nothing to do here if nothing is selected. if (!galleryContext.selectedFile) return; - const notSelectedFiles = displayFiles?.filter( + const notSelectedFiles = (displayFiles ?? []).filter( (item) => !galleryContext.selectedFile[item.id], ); - const unselectedDates = [ - ...new Set(notSelectedFiles?.map((item) => getDate(item))), // to get file's date which were manually unselected - ]; - const localSelectedFiles = displayFiles.filter( + const unselectedDates = new Set( + notSelectedFiles.map((item) => item.timelineDateString), + ); // to get file's date which were manually unselected + + const localSelectedFiles = (displayFiles ?? []).filter( // to get files which were manually selected - (item) => !unselectedDates.includes(getDate(item)), + (item) => !unselectedDates.has(item.timelineDateString), ); - const localSelectedDates = [ - ...new Set(localSelectedFiles?.map((item) => getDate(item))), - ]; // to get file's date which were manually selected + const localSelectedDates = new Set( + localSelectedFiles.map((item) => item.timelineDateString), + ); // to get file's date which were manually selected - unselectedDates.forEach((date) => { - setCheckedDates((prev) => ({ - ...prev, - [date]: false, - })); // To uncheck select all checkbox if any of the file on the date is unselected - }); - - localSelectedDates.map((date) => { - setCheckedDates((prev) => ({ - ...prev, - [date]: true, - })); - // To check select all checkbox if all of the files on the date is selected manually + setCheckedTimelineDateStrings((prev) => { + const checked = new Set(prev); + // Uncheck the "Select all" checkbox if any of the files on the date + // is unselected. + unselectedDates.forEach((date) => checked.delete(date)); + // Check the "Select all" checkbox if all of the files on a date are + // selected. + localSelectedDates.forEach((date) => checked.add(date)); + return checked; }); }, [galleryContext.selectedFile]); - const handleSelect = handleSelectCreator( + const handleSelect = handleSelectCreatorMulti( galleryContext.setSelectedFiles, mode, + galleryContext?.user?.id, activeCollectionID, activePersonID, ); const onChangeSelectAllCheckBox = (date: string) => { - const dates = { ...checkedDates, [date]: !checkedDates[date] }; - const isDateSelected = !checkedDates[date]; - - setCheckedDates(dates); + const next = new Set(checkedTimelineDateStrings); + let isDateSelected: boolean; + if (!next.has(date)) { + next.add(date); + isDateSelected = true; + } else { + next.delete(date); + isDateSelected = false; + } + setCheckedTimelineDateStrings(next); const filesOnADay = displayFiles?.filter( - (item) => getDate(item) === date, + (item) => item.timelineDateString === date, ); // all files on a checked/unchecked day - filesOnADay.forEach((file) => { - handleSelect( - file.id, - file.ownerID === galleryContext?.user?.id, - )(isDateSelected); - }); + handleSelect(filesOnADay)(isDateSelected); }; const renderListItem = ( @@ -817,7 +815,9 @@ export function PhotoList({ onChangeSelectAllCheckBox(item.date) } @@ -836,7 +836,9 @@ export function PhotoList({ onChangeSelectAllCheckBox(listItem.date) } @@ -918,24 +920,3 @@ export function PhotoList({ ); } - -const A_DAY = 24 * 60 * 60 * 1000; - -const getDate = (item: EnteFile) => { - const currentDate = item.metadata.creationTime / 1000; - const date = isSameDay(new Date(currentDate), new Date()) - ? t("TODAY") - : isSameDay(new Date(currentDate), new Date(Date.now() - A_DAY)) - ? t("YESTERDAY") - : formatDate(currentDate); - - return date; -}; - -const isSameDay = (first: Date, second: Date) => { - return ( - first.getFullYear() === second.getFullYear() && - first.getMonth() === second.getMonth() && - first.getDate() === second.getDate() - ); -}; diff --git a/web/apps/photos/src/components/PhotoViewer/FileInfo.tsx b/web/apps/photos/src/components/PhotoViewer/FileInfo.tsx deleted file mode 100644 index d79f55eec1..0000000000 --- a/web/apps/photos/src/components/PhotoViewer/FileInfo.tsx +++ /dev/null @@ -1,1014 +0,0 @@ -import { LinkButtonUndecorated } from "@/base/components/LinkButton"; -import { TitledMiniDialog } from "@/base/components/MiniDialog"; -import { type ButtonishProps } from "@/base/components/mui"; -import { ActivityIndicator } from "@/base/components/mui/ActivityIndicator"; -import { SidebarDrawer } from "@/base/components/mui/SidebarDrawer"; -import { Titlebar } from "@/base/components/Titlebar"; -import { EllipsizedTypography } from "@/base/components/Typography"; -import { useModalVisibility } from "@/base/components/utils/modal"; -import { haveWindow } from "@/base/env"; -import { nameAndExtension } from "@/base/file-name"; -import log from "@/base/log"; -import type { Location } from "@/base/types"; -import { EnteFile } from "@/media/file"; -import type { ParsedMetadata } from "@/media/file-metadata"; -import { - fileCreationPhotoDate, - fileLocation, - updateRemotePublicMagicMetadata, - type ParsedMetadataDate, -} from "@/media/file-metadata"; -import { FileType } from "@/media/file-type"; -import { CopyButton } from "@/new/photos/components/FileInfo"; -import { ChipButton } from "@/new/photos/components/mui/ChipButton"; -import { FilePeopleList } from "@/new/photos/components/PeopleList"; -import { PhotoDateTimePicker } from "@/new/photos/components/PhotoDateTimePicker"; -import { - confirmDisableMapsDialogAttributes, - confirmEnableMapsDialogAttributes, -} from "@/new/photos/components/utils/dialog"; -import { useSettingsSnapshot } from "@/new/photos/components/utils/use-snapshot"; -import { - aboveFileViewerContentZ, - fileInfoDrawerZ, -} from "@/new/photos/components/utils/z-index"; -import { tagNumericValue, type RawExifTags } from "@/new/photos/services/exif"; -import { - getAnnotatedFacesForFile, - isMLEnabled, - type AnnotatedFaceID, -} from "@/new/photos/services/ml"; -import { updateMapEnabled } from "@/new/photos/services/settings"; -import { AppContext } from "@/new/photos/types/context"; -import { formattedByteSize } from "@/new/photos/utils/units"; -import { FlexWrapper } from "@ente/shared/components/Container"; -import SingleInputForm, { - type SingleInputFormProps, -} from "@ente/shared/components/SingleInputForm"; -import { getPublicMagicMetadataSync } from "@ente/shared/file-metadata"; -import { formatDate, formatTime } from "@ente/shared/time/format"; -import CalendarTodayIcon from "@mui/icons-material/CalendarToday"; -import CameraOutlinedIcon from "@mui/icons-material/CameraOutlined"; -import CloseIcon from "@mui/icons-material/Close"; -import DoneIcon from "@mui/icons-material/Done"; -import EditIcon from "@mui/icons-material/Edit"; -import FaceRetouchingNaturalIcon from "@mui/icons-material/FaceRetouchingNatural"; -import FolderOutlinedIcon from "@mui/icons-material/FolderOutlined"; -import LocationOnOutlinedIcon from "@mui/icons-material/LocationOnOutlined"; -import PhotoOutlinedIcon from "@mui/icons-material/PhotoOutlined"; -import TextSnippetOutlinedIcon from "@mui/icons-material/TextSnippetOutlined"; -import VideocamOutlinedIcon from "@mui/icons-material/VideocamOutlined"; -import { - Box, - CircularProgress, - DialogProps, - IconButton, - Link, - Stack, - styled, - TextField, - Typography, -} from "@mui/material"; -import type { DisplayFile } from "components/PhotoFrame"; -import { Formik } from "formik"; -import { t } from "i18next"; -import { GalleryContext } from "pages/gallery"; -import React, { useContext, useEffect, useMemo, useRef, useState } from "react"; -import { - changeCaption, - changeFileName, - updateExistingFilePubMetadata, -} from "utils/file"; -import { PublicCollectionGalleryContext } from "utils/publicCollectionGallery"; -import * as Yup from "yup"; - -// Re-uses images from ~leaflet package. -import "leaflet-defaulticon-compatibility/dist/leaflet-defaulticon-compatibility.webpack.css"; -import "leaflet/dist/leaflet.css"; -// eslint-disable-next-line @typescript-eslint/no-require-imports -haveWindow() && require("leaflet-defaulticon-compatibility"); -const leaflet = haveWindow() - ? // eslint-disable-next-line @typescript-eslint/no-require-imports - (require("leaflet") as typeof import("leaflet")) - : null; - -export interface FileInfoExif { - tags: RawExifTags | undefined; - parsed: ParsedMetadata | undefined; -} - -export interface FileInfoProps { - showInfo: boolean; - handleCloseInfo: () => void; - closePhotoViewer: () => void; - file: EnteFile | undefined; - exif: FileInfoExif | undefined; - shouldDisableEdits?: boolean; - scheduleUpdate: () => void; - refreshPhotoswipe: () => void; - fileToCollectionsMap?: Map; - collectionNameMap?: Map; - showCollectionChips: boolean; - /** - * Called when the user selects a person in the file info panel. - */ - onSelectPerson?: ((personID: string) => void) | undefined; -} - -export const FileInfo: React.FC = ({ - shouldDisableEdits, - showInfo, - handleCloseInfo, - file, - exif, - scheduleUpdate, - refreshPhotoswipe, - fileToCollectionsMap, - collectionNameMap, - showCollectionChips, - closePhotoViewer, - onSelectPerson, -}) => { - const { mapEnabled } = useSettingsSnapshot(); - - const { showMiniDialog } = useContext(AppContext); - const galleryContext = useContext(GalleryContext); - const publicCollectionGalleryContext = useContext( - PublicCollectionGalleryContext, - ); - - const [exifInfo, setExifInfo] = useState(); - const { show: showRawExif, props: rawExifVisibilityProps } = - useModalVisibility(); - const [annotatedFaces, setAnnotatedFaces] = useState([]); - - const location = useMemo(() => { - if (file) { - const location = fileLocation(file); - if (location) return location; - } - return exif?.parsed?.location; - }, [file, exif]); - - useEffect(() => { - if (!file) return; - - let didCancel = false; - - void (async () => { - const result = await getAnnotatedFacesForFile(file); - !didCancel && setAnnotatedFaces(result); - })(); - - return () => { - didCancel = true; - }; - }, [file]); - - useEffect(() => { - setExifInfo(parseExifInfo(exif)); - }, [exif]); - - if (!file) { - return <>; - } - - const onCollectionChipClick = (collectionID) => { - galleryContext.onShowCollection(collectionID); - closePhotoViewer(); - }; - - const openEnableMapConfirmationDialog = () => - showMiniDialog( - confirmEnableMapsDialogAttributes(() => updateMapEnabled(true)), - ); - - const openDisableMapConfirmationDialog = () => - showMiniDialog( - confirmDisableMapsDialogAttributes(() => updateMapEnabled(false)), - ); - - const handleSelectFace = (annotatedFaceID: AnnotatedFaceID) => { - if (onSelectPerson) { - onSelectPerson(annotatedFaceID.personID); - closePhotoViewer(); - } - }; - - return ( - - - - - - - - - - {exifInfo?.takenOnDevice && ( - } - title={exifInfo?.takenOnDevice} - caption={ - - } - /> - )} - - {location && ( - <> - } - title={t("location")} - caption={ - !mapEnabled || - publicCollectionGalleryContext.credentials ? ( - - {t("view_on_map")} - - ) : ( - - {t("disable_map")} - - ) - } - trailingButton={ - - } - /> - {!publicCollectionGalleryContext.credentials && ( - - )} - - )} - } - title={t("details")} - caption={ - !exif ? ( - - ) : !exif.tags ? ( - t("no_exif") - ) : ( - - {t("view_exif")} - - ) - } - /> - {isMLEnabled() && annotatedFaces.length > 0 && ( - }> - - - )} - {showCollectionChips && ( - }> - - {fileToCollectionsMap - ?.get(file.id) - ?.filter((collectionID) => - collectionNameMap.has(collectionID), - ) - ?.map((collectionID) => ( - - onCollectionChipClick(collectionID) - } - > - {collectionNameMap.get(collectionID)} - - ))} - - - )} - - - - ); -}; - -/** - * Some immediate fields of interest, in the form that we want to display on the - * info panel for a file. - */ -type ExifInfo = Required & { - resolution?: string; - megaPixels?: string; - takenOnDevice?: string; - fNumber?: string; - exposureTime?: string; - iso?: string; -}; - -const parseExifInfo = ( - fileInfoExif: FileInfoExif | undefined, -): ExifInfo | undefined => { - if (!fileInfoExif || !fileInfoExif.tags || !fileInfoExif.parsed) - return undefined; - - const info: ExifInfo = { ...fileInfoExif }; - - const { width, height } = fileInfoExif.parsed; - if (width && height) { - info.resolution = `${width} x ${height}`; - const mp = Math.round((width * height) / 1000000); - if (mp) info.megaPixels = `${mp}MP`; - } - - const { tags } = fileInfoExif; - const { exif } = tags; - - if (exif) { - if (exif.Make && exif.Model) - info.takenOnDevice = `${exif.Make.description} ${exif.Model.description}`; - - if (exif.FNumber) - info.fNumber = exif.FNumber.description; /* e.g. "f/16" */ - - if (exif.ExposureTime) - info.exposureTime = exif.ExposureTime.description; /* "1/10" */ - - if (exif.ISOSpeedRatings) - info.iso = `ISO${tagNumericValue(exif.ISOSpeedRatings)}`; - } - return info; -}; - -const FileInfoSidebar = styled( - (props: Pick) => ( - - ), -)(({ theme }) => ({ - zIndex: fileInfoDrawerZ, - // [Note: Lighter backdrop for overlays on photo viewer] - // - // The default backdrop color we use for the drawer in light mode is too - // "white" when used in the image gallery because unlike the rest of the app - // the gallery retains a black background irrespective of the mode. So use a - // lighter scrim when overlaying content directly atop the image gallery. - // - // We don't need to add this special casing for nested overlays (e.g. - // dialogs initiated from the file info drawer itself) since now there is - // enough "white" on the screen to warrant the stronger (default) backdrop. - ...theme.applyStyles("light", { - ".MuiBackdrop-root": { - backgroundColor: theme.vars.palette.backdrop.faint, - }, - }), -})); - -interface InfoItemProps { - /** - * The icon associated with the info entry. - */ - icon: React.ReactNode; - /** - * The primary content / title of the info entry. - * - * Only used if {@link children} are not specified. - */ - title?: string; - /** - * The secondary information / subtext associated with the info entry. - * - * Only used if {@link children} are not specified. - */ - caption?: React.ReactNode; - /** - * A component, usually a button (e.g. an "edit button"), shown at the - * trailing edge of the info entry. - */ - trailingButton?: React.ReactNode; -} - -/** - * An entry in the file info panel listing. - */ -const InfoItem: React.FC> = ({ - icon, - title, - caption, - trailingButton, - children, -}) => ( - - {icon} - - {children ? ( - children - ) : ( - <> - - {title} - - - {caption} - - - )} - - {trailingButton} - -); - -const InfoItemIconContainer = styled("div")( - ({ theme }) => ` - width: 48px; - aspect-ratio: 1; - display: flex; - justify-content: center; - align-items: center; - color: ${theme.vars.palette.stroke.muted} -`, -); - -type EditButtonProps = ButtonishProps & { - /** - * If true, then an activity indicator is shown in place of the edit icon. - */ - loading?: boolean; -}; - -const EditButton: React.FC = ({ onClick, loading }) => ( - - {!loading ? ( - - ) : ( - - )} - -); - -interface RenderCaptionFormValues { - caption: string; -} - -function RenderCaption({ - file, - scheduleUpdate, - refreshPhotoswipe, - shouldDisableEdits, -}: { - shouldDisableEdits: boolean; - file: DisplayFile; - scheduleUpdate: () => void; - refreshPhotoswipe: () => void; -}) { - const [caption, setCaption] = useState( - file?.pubMagicMetadata?.data.caption, - ); - - const [loading, setLoading] = useState(false); - - const saveEdits = async (newCaption: string) => { - try { - if (file) { - if (caption === newCaption) { - return; - } - setCaption(newCaption); - - const updatedFile = await changeCaption(file, newCaption); - updateExistingFilePubMetadata(file, updatedFile); - file.title = file.pubMagicMetadata.data.caption; - refreshPhotoswipe(); - scheduleUpdate(); - } - } catch (e) { - log.error("failed to update caption", e); - } - }; - - const onSubmit = async (values: RenderCaptionFormValues) => { - try { - setLoading(true); - await saveEdits(values.caption); - } finally { - setLoading(false); - } - }; - if (!caption?.length && shouldDisableEdits) { - return <>; - } - return ( - - - initialValues={{ caption }} - validationSchema={Yup.object().shape({ - caption: Yup.string().max( - 5000, - t("caption_character_limit"), - ), - })} - validateOnBlur={false} - onSubmit={onSubmit} - > - {({ - values, - errors, - handleChange, - handleSubmit, - resetForm, - }) => ( -
- - {values.caption !== caption && ( - - - {loading ? ( - - ) : ( - - )} - - - resetForm({ - values: { caption: caption ?? "" }, - touched: { caption: false }, - }) - } - disabled={loading} - > - - - - )} - - )} - -
- ); -} - -interface CreationTimeProps { - file: EnteFile; - shouldDisableEdits: boolean; - scheduleUpdate: () => void; -} - -const CreationTime: React.FC = ({ - file, - shouldDisableEdits, - scheduleUpdate, -}) => { - const [loading, setLoading] = useState(false); - const [isInEditMode, setIsInEditMode] = useState(false); - - const openEditMode = () => setIsInEditMode(true); - const closeEditMode = () => setIsInEditMode(false); - - const publicMagicMetadata = getPublicMagicMetadataSync(file); - const originalDate = fileCreationPhotoDate(file, publicMagicMetadata); - - const saveEdits = async (pickedTime: ParsedMetadataDate) => { - try { - setLoading(true); - if (isInEditMode && file) { - // [Note: Don't modify offsetTime when editing date via picker] - // - // Use the updated date time (both in its canonical dateTime - // form, and also as in the epoch timestamp), but don't use the - // offset. - // - // The offset here will be the offset of the computer where this - // user is making this edit, not the offset of the place where - // the photo was taken. In a future iteration of the date time - // editor, we can provide functionality for the user to edit the - // associated offset, but right now it is not even surfaced, so - // don't also potentially overwrite it. - const { dateTime, timestamp } = pickedTime; - if (timestamp == originalDate.getTime()) { - // Same as before. - closeEditMode(); - return; - } - - await updateRemotePublicMagicMetadata(file, { - dateTime, - editedTime: timestamp, - }); - - scheduleUpdate(); - } - } catch (e) { - log.error("failed to update creationTime", e); - } finally { - closeEditMode(); - setLoading(false); - } - }; - - return ( - <> - - } - title={formatDate(originalDate)} - caption={formatTime(originalDate)} - trailingButton={ - shouldDisableEdits || ( - - ) - } - /> - {isInEditMode && ( - - )} - - - ); -}; - -interface RenderFileNameProps { - file: EnteFile; - shouldDisableEdits: boolean; - exifInfo: ExifInfo | undefined; - scheduleUpdate: () => void; -} - -const RenderFileName: React.FC = ({ - file, - shouldDisableEdits, - exifInfo, - scheduleUpdate, -}) => { - const [isInEditMode, setIsInEditMode] = useState(false); - const openEditMode = () => setIsInEditMode(true); - const closeEditMode = () => setIsInEditMode(false); - const [fileName, setFileName] = useState(); - const [extension, setExtension] = useState(); - - useEffect(() => { - const [filename, extension] = nameAndExtension(file.metadata.title); - setFileName(filename); - setExtension(extension); - }, [file]); - - const saveEdits = async (newFilename: string) => { - if (!file) return; - if (fileName === newFilename) { - closeEditMode(); - return; - } - setFileName(newFilename); - const newTitle = [newFilename, extension].join("."); - const updatedFile = await changeFileName(file, newTitle); - updateExistingFilePubMetadata(file, updatedFile); - scheduleUpdate(); - }; - - return ( - <> - - ) : ( - - ) - } - title={[fileName, extension].join(".")} - caption={getCaption(file, exifInfo)} - trailingButton={ - shouldDisableEdits || - } - /> - - - ); -}; - -const getCaption = (file: EnteFile, exifInfo: ExifInfo | undefined) => { - const megaPixels = exifInfo?.megaPixels; - const resolution = exifInfo?.resolution; - const fileSize = file.info?.fileSize; - - const captionParts = []; - if (megaPixels) { - captionParts.push(megaPixels); - } - if (resolution) { - captionParts.push(resolution); - } - if (fileSize) { - captionParts.push(formattedByteSize(fileSize)); - } - return ( - - {captionParts.map((caption) => ( - {caption} - ))} - - ); -}; - -const FileNameEditDialog = ({ - isInEditMode, - closeEditMode, - filename, - extension, - saveEdits, -}) => { - const onSubmit: SingleInputFormProps["callback"] = async ( - filename, - setFieldError, - ) => { - try { - await saveEdits(filename); - closeEditMode(); - } catch (e) { - log.error(e); - setFieldError(t("generic_error_retry")); - } - }; - return ( - - - - ); -}; - -const BasicDeviceCamera: React.FC<{ parsedExif: ExifInfo }> = ({ - parsedExif, -}) => { - return ( - - {parsedExif.fNumber} - {parsedExif.exposureTime} - {parsedExif.iso} - - ); -}; - -const openStreetMapLink = ({ latitude, longitude }: Location) => - `https://www.openstreetmap.org/?mlat=${latitude}&mlon=${longitude}#map=15/${latitude}/${longitude}`; - -interface MapBoxProps { - location: Location; - mapEnabled: boolean; - openUpdateMapConfirmationDialog: () => void; -} - -const MapBox: React.FC = ({ - location, - mapEnabled, - openUpdateMapConfirmationDialog, -}) => { - const urlTemplate = "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"; - const attribution = - '© OpenStreetMap contributors'; - const zoom = 16; - - const mapBoxContainerRef = useRef(null); - - useEffect(() => { - const mapContainer = mapBoxContainerRef.current; - if (mapEnabled) { - const position: L.LatLngTuple = [ - location.latitude, - location.longitude, - ]; - if (mapContainer && !mapContainer.hasChildNodes()) { - const map = leaflet.map(mapContainer).setView(position, zoom); - leaflet - .tileLayer(urlTemplate, { - attribution, - }) - .addTo(map); - leaflet.marker(position).addTo(map).openPopup(); - } - } else { - if (mapContainer?.hasChildNodes()) { - if (mapContainer.firstChild) { - mapContainer.removeChild(mapContainer.firstChild); - } - } - } - }, [mapEnabled]); - - return mapEnabled ? ( - - ) : ( - - - {t("enable_map")} - - - ); -}; - -const MapBoxContainer = styled("div")` - height: 200px; - width: 100%; -`; - -const MapBoxEnableContainer = styled(MapBoxContainer)( - ({ theme }) => ` - position: relative; - display: flex; - justify-content: center; - align-items: center; - background-color: ${theme.vars.palette.fill.fainter}; -`, -); - -interface RawExifProps { - open: boolean; - onClose: () => void; - onInfoClose: () => void; - tags: RawExifTags | undefined; - fileName: string; -} - -const RawExif: React.FC = ({ - open, - onClose, - onInfoClose, - tags, - fileName, -}) => { - if (!tags) { - return <>; - } - - const handleRootClose = () => { - onClose(); - onInfoClose(); - }; - - const items: (readonly [string, string, string, string])[] = Object.entries( - tags, - ) - .map(([namespace, namespaceTags]) => { - return Object.entries(namespaceTags).map(([tagName, tag]) => { - const key = `${namespace}:${tagName}`; - let description = "<...>"; - if (typeof tag == "string") { - description = tag; - } else if (typeof tag == "number") { - description = `${tag}`; - } else if ( - tag && - typeof tag == "object" && - "description" in tag && - typeof tag.description == "string" - ) { - description = tag.description; - } - return [key, namespace, tagName, description] as const; - }); - }) - .flat() - .filter(([, , , description]) => description); - - return ( - - - } - /> - - {items.map(([key, namespace, tagName, description]) => ( - - - - {tagName} - - - {namespace} - - - - {description} - - - ))} - - - ); -}; - -const ExifItem = styled("div")` - padding-left: 8px; - padding-right: 8px; - display: flex; - flex-direction: column; - gap: 4px; -`; diff --git a/web/apps/photos/src/components/PhotoViewer/index.tsx b/web/apps/photos/src/components/PhotoViewer/index.tsx index 16d04eb195..b2376b707c 100644 --- a/web/apps/photos/src/components/PhotoViewer/index.tsx +++ b/web/apps/photos/src/components/PhotoViewer/index.tsx @@ -9,14 +9,17 @@ import { } from "@/base/components/utils/modal"; import { lowercaseExtension } from "@/base/file-name"; import log from "@/base/log"; +import { FileInfo, type FileInfoExif } from "@/gallery/components/FileInfo"; import { downloadManager } from "@/gallery/services/download"; +import { extractRawExif, parseExif } from "@/gallery/services/exif"; +import type { Collection } from "@/media/collection"; import { fileLogID, type EnteFile } from "@/media/file"; import { FileType } from "@/media/file-type"; import { isHEICExtension, needsJPEGConversion } from "@/media/formats"; -import { ConfirmDeleteFileDialog } from "@/new/photos/components/FileViewer"; +import { ConfirmDeleteFileDialog } from "@/new/photos/components/FileViewerComponents"; +import { ImageEditorOverlay } from "@/new/photos/components/ImageEditorOverlay"; import { moveToTrash } from "@/new/photos/services/collection"; -import { extractRawExif, parseExif } from "@/new/photos/services/exif"; -import { AppContext } from "@/new/photos/types/context"; +import { usePhotosAppContext } from "@/new/photos/types/context"; import AlbumOutlinedIcon from "@mui/icons-material/AlbumOutlined"; import ChevronLeftIcon from "@mui/icons-material/ChevronLeft"; import ChevronRightIcon from "@mui/icons-material/ChevronRight"; @@ -61,6 +64,7 @@ import { addToFavorites, removeFromFavorites, } from "services/collectionService"; +import uploadManager from "services/upload/uploadManager"; import { SetFilesDownloadProgressAttributesCreator } from "types/gallery"; import { copyFileToClipboard, @@ -68,12 +72,16 @@ import { getFileFromURL, } from "utils/file"; import { PublicCollectionGalleryContext } from "utils/publicCollectionGallery"; -import { FileInfo, type FileInfoExif, type FileInfoProps } from "./FileInfo"; -import { ImageEditorOverlay } from "./ImageEditorOverlay"; export type PhotoViewerProps = Pick< PhotoFrameProps, - "favoriteFileIDs" | "markUnsyncedFavoriteUpdate" | "markTempDeleted" + | "favoriteFileIDs" + | "onMarkUnsyncedFavoriteUpdate" + | "onMarkTempDeleted" + | "fileCollectionIDs" + | "allCollectionsNameByID" + | "onSelectCollection" + | "onSelectPerson" > & { /** * The PhotoViewer is shown when this is `true`. @@ -99,9 +107,6 @@ export type PhotoViewerProps = Pick< isInHiddenSection: boolean; enableDownload: boolean; setFilesDownloadProgressAttributesCreator: SetFilesDownloadProgressAttributesCreator; - fileToCollectionsMap: Map; - collectionNameMap: Map; - onSelectPerson?: FileInfoProps["onSelectPerson"]; }; /** @@ -127,18 +132,19 @@ export const PhotoViewer: React.FC = ({ gettingData, forceConvertItem, favoriteFileIDs, - markUnsyncedFavoriteUpdate, - markTempDeleted, + onMarkUnsyncedFavoriteUpdate, + onMarkTempDeleted, isTrashCollection, isInHiddenSection, enableDownload, setFilesDownloadProgressAttributesCreator, - fileToCollectionsMap, - collectionNameMap, + fileCollectionIDs, + allCollectionsNameByID, + onSelectCollection, onSelectPerson, }) => { + const { showLoadingBar, hideLoadingBar } = usePhotosAppContext(); const galleryContext = useContext(GalleryContext); - const { showLoadingBar, hideLoadingBar } = useContext(AppContext); const publicCollectionGalleryContext = useContext( PublicCollectionGalleryContext, ); @@ -384,7 +390,7 @@ export const PhotoViewer: React.FC = ({ const extension = lowercaseExtension(file.metadata.title); // Assume it is supported. let isSupported = true; - if (needsJPEGConversion(extension)) { + if (extension && needsJPEGConversion(extension)) { // See if the file is on the whitelist of extensions that we know // will not be directly renderable. if (!isDesktop) { @@ -505,16 +511,16 @@ export const PhotoViewer: React.FC = ({ const isFavorite = favoriteFileIDs!.has(file.id); if (!isFavorite) { - markUnsyncedFavoriteUpdate(file.id, true); + onMarkUnsyncedFavoriteUpdate(file.id, true); void addToFavorites(file).catch((e: unknown) => { log.error("Failed to add favorite", e); - markUnsyncedFavoriteUpdate(file.id, undefined); + onMarkUnsyncedFavoriteUpdate(file.id, undefined); }); } else { - markUnsyncedFavoriteUpdate(file.id, false); + onMarkUnsyncedFavoriteUpdate(file.id, false); void removeFromFavorites(file).catch((e: unknown) => { log.error("Failed to remove favorite", e); - markUnsyncedFavoriteUpdate(file.id, undefined); + onMarkUnsyncedFavoriteUpdate(file.id, undefined); }); } @@ -532,7 +538,7 @@ export const PhotoViewer: React.FC = ({ const handleDeleteFile = async () => { const file = fileToDelete!; await moveToTrash([file]); - markTempDeleted?.([file]); + onMarkTempDeleted?.([file]); updateItems(items.filter((item) => item.id !== file.id)); setFileToDelete(undefined); needUpdate.current = true; @@ -650,6 +656,18 @@ export const PhotoViewer: React.FC = ({ setShowImageEditorOverlay(false); }; + const handleSaveEditedCopy = ( + editedFile: File, + collection: Collection, + enteFile: EnteFile, + ) => { + uploadManager.prepareForNewUpload(); + uploadManager.showUploadProgressDialog(); + uploadManager.uploadFile(editedFile, collection, enteFile); + handleCloseEditor(); + handleClose(); + }; + const downloadFileHelper = async (file: EnteFile) => { if ( file && @@ -717,6 +735,18 @@ export const PhotoViewer: React.FC = ({ } }; + const handleSelectCollection = (collectionID: number) => { + onSelectCollection(collectionID); + handleClose(); + }; + + const handleSelectPerson = onSelectPerson + ? (personID: string) => { + onSelectPerson(personID); + handleClose(); + } + : undefined; + const handleForceConvert = () => forceConvertItem( photoSwipe, @@ -962,26 +992,27 @@ export const PhotoViewer: React.FC = ({ onConfirm={handleDeleteFile} /> ); diff --git a/web/apps/photos/src/components/Sidebar.tsx b/web/apps/photos/src/components/Sidebar.tsx index 583a9b1443..647a8719d6 100644 --- a/web/apps/photos/src/components/Sidebar.tsx +++ b/web/apps/photos/src/components/Sidebar.tsx @@ -12,6 +12,7 @@ import { } from "@/base/components/RowButton"; import { SpacedRow } from "@/base/components/containers"; import { ActivityIndicator } from "@/base/components/mui/ActivityIndicator"; +import { DialogCloseIconButton } from "@/base/components/mui/DialogCloseIconButton"; import { NestedSidebarDrawer, SidebarDrawer, @@ -19,8 +20,11 @@ import { type NestedSidebarDrawerVisibilityProps, } from "@/base/components/mui/SidebarDrawer"; import { useIsSmallWidth } from "@/base/components/utils/hooks"; -import { useModalVisibility } from "@/base/components/utils/modal"; -import { isDevBuild } from "@/base/env"; +import { + useModalVisibility, + type ModalVisibilityProps, +} from "@/base/components/utils/modal"; +import { useBaseContext } from "@/base/context"; import { getLocaleInUse, setLocaleInUse, @@ -32,8 +36,8 @@ import log from "@/base/log"; import { savedLogs } from "@/base/log-web"; import { customAPIHost } from "@/base/origins"; import { downloadString } from "@/base/utils/web"; +import { DeleteAccount } from "@/new/photos/components/DeleteAccount"; import { DropdownInput } from "@/new/photos/components/DropdownInput"; -import { DialogCloseIconButton } from "@/new/photos/components/mui/Dialog"; import { MLSettings } from "@/new/photos/components/sidebar/MLSettings"; import { TwoFactorSettings } from "@/new/photos/components/sidebar/TwoFactorSettings"; import { @@ -53,7 +57,7 @@ import { import type { CollectionSummaries } from "@/new/photos/services/collection/ui"; import { isMLSupported } from "@/new/photos/services/ml"; import { - isInternalUser, + isDevBuildAndUser, syncSettings, updateCFProxyDisabledPreference, updateMapEnabled, @@ -75,7 +79,7 @@ import { userDetailsAddOnBonuses, type UserDetails, } from "@/new/photos/services/user-details"; -import { AppContext, useAppContext } from "@/new/photos/types/context"; +import { usePhotosAppContext } from "@/new/photos/types/context"; import { initiateEmail, openURL } from "@/new/photos/utils/web"; import { FlexWrapper, @@ -105,7 +109,6 @@ import { useColorScheme, } from "@mui/material"; import Typography from "@mui/material/Typography"; -import DeleteAccountModal from "components/DeleteAccountModal"; import { WatchFolder } from "components/WatchFolder"; import { t } from "i18next"; import { useRouter } from "next/router"; @@ -124,27 +127,51 @@ import exportService from "services/export"; import { testUpload } from "../../tests/upload.test"; import { SubscriptionCard } from "./SubscriptionCard"; -interface SidebarProps { +type SidebarProps = ModalVisibilityProps & { + /** + * The latest UI collections. + * + * These are used to obtain data about the uncategorized, hidden and other + * items shown in the shortcut section within the sidebar. + */ collectionSummaries: CollectionSummaries; - sidebarView: boolean; - closeSidebar: () => void; -} + /** + * Called when the plan selection modal should be shown. + */ + onShowPlanSelector: () => void; + /** + * Called when the export dialog should be shown. + */ + onShowExport: () => void; + /** + * Called when the user should be authenticated again. + * + * This will be invoked before sensitive actions, and the action will only + * proceed if the promise returned by this function is fulfilled. + */ + onAuthenticateUser: () => Promise; +}; export const Sidebar: React.FC = ({ + open, + onClose, collectionSummaries, - sidebarView, - closeSidebar, + onShowPlanSelector, + onShowExport, + onAuthenticateUser, }) => ( - - - + + + - - + @@ -158,33 +185,31 @@ const RootSidebarDrawer = styled(SidebarDrawer)(({ theme }) => ({ }, })); -interface HeaderSectionProps { - closeSidebar: () => void; +interface SectionProps { + onCloseSidebar: SidebarProps["onClose"]; } -const HeaderSection: React.FC = ({ closeSidebar }) => { - return ( - - - - - - - ); +const HeaderSection: React.FC = ({ onCloseSidebar }) => ( + + + + + + +); + +type UserDetailsSectionProps = Pick & { + sidebarOpen: boolean; }; -interface UserDetailsSectionProps { - sidebarView: boolean; -} - const UserDetailsSection: React.FC = ({ - sidebarView, + sidebarOpen, + onShowPlanSelector, }) => { - const galleryContext = useContext(GalleryContext); const userDetails = useUserDetailsSnapshot(); const [memberSubscriptionManageView, setMemberSubscriptionManageView] = useState(false); @@ -195,8 +220,8 @@ const UserDetailsSection: React.FC = ({ setMemberSubscriptionManageView(false); useEffect(() => { - if (sidebarView) void syncUserDetails(); - }, [sidebarView]); + if (sidebarOpen) void syncUserDetails(); + }, [sidebarOpen]); const isNonAdminFamilyMember = useMemo( () => @@ -217,7 +242,7 @@ const UserDetailsSection: React.FC = ({ ) { redirectToCustomerPortal(); } else { - galleryContext.showPlanSelectorModal(); + onShowPlanSelector(); } } }; @@ -238,7 +263,9 @@ const UserDetailsSection: React.FC = ({ onClick={handleSubscriptionCardClick} /> {userDetails && ( - + )} {isNonAdminFamilyMember && ( @@ -252,15 +279,14 @@ const UserDetailsSection: React.FC = ({ ); }; -interface SubscriptionStatusProps { +type SubscriptionStatusProps = Pick & { userDetails: UserDetails; -} +}; const SubscriptionStatus: React.FC = ({ userDetails, + onShowPlanSelector, }) => { - const { showPlanSelectorModal } = useContext(GalleryContext); - const hasAMessage = useMemo(() => { if (isPartOfFamily(userDetails) && !isFamilyAdmin(userDetails)) { return false; @@ -280,7 +306,7 @@ const SubscriptionStatus: React.FC = ({ if (isSubscriptionActive(userDetails.subscription)) { if (hasExceededStorageQuota(userDetails)) { - showPlanSelectorModal(); + onShowPlanSelector(); } } else { if ( @@ -289,7 +315,7 @@ const SubscriptionStatus: React.FC = ({ ) { redirectToCustomerPortal(); } else { - showPlanSelectorModal(); + onShowPlanSelector(); } } }; @@ -354,7 +380,7 @@ const SubscriptionStatus: React.FC = ({ }; function MemberSubscriptionManage({ open, userDetails, onClose }) { - const { showMiniDialog } = useAppContext(); + const { showMiniDialog } = useBaseContext(); const fullScreen = useIsSmallWidth(); const confirmLeaveFamily = () => @@ -415,13 +441,12 @@ function MemberSubscriptionManage({ open, userDetails, onClose }) { ); } -interface ShortcutSectionProps { - closeSidebar: () => void; - collectionSummaries: CollectionSummaries; -} +type ShortcutSectionProps = SectionProps & { + collectionSummaries: SidebarProps["collectionSummaries"]; +}; const ShortcutSection: React.FC = ({ - closeSidebar, + onCloseSidebar, collectionSummaries, }) => { const galleryContext = useContext(GalleryContext); @@ -429,35 +454,31 @@ const ShortcutSection: React.FC = ({ useState(); useEffect(() => { - const main = async () => { - const unCategorizedCollection = await getUncategorizedCollection(); - if (unCategorizedCollection) { - setUncategorizedCollectionID(unCategorizedCollection.id); - } else { - setUncategorizedCollectionID(DUMMY_UNCATEGORIZED_COLLECTION); - } - }; - main(); + void getUncategorizedCollection().then((uncat) => + setUncategorizedCollectionID( + uncat?.id ?? DUMMY_UNCATEGORIZED_COLLECTION, + ), + ); }, []); const openUncategorizedSection = () => { galleryContext.setActiveCollectionID(uncategorizedCollectionId); - closeSidebar(); + onCloseSidebar(); }; const openTrashSection = () => { galleryContext.setActiveCollectionID(TRASH_SECTION); - closeSidebar(); + onCloseSidebar(); }; const openArchiveSection = () => { galleryContext.setActiveCollectionID(ARCHIVE_SECTION); - closeSidebar(); + onCloseSidebar(); }; const openHiddenSection = () => { galleryContext.openHiddenSection(() => { - closeSidebar(); + onCloseSidebar(); }); }; @@ -504,11 +525,20 @@ const ShortcutSection: React.FC = ({ ); }; -const UtilitySection: React.FC> = ({ - closeSidebar, +type UtilitySectionProps = SectionProps & + Pick; + +const UtilitySection: React.FC = ({ + onCloseSidebar, + onShowExport, + onAuthenticateUser, }) => { + const { showMiniDialog } = useBaseContext(); + const { watchFolderView, setWatchFolderView } = usePhotosAppContext(); + const router = useRouter(); - const { watchFolderView, setWatchFolderView } = useAppContext(); + + const { show: showHelp, props: helpVisibilityProps } = useModalVisibility(); const { show: showAccount, props: accountVisibilityProps } = useModalVisibility(); @@ -520,6 +550,11 @@ const UtilitySection: React.FC> = ({ const handleDeduplicate = () => router.push("/duplicates"); + const handleExport = () => + isDesktop + ? onShowExport() + : showMiniDialog(downloadAppDialogAttributes()); + return ( <> > = ({ label={t("preferences")} onClick={showPreferences} /> - {isDesktop && ( - - )} - - - - ); -}; - -const HelpSection: React.FC> = ({ - closeSidebar, -}) => { - const { showMiniDialog } = useContext(AppContext); - const { openExportModal } = useContext(GalleryContext); - - const { show: showHelp, props: helpVisibilityProps } = useModalVisibility(); - - const handleExport = () => - isDesktop - ? openExportModal() - : showMiniDialog(downloadAppDialogAttributes()); - - return ( - <> > = ({ } onClick={handleExport} /> - + + {isDesktop && ( + + )} + + ); }; const ExitSection: React.FC = () => { - const { showMiniDialog, logout } = useContext(AppContext); + const { logout, showMiniDialog } = useBaseContext(); const handleLogout = () => showMiniDialog({ @@ -643,12 +663,15 @@ const InfoSection: React.FC = () => { ); }; -const Account: React.FC = ({ +type AccountProps = NestedSidebarDrawerVisibilityProps & + Pick; +const Account: React.FC = ({ open, onClose, onRootClose, + onAuthenticateUser, }) => { - const { showMiniDialog } = useAppContext(); + const { showMiniDialog } = useBaseContext(); const router = useRouter(); @@ -731,7 +754,10 @@ const Account: React.FC = ({ {...twoFactorVisibilityProps} onRootClose={onRootClose} /> - + ); }; @@ -876,6 +902,8 @@ const localeName = (locale: SupportedLocale) => { return "Українська"; case "vi-VN": return "Tiếng Việt"; + case "ja-JP": + return "日本語"; } }; @@ -908,7 +936,7 @@ const MapSettings: React.FC = ({ onClose, onRootClose, }) => { - const { showMiniDialog } = useAppContext(); + const { showMiniDialog } = useBaseContext(); const { mapEnabled } = useSettingsSnapshot(); @@ -1033,7 +1061,7 @@ const Help: React.FC = ({ onClose, onRootClose, }) => { - const { showMiniDialog } = useAppContext(); + const { showMiniDialog } = useBaseContext(); const handleRootClose = () => { onClose(); @@ -1119,7 +1147,7 @@ const Help: React.FC = ({ } onClick={confirmViewLogs} /> - {isInternalUser() && isDevBuild && ( + {isDevBuildAndUser() && ( = ({ userDetails }) => ( - - - {isPartOfFamilyWithOtherMembers(userDetails) ? ( - - ) : ( - - )} - - -); - -const IndividualSubscriptionCardContents: React.FC< - SubscriptionCardContentOverlayProps > = ({ userDetails }) => { - const totalStorage = - userDetails.subscription.storage + userDetails.storageBonus; + const inFamily = isPartOfFamilyWithOtherMembers(userDetails); + const storageLimit = inFamily + ? familyMemberStorageLimit(userDetails) + : undefined; return ( - <> - - - + + + {inFamily ? ( + storageLimit ? ( + + ) : ( + + ) + ) : ( + + )} + + ); }; +type UserSubscriptionCardContentsProps = SubscriptionCardContentOverlayProps & { + totalStorage: number; +}; + +const UserSubscriptionCardContents: React.FC< + UserSubscriptionCardContentsProps +> = ({ userDetails, totalStorage }) => ( + <> + + + +); + interface StorageSectionProps { usage: number; storage: number; diff --git a/web/apps/photos/src/components/Upload/Uploader.tsx b/web/apps/photos/src/components/Upload.tsx similarity index 69% rename from web/apps/photos/src/components/Upload/Uploader.tsx rename to web/apps/photos/src/components/Upload.tsx index 67498b49d1..14542a8313 100644 --- a/web/apps/photos/src/components/Upload/Uploader.tsx +++ b/web/apps/photos/src/components/Upload.tsx @@ -1,8 +1,23 @@ import { isDesktop } from "@/base/app"; -import { useModalVisibility } from "@/base/components/utils/modal"; +import { SpacedRow } from "@/base/components/containers"; +import { DialogCloseIconButton } from "@/base/components/mui/DialogCloseIconButton"; +import { FocusVisibleButton } from "@/base/components/mui/FocusVisibleButton"; +import { RowButton } from "@/base/components/RowButton"; +import { useIsTouchscreen } from "@/base/components/utils/hooks"; +import { + useModalVisibility, + type ModalVisibilityProps, +} from "@/base/components/utils/modal"; +import { useBaseContext } from "@/base/context"; import { basename } from "@/base/file-name"; import log from "@/base/log"; import type { CollectionMapping, Electron, ZipItem } from "@/base/types/ipc"; +import { useFileInput } from "@/gallery/components/utils/use-file-input"; +import type { + FileAndPath, + UploadItem, + UploadPhase, +} from "@/gallery/services/upload"; import type { Collection } from "@/media/collection"; import type { EnteFile } from "@/media/file"; import { UploaderNameInput } from "@/new/albums/components/UploaderNameInput"; @@ -11,20 +26,35 @@ import type { CollectionSelectorAttributes } from "@/new/photos/components/Colle import { downloadAppDialogAttributes } from "@/new/photos/components/utils/download"; import { getLatestCollections } from "@/new/photos/services/collections"; import { exportMetadataDirectoryName } from "@/new/photos/services/export"; -import type { - FileAndPath, - UploadItem, - UploadPhase, -} from "@/new/photos/services/upload/types"; import { redirectToCustomerPortal } from "@/new/photos/services/user-details"; -import { useAppContext } from "@/new/photos/types/context"; +import { usePhotosAppContext } from "@/new/photos/types/context"; import { firstNonEmpty } from "@/utils/array"; import { CustomError } from "@ente/shared/error"; +import ChevronRightIcon from "@mui/icons-material/ChevronRight"; import DiscFullIcon from "@mui/icons-material/DiscFull"; +import GoogleIcon from "@mui/icons-material/Google"; +import ImageOutlinedIcon from "@mui/icons-material/ImageOutlined"; import InfoRoundedIcon from "@mui/icons-material/InfoRounded"; +import PermMediaOutlinedIcon from "@mui/icons-material/PermMediaOutlined"; +import { + Box, + CircularProgress, + Dialog, + DialogTitle, + Link, + Stack, + Typography, + type DialogProps, +} from "@mui/material"; import { t } from "i18next"; import { GalleryContext } from "pages/gallery"; -import { useContext, useEffect, useRef, useState } from "react"; +import React, { + useCallback, + useContext, + useEffect, + useRef, + useState, +} from "react"; import { Trans } from "react-i18next"; import { getPublicCollectionUID, @@ -43,20 +73,12 @@ 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 { SetCollectionNamerAttributes } from "./Collections/CollectionNamer"; import { UploadProgress } from "./UploadProgress"; -import { - UploadTypeSelector, - type UploadTypeSelectorIntent, -} from "./UploadTypeSelector"; -enum PICKED_UPLOAD_TYPE { - FILES = "files", - FOLDERS = "folders", - ZIPS = "zips", -} +export type UploadTypeSelectorIntent = "upload" | "import" | "collect"; -interface Props { +interface UploadProps { syncWithRemote: (force?: boolean, silent?: boolean) => Promise; closeUploadTypeSelector: () => void; /** @@ -79,41 +101,38 @@ interface Props { * @param file The newly uploaded file. */ onUploadFile: (file: EnteFile) => void; + /** + * Called when the plan selection modal should be shown. + * + * It is optional because {@link Upload} is also used by the public albums + * app, where the scenario requiring this will not arise. + */ + onShowPlanSelector?: () => void; setCollections?: (cs: Collection[]) => void; isFirstUpload?: boolean; uploadTypeSelectorView: boolean; showSessionExpiredMessage: () => void; dragAndDropFiles: File[]; - openFileSelector: () => void; - fileSelectorFiles: File[]; - openFolderSelector: () => void; - folderSelectorFiles: File[]; - openZipFileSelector?: () => void; - fileSelectorZipFiles?: File[]; uploadCollection?: Collection; uploadTypeSelectorIntent: UploadTypeSelectorIntent; activeCollection?: Collection; } -export default function Uploader({ +type UploadType = "files" | "folders" | "zips"; + +/** + * Top level component that houses the infrastructure for handling uploads. + */ +export const Upload: React.FC = ({ isFirstUpload, dragAndDropFiles, - openFileSelector, - fileSelectorFiles, - openFolderSelector, - folderSelectorFiles, - openZipFileSelector, - fileSelectorZipFiles, onUploadFile, + onShowPlanSelector, showSessionExpiredMessage, ...props -}: Props) { - const { - showMiniDialog, - showNotification, - onGenericError, - watchFolderView, - } = useAppContext(); +}) => { + const { showMiniDialog, onGenericError } = useBaseContext(); + const { showNotification, watchFolderView } = usePhotosAppContext(); const galleryContext = useContext(GalleryContext); const publicCollectionGalleryContext = useContext( PublicCollectionGalleryContext, @@ -137,7 +156,7 @@ export default function Uploader({ const [openCollectionMappingChoice, setOpenCollectionMappingChoice] = useState(false); const [importSuggestion, setImportSuggestion] = useState( - DEFAULT_IMPORT_SUGGESTION, + defaultImportSuggestion, ); const { show: showUploaderNameInput, @@ -211,13 +230,71 @@ export default function Uploader({ * This is set to thue user's choice when the user chooses one of the * predefined type to upload from the upload type selector dialog */ - const pickedUploadType = useRef(null); + const selectedUploadType = useRef(undefined); const currentUploadPromise = useRef | undefined>(undefined); const uploadRunning = useRef(false); const uploaderNameRef = useRef(null); const isDragAndDrop = useRef(false); + /** + * `true` if we've activated one hidden {@link Inputs} that allow the user + * to select items, and haven't heard back from the browser as to the + * selection (or cancellation). + * + * [Note: Showing an activity indicator during upload item selection] + * + * When selecting a large number of items (100K+), the browser can take + * significant time (10s+) before it hands back control to us. The + * {@link isInputPending} state tracks this intermediate state, and we use + * it to show an activity indicator to let that the user know that their + * selection is still being processed. + */ + const [isInputPending, setIsInputPending] = useState(false); + + /** + * Files that were selected by the user in the last activation of one of the + * hidden {@link Inputs}. + */ + const [selectedInputFiles, setSelectedInputFiles] = useState([]); + + const handleInputSelect = useCallback((files: File[]) => { + setIsInputPending(false); + setSelectedInputFiles(files); + }, []); + + const handleInputCancel = useCallback(() => { + setIsInputPending(false); + }, []); + + const { + getInputProps: getFileSelectorInputProps, + openSelector: openFileSelector, + } = useFileInput({ + directory: false, + onSelect: handleInputSelect, + onCancel: handleInputCancel, + }); + + const { + getInputProps: getFolderSelectorInputProps, + openSelector: openFolderSelector, + } = useFileInput({ + directory: true, + onSelect: handleInputSelect, + onCancel: handleInputCancel, + }); + + const { + getInputProps: getZipFileSelectorInputProps, + openSelector: openZipFileSelector, + } = useFileInput({ + directory: false, + accept: ".zip", + onSelect: handleInputSelect, + onCancel: handleInputCancel, + }); + const electron = globalThis.electron; const closeUploadProgress = () => setUploadProgressView(false); @@ -302,17 +379,11 @@ export default function Uploader({ let files: File[]; isDragAndDrop.current = false; - switch (pickedUploadType.current) { - case PICKED_UPLOAD_TYPE.FILES: - files = fileSelectorFiles; - break; - - case PICKED_UPLOAD_TYPE.FOLDERS: - files = folderSelectorFiles; - break; - - case PICKED_UPLOAD_TYPE.ZIPS: - files = fileSelectorZipFiles; + switch (selectedUploadType.current) { + case "files": + case "folders": + case "zips": + files = selectedInputFiles; break; default: @@ -331,12 +402,7 @@ export default function Uploader({ } else { setWebFiles(files); } - }, [ - dragAndDropFiles, - fileSelectorFiles, - folderSelectorFiles, - fileSelectorZipFiles, - ]); + }, [selectedInputFiles, dragAndDropFiles]); // Trigger an upload when any of the dependencies change. useEffect(() => { @@ -399,7 +465,7 @@ export default function Uploader({ } const importSuggestion = getImportSuggestion( - pickedUploadType.current, + selectedUploadType.current, // eslint-disable-next-line @typescript-eslint/no-unused-vars prunedItemAndPaths.map(([_, p]) => p), ); @@ -408,8 +474,8 @@ export default function Uploader({ log.debug(() => ["Upload request", uploadItemsAndPaths.current]); log.debug(() => ["Import suggestion", importSuggestion]); - const _pickedUploadType = pickedUploadType.current; - pickedUploadType.current = null; + const _selectedUploadType = selectedUploadType.current; + selectedUploadType.current = null; props.setLoading(false); (async () => { @@ -438,7 +504,7 @@ export default function Uploader({ return; } - if (electron && _pickedUploadType === PICKED_UPLOAD_TYPE.ZIPS) { + if (electron && _selectedUploadType == "zips") { uploadFilesToNewCollections("parent"); return; } @@ -666,7 +732,7 @@ export default function Uploader({ captionFirst: true, caption: t("storage_quota_exceeded"), title: t("upgrade_now"), - onClick: galleryContext.showPlanSelectorModal, + onClick: onShowPlanSelector, startIcon: , }); break; @@ -695,25 +761,26 @@ export default function Uploader({ uploadManager.cancelRunningUpload(); }; - const handleUpload = (type: PICKED_UPLOAD_TYPE) => { - pickedUploadType.current = type; - if (type === PICKED_UPLOAD_TYPE.FILES) { - openFileSelector(); - } else if (type === PICKED_UPLOAD_TYPE.FOLDERS) { - openFolderSelector(); - } else { - if (openZipFileSelector && electron) { - openZipFileSelector(); - } else { - showMiniDialog(downloadAppDialogAttributes()); - } + const handleUploadTypeSelect = (type: UploadType) => { + selectedUploadType.current = type; + setIsInputPending(true); + switch (type) { + case "files": + openFileSelector(); + break; + case "folders": + openFolderSelector(); + break; + case "zips": + if (electron) { + openZipFileSelector(); + } else { + showMiniDialog(downloadAppDialogAttributes()); + } + break; } }; - const handleFileUpload = () => handleUpload(PICKED_UPLOAD_TYPE.FILES); - const handleFolderUpload = () => handleUpload(PICKED_UPLOAD_TYPE.FOLDERS); - const handleZipUpload = () => handleUpload(PICKED_UPLOAD_TYPE.ZIPS); - const handlePublicUpload = async ( uploaderName: string, skipSave?: boolean, @@ -777,6 +844,13 @@ export default function Uploader({ return ( <> + ); +}; + +type GetInputProps = () => React.HTMLAttributes; + +interface InputsProps { + getFileSelectorInputProps: GetInputProps; + getFolderSelectorInputProps: GetInputProps; + getZipFileSelectorInputProps: GetInputProps; } +/** + * Create a bunch of HTML inputs elements, one each for the given props. + * + * These hidden input element serve as the way for us to show various file / + * folder Selector dialogs and handle drag and drop inputs. + */ +const Inputs: React.FC = ({ + getFileSelectorInputProps, + getFolderSelectorInputProps, + getZipFileSelectorInputProps, +}) => ( + <> + + + + +); + const desktopFilesAndZipItems = async (electron: Electron, files: File[]) => { const fileAndPaths: FileAndPath[] = []; let zipItems: ZipItem[] = []; @@ -857,25 +958,29 @@ const pathLikeForWebFile = (file: File): string => file.name, ])!; -// This is used to prompt the user the make upload strategy choice +/** + * This is used to prompt the user the make upload strategy choice. + * + * This is derived from the items that the user selected. + */ interface ImportSuggestion { rootFolderName: string; hasNestedFolders: boolean; hasRootLevelFileWithFolder: boolean; } -const DEFAULT_IMPORT_SUGGESTION: ImportSuggestion = { +const defaultImportSuggestion: ImportSuggestion = { rootFolderName: "", hasNestedFolders: false, hasRootLevelFileWithFolder: false, }; function getImportSuggestion( - uploadType: PICKED_UPLOAD_TYPE, + uploadType: UploadType, paths: string[], ): ImportSuggestion { - if (isDesktop && uploadType === PICKED_UPLOAD_TYPE.FILES) { - return DEFAULT_IMPORT_SUGGESTION; + if (isDesktop && uploadType == "files") { + return defaultImportSuggestion; } const separatorCounts = new Map( @@ -905,6 +1010,7 @@ function getImportSuggestion( ); } } + return { rootFolderName: commonPathPrefix || null, hasNestedFolders: firstFileFolder !== lastFileFolder, @@ -948,7 +1054,7 @@ const groupFilesBasedOnParentFolder = ( return result; }; -export const setPendingUploads = async ( +const setPendingUploads = async ( electron: Electron, collections: Collection[], uploadItems: UploadItem[], @@ -983,3 +1089,253 @@ export const setPendingUploads = async ( await electron.setPendingUploads({ collectionName, filePaths, zipItems }); }; + +type UploadTypeSelectorProps = ModalVisibilityProps & { + /** + * The particular context / scenario in which this upload is occuring. + */ + intent: UploadTypeSelectorIntent; + /** + * If we're waiting on the user to select items using a previously activated + * file input, then this will be set to the type of that input. + */ + pendingUploadType: UploadType | undefined; + /** + * Called when the user selects one of the options. + */ + onSelect: (type: UploadType) => void; +}; + +/** + * Request the user to specify which type of file / folder / zip it is that they + * wish to upload. + * + * This selector (and the "Upload" button) is functionally redundant, the user + * can just drag and drop any of these into the app to directly initiate the + * upload. But having an explicit easy to reach button is also necessary for new + * users, or for cases where drag-and-drop might not be appropriate. + */ +const UploadTypeSelector: React.FC = ({ + open, + onClose, + intent, + pendingUploadType, + onSelect, +}) => { + const publicCollectionGalleryContext = useContext( + PublicCollectionGalleryContext, + ); + + // Directly show the file selector for the public albums app on likely + // mobile devices. + const directlyShowUploadFiles = useIsTouchscreen(); + + useEffect(() => { + if ( + open && + directlyShowUploadFiles && + publicCollectionGalleryContext.credentials + ) { + onSelect("files"); + onClose(); + } + }, [open]); + + const handleClose: DialogProps["onClose"] = (_, reason) => { + // Disable backdrop clicks and esc keypresses if a selection is pending + // processing so that the user doesn't inadvertently close the dialog. + if ( + pendingUploadType && + (reason == "backdropClick" || reason == "escapeKeyDown") + ) { + return; + } + onClose(); + }; + + return ( + ({ + maxWidth: "375px", + p: 1, + [theme.breakpoints.down(360)]: { p: 0 }, + }), + }, + }} + > + + + ); +}; + +type UploadOptionsProps = Pick< + UploadTypeSelectorProps, + "onClose" | "intent" | "pendingUploadType" | "onSelect" +>; + +const UploadOptions: React.FC = ({ + intent, + pendingUploadType, + onSelect, + onClose, +}) => { + // [Note: Dialog state remains preseved on reopening] + // + // Keep dialog content specific state here, in a separate component, so that + // this state is not tied to the lifetime of the dialog. + // + // If we don't do this, then a MUI dialog retains whatever it was doing when + // it was last closed. Sometimes that is desirable, but sometimes not, and + // in the latter cases moving the instance specific state to a child works. + + const [showTakeoutOptions, setShowTakeoutOptions] = useState(false); + + const handleTakeoutClose = () => setShowTakeoutOptions(false); + + const handleSelect = (option: UploadType) => { + switch (option) { + case "files": + onSelect("files"); + break; + case "folders": + onSelect("folders"); + break; + case "zips": + !showTakeoutOptions + ? setShowTakeoutOptions(true) + : onSelect("zips"); + break; + } + }; + + return !showTakeoutOptions ? ( + + ) : ( + + ); +}; + +const DefaultOptions: React.FC = ({ + intent, + pendingUploadType, + onClose, + onSelect, +}) => { + return ( + <> + + + {intent == "collect" + ? t("select_photos") + : intent == "import" + ? t("import") + : t("upload")} + + + + + + {intent != "import" && ( + } + endIcon={ + pendingUploadType == "files" ? ( + + ) : ( + + ) + } + label={t("file")} + onClick={() => onSelect("files")} + /> + )} + } + endIcon={ + pendingUploadType == "folders" ? ( + + ) : ( + + ) + } + label={t("folder")} + onClick={() => onSelect("folders")} + /> + {intent !== "collect" && ( + } + endIcon={} + label={t("google_takeout")} + onClick={() => onSelect("zips")} + /> + )} + + + {t("drag_and_drop_hint")} + + + + ); +}; + +const PendingIndicator = () => ( + +); + +const TakeoutOptions: React.FC< + Pick +> = ({ onSelect, onClose }) => ( + <> + + {t("google_takeout")} + + + + + onSelect("folders")} + > + {t("select_folder")} + + onSelect("zips")} + > + {t("select_zips")} + + + + {t("faq")} + + + + + {t("takeout_hint")} + + + +); diff --git a/web/apps/photos/src/components/Upload/UploadTypeSelector.tsx b/web/apps/photos/src/components/Upload/UploadTypeSelector.tsx index 065cea33b0..e69de29bb2 100644 --- a/web/apps/photos/src/components/Upload/UploadTypeSelector.tsx +++ b/web/apps/photos/src/components/Upload/UploadTypeSelector.tsx @@ -1,261 +0,0 @@ -import { SpacedRow } from "@/base/components/containers"; -import { FocusVisibleButton } from "@/base/components/mui/FocusVisibleButton"; -import { RowButton } from "@/base/components/RowButton"; -import { useIsTouchscreen } from "@/base/components/utils/hooks"; -import { DialogCloseIconButton } from "@/new/photos/components/mui/Dialog"; -import ChevronRightIcon from "@mui/icons-material/ChevronRight"; -import GoogleIcon from "@mui/icons-material/Google"; -import ImageOutlinedIcon from "@mui/icons-material/ImageOutlined"; -import PermMediaOutlinedIcon from "@mui/icons-material/PermMediaOutlined"; -import { - Box, - Dialog, - DialogTitle, - Link, - Stack, - Typography, -} from "@mui/material"; -import { t } from "i18next"; -import React, { useContext, useEffect, useState } from "react"; -import { PublicCollectionGalleryContext } from "utils/publicCollectionGallery"; - -export type UploadTypeSelectorIntent = "upload" | "import" | "collect"; - -interface UploadTypeSelectorProps { - /** If `true`, then the selector is shown. */ - open: boolean; - /** Callback to indicate that the selector should be closed. */ - onClose: () => void; - /** The particular context / scenario in which this upload is occuring. */ - intent: UploadTypeSelectorIntent; - uploadFiles: () => void; - uploadFolders: () => void; - uploadGoogleTakeoutZips: () => void; -} - -/** - * Request the user to specify which type of file / folder / zip it is that they - * wish to upload. - * - * This selector (and the "Upload" button) is functionally redundant, the user - * can just drag and drop any of these into the app to directly initiate the - * upload. But having an explicit easy to reach button is also necessary for new - * users, or for cases where drag-and-drop might not be appropriate. - */ -export const UploadTypeSelector: React.FC = ({ - open, - onClose, - intent, - uploadFiles, - uploadFolders, - uploadGoogleTakeoutZips, -}) => { - const publicCollectionGalleryContext = useContext( - PublicCollectionGalleryContext, - ); - - // Directly show the file selector for the public albums app on likely - // mobile devices. - const directlyShowUploadFiles = useIsTouchscreen(); - - useEffect(() => { - if ( - open && - directlyShowUploadFiles && - publicCollectionGalleryContext.credentials - ) { - uploadFiles(); - onClose(); - } - }, [open]); - - const handleSelect = (option: OptionType) => { - switch (option) { - case "files": - uploadFiles(); - break; - case "folders": - uploadFolders(); - break; - case "zips": - uploadGoogleTakeoutZips(); - break; - } - }; - - return ( - ({ - maxWidth: "375px", - p: 1, - [theme.breakpoints.down(360)]: { p: 0 }, - }), - }, - }} - onClose={onClose} - > - - - ); -}; - -type OptionType = "files" | "folders" | "zips"; - -interface OptionsProps { - intent: UploadTypeSelectorIntent; - /** Called when the user selects one of the provided options. */ - onSelect: (option: OptionType) => void; - /** Called when the dialog should be closed. */ - onClose: () => void; -} - -export const Options: React.FC = ({ - intent, - onSelect, - onClose, -}) => { - // [Note: Dialog state remains preseved on reopening] - // - // Keep dialog content specific state here, in a separate component, so that - // this state is not tied to the lifetime of the dialog. - // - // If we don't do this, then a MUI dialog retains whatever it was doing when - // it was last closed. Sometimes that is desirable, but sometimes not, and - // in the latter cases moving the instance specific state to a child works. - - const [showTakeoutOptions, setShowTakeoutOptions] = useState(false); - - const handleTakeoutClose = () => setShowTakeoutOptions(false); - - const handleSelect = (option: OptionType) => { - switch (option) { - case "files": - onSelect("files"); - break; - case "folders": - onSelect("folders"); - break; - case "zips": - !showTakeoutOptions - ? setShowTakeoutOptions(true) - : onSelect("zips"); - break; - } - }; - - return !showTakeoutOptions ? ( - - ) : ( - - ); -}; - -const DefaultOptions: React.FC = ({ - intent, - onClose, - onSelect, -}) => { - return ( - <> - - - {intent == "collect" - ? t("select_photos") - : intent == "import" - ? t("import") - : t("upload")} - - - - - - {intent != "import" && ( - } - endIcon={} - label={t("file")} - onClick={() => onSelect("files")} - /> - )} - } - endIcon={} - label={t("folder")} - onClick={() => onSelect("folders")} - /> - {intent !== "collect" && ( - } - endIcon={} - label={t("google_takeout")} - onClick={() => onSelect("zips")} - /> - )} - - - {t("drag_and_drop_hint")} - - - - ); -}; - -const TakeoutOptions: React.FC> = ({ - onSelect, - onClose, -}) => { - return ( - <> - - {t("google_takeout")} - - - - - onSelect("folders")} - > - {t("select_folder")} - - onSelect("zips")} - > - {t("select_zips")} - - - - {t("faq")} - - - - - - {t("takeout_hint")} - - - - ); -}; diff --git a/web/apps/photos/src/components/Upload/UploadProgress.tsx b/web/apps/photos/src/components/UploadProgress.tsx similarity index 98% rename from web/apps/photos/src/components/Upload/UploadProgress.tsx rename to web/apps/photos/src/components/UploadProgress.tsx index b96d00cb34..451d19ba54 100644 --- a/web/apps/photos/src/components/Upload/UploadProgress.tsx +++ b/web/apps/photos/src/components/UploadProgress.tsx @@ -1,9 +1,6 @@ import { FilledIconButton } from "@/base/components/mui"; -import { - UPLOAD_RESULT, - type UploadPhase, -} from "@/new/photos/services/upload/types"; -import { useAppContext } from "@/new/photos/types/context"; +import { useBaseContext } from "@/base/context"; +import { UPLOAD_RESULT, type UploadPhase } from "@/gallery/services/upload"; import { SpaceBetweenFlex } from "@ente/shared/components/Container"; import CloseIcon from "@mui/icons-material/Close"; import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; @@ -67,7 +64,8 @@ export const UploadProgress: React.FC = ({ finishedUploads, cancelUploads, }) => { - const { showMiniDialog } = useAppContext(); + const { showMiniDialog } = useBaseContext(); + const [expanded, setExpanded] = useState(false); useEffect(() => { diff --git a/web/apps/photos/src/components/UploadSelectorInputs.tsx b/web/apps/photos/src/components/UploadSelectorInputs.tsx deleted file mode 100644 index e22e2f541a..0000000000 --- a/web/apps/photos/src/components/UploadSelectorInputs.tsx +++ /dev/null @@ -1,32 +0,0 @@ -type GetInputProps = () => React.HTMLAttributes; - -interface UploadSelectorInputsProps { - getDragAndDropInputProps: GetInputProps; - getFileSelectorInputProps: GetInputProps; - getFolderSelectorInputProps: GetInputProps; - getZipFileSelectorInputProps?: GetInputProps; -} - -/** - * Create a bunch of HTML inputs elements, one each for the given props. - * - * These hidden input element serve as the way for us to show various file / - * folder Selector dialogs and handle drag and drop inputs. - */ -export const UploadSelectorInputs: React.FC = ({ - getDragAndDropInputProps, - getFileSelectorInputProps, - getFolderSelectorInputProps, - getZipFileSelectorInputProps, -}) => { - return ( - <> - - - - {getZipFileSelectorInputProps && ( - - )} - - ); -}; diff --git a/web/apps/photos/src/components/WatchFolder.tsx b/web/apps/photos/src/components/WatchFolder.tsx index 985cf61224..9f781e7af9 100644 --- a/web/apps/photos/src/components/WatchFolder.tsx +++ b/web/apps/photos/src/components/WatchFolder.tsx @@ -1,4 +1,5 @@ import { CenteredFill, SpacedRow } from "@/base/components/containers"; +import { DialogCloseIconButton } from "@/base/components/mui/DialogCloseIconButton"; import { FocusVisibleButton } from "@/base/components/mui/FocusVisibleButton"; import { OverflowMenu, @@ -9,12 +10,11 @@ import { useModalVisibility, type ModalVisibilityProps, } from "@/base/components/utils/modal"; +import { useBaseContext } from "@/base/context"; import { ensureElectron } from "@/base/electron"; import { basename, dirname } from "@/base/file-name"; import type { CollectionMapping, FolderWatch } from "@/base/types/ipc"; import { CollectionMappingChoice } from "@/new/photos/components/CollectionMappingChoice"; -import { DialogCloseIconButton } from "@/new/photos/components/mui/Dialog"; -import { useAppContext } from "@/new/photos/types/context"; import AddIcon from "@mui/icons-material/Add"; import CheckIcon from "@mui/icons-material/Check"; import DoNotDisturbOutlinedIcon from "@mui/icons-material/DoNotDisturbOutlined"; @@ -205,7 +205,7 @@ interface WatchEntryProps { } const WatchEntry: React.FC = ({ watch, removeWatch }) => { - const { showMiniDialog } = useAppContext(); + const { showMiniDialog } = useBaseContext(); const confirmStopWatching = () => { showMiniDialog({ diff --git a/web/apps/photos/src/components/pages/gallery/SelectedFileOptions.tsx b/web/apps/photos/src/components/pages/gallery/SelectedFileOptions.tsx index 35f97b7387..e9bc01a1fc 100644 --- a/web/apps/photos/src/components/pages/gallery/SelectedFileOptions.tsx +++ b/web/apps/photos/src/components/pages/gallery/SelectedFileOptions.tsx @@ -1,4 +1,5 @@ import { SpacedRow } from "@/base/components/containers"; +import { useBaseContext } from "@/base/context"; import type { Collection } from "@/media/collection"; import type { CollectionSelectorAttributes } from "@/new/photos/components/CollectionSelector"; import type { GalleryBarMode } from "@/new/photos/components/gallery/reducer"; @@ -7,7 +8,6 @@ import { ARCHIVE_SECTION, TRASH_SECTION, } from "@/new/photos/services/collection"; -import { useAppContext } from "@/new/photos/types/context"; import ClockIcon from "@mui/icons-material/AccessTime"; import AddIcon from "@mui/icons-material/Add"; import ArchiveIcon from "@mui/icons-material/ArchiveOutlined"; @@ -48,6 +48,27 @@ interface Props { activeCollectionID: number; isFavoriteCollection: boolean; isUncategorizedCollection: boolean; + /** + * TODO: Need to implement delete-equivalent from shared albums. + * + * Notes: + * + * - Delete action should not be enabled 3 selected (0 Yours). There should + * be separate remove action. + * + * - On remove, if the file and collection both belong to current user, we + * just use move api to existing or uncat collection. + * + * - Otherwise, we call /collections/v3/remove-files (when collection and + * file belong to different users). + * + * - Album owner can remove files of all other users from their collection. + * Particiapant (viewer/collaborator) can only remove files that belong to + * them. + * + * Also note that that user cannot delete files that are not owned by the + * user, even if they are in an album owned by the user. + */ isIncomingSharedCollection: boolean; isInSearchMode: boolean; selectedCollection: Collection; @@ -71,7 +92,7 @@ const SelectedFileOptions = ({ isInSearchMode, isInHiddenSection, }: Props) => { - const { showMiniDialog } = useAppContext(); + const { showMiniDialog } = useBaseContext(); const peopleMode = barMode == "people"; diff --git a/web/apps/photos/src/pages/404.tsx b/web/apps/photos/src/pages/404.tsx index 55b8b0b860..201a7b79ff 100644 --- a/web/apps/photos/src/pages/404.tsx +++ b/web/apps/photos/src/pages/404.tsx @@ -1,3 +1 @@ -import Page from "@/base/components/pages/404"; - -export default Page; +export { default } from "@/base/components/pages/404"; diff --git a/web/apps/photos/src/pages/_app.tsx b/web/apps/photos/src/pages/_app.tsx index f915d0e9df..000cf2d037 100644 --- a/web/apps/photos/src/pages/_app.tsx +++ b/web/apps/photos/src/pages/_app.tsx @@ -6,16 +6,14 @@ import { TranslucentLoadingOverlay, } from "@/base/components/loaders"; import { AttributedMiniDialog } from "@/base/components/MiniDialog"; -import { - genericErrorDialogAttributes, - useAttributedMiniDialog, -} from "@/base/components/utils/dialog"; +import { useAttributedMiniDialog } from "@/base/components/utils/dialog"; import { useIsRouteChangeInProgress, useSetupI18n, useSetupLogs, } from "@/base/components/utils/hooks-app"; import { photosTheme } from "@/base/components/utils/theme"; +import { BaseContext, deriveBaseContext } from "@/base/context"; import log from "@/base/log"; import { logStartupBanner } from "@/base/log-web"; import { AppUpdate } from "@/base/types/ipc"; @@ -30,12 +28,12 @@ import { aboveFileViewerContentZ } from "@/new/photos/components/utils/z-index"; import { runMigrations } from "@/new/photos/services/migration"; import { initML, isMLSupported } from "@/new/photos/services/ml"; import { getFamilyPortalRedirectURL } from "@/new/photos/services/user-details"; -import { AppContext } from "@/new/photos/types/context"; +import { PhotosAppContext } from "@/new/photos/types/context"; import HTTPService from "@ente/shared/network/HTTPService"; import { getData, + isLocalStorageAndIndexedDBMismatch, LS_KEYS, - migrateKVToken, } from "@ente/shared/storage/localStorage"; import type { User } from "@ente/shared/user/types"; import "@fontsource-variable/inter"; @@ -46,11 +44,14 @@ import { useNotification } from "components/utils/hooks-app"; import { t } from "i18next"; import type { AppProps } from "next/app"; import { useRouter } from "next/router"; -import "photoswipe/dist/photoswipe.css"; import { useCallback, useEffect, useMemo, useState } from "react"; import { resumeExportsIfNeeded } from "services/export"; import { photosLogout } from "services/logout"; +import "photoswipe/dist/photoswipe.css"; +// TODO(PS): Note, auto hide only works with the new CSS. +// import "../../../../packages/gallery/components/viewer/ps5/dist/photoswipe.css"; + import "styles/global.css"; const App: React.FC = ({ Component, pageProps }) => { @@ -65,13 +66,21 @@ const App: React.FC = ({ Component, pageProps }) => { const [watchFolderView, setWatchFolderView] = useState(false); + const logout = useCallback(() => void photosLogout(), []); + useEffect(() => { const user = getData(LS_KEYS.USER) as User | undefined | null; - void migrateKVToken(user); logStartupBanner(user?.id); HTTPService.setHeaders({ "X-Client-Package": clientPackageName }); - void runMigrations(); - }, []); + void isLocalStorageAndIndexedDBMismatch().then((mismatch) => { + if (mismatch) { + log.error("Logging out (IndexedDB and local storage mismatch)"); + return logout(); + } else { + return runMigrations(); + } + }); + }, [logout]); useEffect(() => { const electron = globalThis.electron; @@ -134,61 +143,45 @@ const App: React.FC = ({ Component, pageProps }) => { }); }, []); - const onGenericError = useCallback((e: unknown) => { - log.error(e); - // The generic error handler is sometimes called in the context of - // actions that were initiated by a confirmation dialog action handler - // themselves, then we need to let the current one close. - // - // See: [Note: Chained MiniDialogs] - setTimeout(() => { - showMiniDialog(genericErrorDialogAttributes()); - }, 0); - }, []); - - const logout = useCallback(() => void photosLogout(), []); - + const baseContext = useMemo( + () => deriveBaseContext({ logout, showMiniDialog }), + [logout, showMiniDialog], + ); const appContext = useMemo( () => ({ showLoadingBar, hideLoadingBar, watchFolderView, setWatchFolderView, - showMiniDialog, showNotification, - onGenericError, - logout, }), [ showLoadingBar, hideLoadingBar, watchFolderView, - showMiniDialog, + setWatchFolderView, showNotification, - onGenericError, - logout, ], ); const title = isI18nReady ? t("title_photos") : staticAppTitle; return ( - <> + + + - - - + - + - - - {isDesktop && {title}} - + {isDesktop && {title}} + + {!isI18nReady ? ( ) : ( @@ -197,9 +190,9 @@ const App: React.FC = ({ Component, pageProps }) => { )} - - - + + + ); }; diff --git a/web/apps/photos/src/pages/change-email.tsx b/web/apps/photos/src/pages/change-email.tsx index d36a2a3555..4db0816a91 100644 --- a/web/apps/photos/src/pages/change-email.tsx +++ b/web/apps/photos/src/pages/change-email.tsx @@ -1,3 +1 @@ -import Page from "@/accounts/pages/change-email"; - -export default Page; +export { default } from "@/accounts/pages/change-email"; diff --git a/web/apps/photos/src/pages/change-password.tsx b/web/apps/photos/src/pages/change-password.tsx index 3402664eb7..bea6b02df4 100644 --- a/web/apps/photos/src/pages/change-password.tsx +++ b/web/apps/photos/src/pages/change-password.tsx @@ -1,3 +1 @@ -import Page from "@/accounts/pages/change-password"; - -export default Page; +export { default } from "@/accounts/pages/change-password"; diff --git a/web/apps/photos/src/pages/credentials.tsx b/web/apps/photos/src/pages/credentials.tsx index a737676b33..9bb9307c79 100644 --- a/web/apps/photos/src/pages/credentials.tsx +++ b/web/apps/photos/src/pages/credentials.tsx @@ -1,6 +1 @@ -import Page_ from "@/accounts/pages/credentials"; -import { useAppContext } from "@/new/photos/types/context"; - -const Page = () => ; - -export default Page; +export { default } from "@/accounts/pages/credentials"; diff --git a/web/apps/photos/src/pages/gallery.tsx b/web/apps/photos/src/pages/gallery.tsx index 4aaf841a5f..d4926fd54d 100644 --- a/web/apps/photos/src/pages/gallery.tsx +++ b/web/apps/photos/src/pages/gallery.tsx @@ -9,15 +9,16 @@ import { FocusVisibleButton } from "@/base/components/mui/FocusVisibleButton"; import { errorDialogAttributes } from "@/base/components/utils/dialog"; import { useIsSmallWidth } from "@/base/components/utils/hooks"; import { useModalVisibility } from "@/base/components/utils/modal"; +import { useBaseContext } from "@/base/context"; import log from "@/base/log"; import { FullScreenDropZone } from "@/gallery/components/FullScreenDropZone"; -import { useFileInput } from "@/gallery/components/utils/use-file-input"; import { type Collection } from "@/media/collection"; import { mergeMetadata, type EnteFile } from "@/media/file"; import { CollectionSelector, type CollectionSelectorAttributes, } from "@/new/photos/components/CollectionSelector"; +import { resetFileViewerDataSourceOnClose } from "@/new/photos/components/FileViewerComponents-temp"; import { PlanSelector } from "@/new/photos/components/PlanSelector"; import { SearchBar, @@ -65,7 +66,7 @@ import { userDetailsSnapshot, verifyStripeSubscription, } from "@/new/photos/services/user-details"; -import { useAppContext } from "@/new/photos/types/context"; +import { usePhotosAppContext } from "@/new/photos/types/context"; import { splitByPredicate } from "@/utils/array"; import { FlexWrapper } from "@ente/shared/components/Container"; import { PHOTOS_PAGES as PAGES } from "@ente/shared/constants/pages"; @@ -88,7 +89,7 @@ import ArrowBackIcon from "@mui/icons-material/ArrowBack"; import FileUploadOutlinedIcon from "@mui/icons-material/FileUploadOutlined"; import MenuIcon from "@mui/icons-material/Menu"; import { IconButton, Stack, Typography } from "@mui/material"; -import AuthenticateUserModal from "components/AuthenticateUserModal"; +import { AuthenticateUser } from "components/AuthenticateUser"; import CollectionNamer, { CollectionNamerAttributes, } from "components/Collections/CollectionNamer"; @@ -103,21 +104,12 @@ import GalleryEmptyState from "components/GalleryEmptyState"; import PhotoFrame from "components/PhotoFrame"; import { ITEM_TYPE, TimeStampListItem } from "components/PhotoList"; import { Sidebar } from "components/Sidebar"; -import { type UploadTypeSelectorIntent } from "components/Upload/UploadTypeSelector"; -import Uploader from "components/Upload/Uploader"; -import { UploadSelectorInputs } from "components/UploadSelectorInputs"; +import { Upload, type UploadTypeSelectorIntent } from "components/Upload"; import SelectedFileOptions from "components/pages/gallery/SelectedFileOptions"; import { t } from "i18next"; import { useRouter, type NextRouter } from "next/router"; -import { - createContext, - useCallback, - useEffect, - useMemo, - useRef, - useState, -} from "react"; -import { useDropzone } from "react-dropzone"; +import { createContext, useCallback, useEffect, useRef, useState } from "react"; +import { FileWithPath } from "react-dropzone"; import { Trans } from "react-i18next"; import { constructEmailList, @@ -142,14 +134,10 @@ import { import { FILE_OPS_TYPE, getSelectedFiles, handleFileOps } from "utils/file"; const defaultGalleryContext: GalleryContextType = { - showPlanSelectorModal: () => null, setActiveCollectionID: () => null, - onShowCollection: () => null, syncWithRemote: () => null, setBlockingLoad: () => null, photoListHeader: null, - openExportModal: () => null, - authenticateUser: () => null, user: null, userIDToEmailMap: null, emailList: null, @@ -177,14 +165,9 @@ export const GalleryContext = createContext( * Photo List v */ const Page: React.FC = () => { - const { - showLoadingBar, - hideLoadingBar, - showMiniDialog, - onGenericError, - watchFolderView, - logout, - } = useAppContext(); + const { logout, showMiniDialog, onGenericError } = useBaseContext(); + const { showLoadingBar, hideLoadingBar, watchFolderView } = + usePhotosAppContext(); const isOffline = useIsOffline(); const [state, dispatch] = useGalleryReducer(); @@ -201,43 +184,11 @@ const Page: React.FC = () => { useState(null); const [collectionNamerView, setCollectionNamerView] = useState(false); const [shouldDisableDropzone, setShouldDisableDropzone] = useState(false); + const [dragAndDropFiles, setDragAndDropFiles] = useState( + [], + ); const [isPhotoSwipeOpen, setIsPhotoSwipeOpen] = useState(false); - const { - // A function to call to get the props we should apply to the container, - getRootProps: getDragAndDropRootProps, - // ... the props we should apply to the element, - getInputProps: getDragAndDropInputProps, - // ... and the files that we got. - acceptedFiles: dragAndDropFilesReadOnly, - } = useDropzone({ - noClick: true, - noKeyboard: true, - disabled: shouldDisableDropzone, - }); - const { - getInputProps: getFileSelectorInputProps, - openSelector: openFileSelector, - selectedFiles: fileSelectorFiles, - } = useFileInput({ - directory: false, - }); - const { - getInputProps: getFolderSelectorInputProps, - openSelector: openFolderSelector, - selectedFiles: folderSelectorFiles, - } = useFileInput({ - directory: true, - }); - const { - getInputProps: getZipFileSelectorInputProps, - openSelector: openZipFileSelector, - selectedFiles: fileSelectorZipFiles, - } = useFileInput({ - directory: false, - accept: ".zip", - }); - const syncInProgress = useRef(false); const syncInterval = useRef | undefined>( undefined, @@ -254,23 +205,6 @@ const Page: React.FC = () => { const [uploadTypeSelectorIntent, setUploadTypeSelectorIntent] = useState("upload"); - const [sidebarView, setSidebarView] = useState(false); - - const closeSidebar = () => setSidebarView(false); - const openSidebar = () => setSidebarView(true); - - const [authenticateUserModalView, setAuthenticateUserModalView] = - useState(false); - - const onAuthenticateCallback = useRef<(() => void) | undefined>(undefined); - - const authenticateUser = (callback: () => void) => { - onAuthenticateCallback.current = callback; - setAuthenticateUserModalView(true); - }; - const closeAuthenticateUserModal = () => - setAuthenticateUserModalView(false); - // If the fix creation time dialog is being shown, then the list of files on // which it should act. const [fixCreationTimeFiles, setFixCreationTimeFiles] = useState< @@ -295,6 +229,8 @@ const Page: React.FC = () => { const [collectionSelectorAttributes, setCollectionSelectorAttributes] = useState(); + const { show: showSidebar, props: sidebarVisibilityProps } = + useModalVisibility(); const { show: showPlanSelector, props: planSelectorVisibilityProps } = useModalVisibility(); const { show: showWhatsNew, props: whatsNewVisibilityProps } = @@ -303,6 +239,21 @@ const Page: React.FC = () => { useModalVisibility(); const { show: showExport, props: exportVisibilityProps } = useModalVisibility(); + const { + show: showAuthenticateUser, + props: authenticateUserVisibilityProps, + } = useModalVisibility(); + + const onAuthenticateCallback = useRef<(() => void) | undefined>(undefined); + + const authenticateUser = useCallback( + () => + new Promise((resolve) => { + onAuthenticateCallback.current = resolve; + showAuthenticateUser(); + }), + [], + ); // TODO: Temp const user = state.user; @@ -484,30 +435,37 @@ const Page: React.FC = () => { }, [state.isRecomputingSearchResults, state.pendingSearchSuggestions]); const selectAll = (e: KeyboardEvent) => { - // ignore ctrl/cmd + a if the user is typing in a text field + // Ignore CTRL/CMD + a if the user is typing in a text field. if ( e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement ) { return; } - // if any of the modals are open, don't select all + // Ignore select all if: if ( - sidebarView || + // - We haven't fetched the user yet; + !user || + // - There is nothing to select; + !filteredFiles?.length || + // - Any of the modals are open. uploadTypeSelectorView || openCollectionSelector || collectionNamerView || + sidebarVisibilityProps.open || planSelectorVisibilityProps.open || fixCreationTimeVisibilityProps.open || exportVisibilityProps.open || - authenticateUserModalView || - isPhotoSwipeOpen || - !filteredFiles?.length || - !user + authenticateUserVisibilityProps.open || + isPhotoSwipeOpen ) { return; } + + // Prevent the browser's default select all handling. e.preventDefault(); + + // Create a selection with everything based on the current context. const selected = { ownCount: 0, count: 0, @@ -558,94 +516,99 @@ const Page: React.FC = () => { }; }, [selectAll, clearSelection]); - // Create a regular array from the readonly array returned by dropzone. - const dragAndDropFiles = useMemo( - () => [...dragAndDropFilesReadOnly], - [dragAndDropFilesReadOnly], + const showSessionExpiredDialog = useCallback( + () => showMiniDialog(sessionExpiredDialogAttributes(logout)), + [showMiniDialog, logout], ); - const showSessionExpiredDialog = () => - showMiniDialog(sessionExpiredDialogAttributes(logout)); - - const syncWithRemote = async (force = false, silent = false) => { - if (!navigator.onLine) return; - if (syncInProgress.current && !force) { - resync.current = { force, silent }; - return; - } - const isForced = syncInProgress.current && force; - syncInProgress.current = true; - try { - const token = getToken(); - if (!token) { + const handleSyncWithRemote = useCallback( + async (force = false, silent = false) => { + if (!navigator.onLine) return; + if (syncInProgress.current && !force) { + resync.current = { force, silent }; return; } - const tokenValid = await isTokenValid(token); - if (!tokenValid) { - throw new Error(CustomError.SESSION_EXPIRED); + const isForced = syncInProgress.current && force; + syncInProgress.current = true; + try { + const token = getToken(); + if (!token) { + return; + } + const tokenValid = await isTokenValid(token); + if (!tokenValid) { + throw new Error(CustomError.SESSION_EXPIRED); + } + !silent && showLoadingBar(); + await preCollectionsAndFilesSync(); + const allCollections = await getAllLatestCollections(); + const [hiddenCollections, collections] = splitByPredicate( + allCollections, + isHiddenCollection, + ); + dispatch({ + type: "setAllCollections", + collections, + hiddenCollections, + }); + const didUpdateNormalFiles = await syncFiles( + "normal", + collections, + (files) => dispatch({ type: "setFiles", files }), + (files) => dispatch({ type: "fetchFiles", files }), + ); + const didUpdateHiddenFiles = await syncFiles( + "hidden", + hiddenCollections, + (hiddenFiles) => + dispatch({ type: "setHiddenFiles", hiddenFiles }), + (hiddenFiles) => + dispatch({ type: "fetchHiddenFiles", hiddenFiles }), + ); + await syncTrash(allCollections, (trashedFiles: EnteFile[]) => + dispatch({ type: "setTrashedFiles", trashedFiles }), + ); + if (didUpdateNormalFiles || didUpdateHiddenFiles) { + exportService.onLocalFilesUpdated(); + // TODO(PS): Use direct one + resetFileViewerDataSourceOnClose(); + } + // 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. + // + // Do the non-file-related sync only for one of these parallel ones. + if (!isForced) { + await sync(); + } + } catch (e) { + switch (e.message) { + case CustomError.SESSION_EXPIRED: + showSessionExpiredDialog(); + break; + case CustomError.KEY_MISSING: + clearKeys(); + router.push(PAGES.CREDENTIALS); + break; + default: + log.error("syncWithRemote failed", e); + } + } finally { + dispatch({ type: "clearUnsyncedState" }); + !silent && hideLoadingBar(); } - !silent && showLoadingBar(); - await preCollectionsAndFilesSync(); - const allCollections = await getAllLatestCollections(); - const [hiddenCollections, collections] = splitByPredicate( - allCollections, - isHiddenCollection, - ); - dispatch({ - type: "setAllCollections", - collections, - hiddenCollections, - }); - const didUpdateNormalFiles = await syncFiles( - "normal", - collections, - (files) => dispatch({ type: "setFiles", files }), - (files) => dispatch({ type: "fetchFiles", files }), - ); - const didUpdateHiddenFiles = await syncFiles( - "hidden", - hiddenCollections, - (hiddenFiles) => - dispatch({ type: "setHiddenFiles", hiddenFiles }), - (hiddenFiles) => - dispatch({ type: "fetchHiddenFiles", hiddenFiles }), - ); - if (didUpdateNormalFiles || didUpdateHiddenFiles) - exportService.onLocalFilesUpdated(); - await syncTrash(allCollections, (trashedFiles: EnteFile[]) => - dispatch({ type: "setTrashedFiles", trashedFiles }), - ); - // 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. - // - // Do the non-file-related sync only for one of these parallel ones. - if (!isForced) { - await sync(); + syncInProgress.current = false; + if (resync.current) { + const { force, silent } = resync.current; + setTimeout(() => handleSyncWithRemote(force, silent), 0); + resync.current = undefined; } - } catch (e) { - switch (e.message) { - case CustomError.SESSION_EXPIRED: - showSessionExpiredDialog(); - break; - case CustomError.KEY_MISSING: - clearKeys(); - router.push(PAGES.CREDENTIALS); - break; - default: - log.error("syncWithRemote failed", e); - } - } finally { - dispatch({ type: "clearUnsyncedState" }); - !silent && hideLoadingBar(); - } - syncInProgress.current = false; - if (resync.current) { - const { force, silent } = resync.current; - setTimeout(() => syncWithRemote(force, silent), 0); - resync.current = undefined; - } - }; + }, + [showLoadingBar, hideLoadingBar, router, showSessionExpiredDialog], + ); + + // Alias for existing code. + const syncWithRemote = handleSyncWithRemote; const setupSelectAllKeyBoardShortcutHandler = () => { const handleKeyUp = (e: KeyboardEvent) => { @@ -667,8 +630,8 @@ const Page: React.FC = () => { }; const setFilesDownloadProgressAttributesCreator: SetFilesDownloadProgressAttributesCreator = - (folderName, collectionID, isHidden) => { - const id = filesDownloadProgressAttributesList?.length ?? 0; + useCallback((folderName, collectionID, isHidden) => { + const id = Math.random(); const updater: SetFilesDownloadProgressAttributes = (value) => { setFilesDownloadProgressAttributesList((prev) => { const attributes = prev?.find((attr) => attr.id === id); @@ -697,7 +660,7 @@ const Page: React.FC = () => { downloadDirPath: null, }); return updater; - }; + }, []); const collectionOpsHelper = (ops: COLLECTION_OPS_TYPE) => async (collection: Collection) => { @@ -744,7 +707,7 @@ const Page: React.FC = () => { await handleFileOps( ops, toProcessFiles, - (files) => dispatch({ type: "markTempDeleted", files }), + handleMarkTempDeleted, () => dispatch({ type: "clearTempDeleted" }), (files) => dispatch({ type: "markTempHidden", files }), () => dispatch({ type: "clearTempHidden" }), @@ -836,12 +799,41 @@ const Page: React.FC = () => { const openHiddenSection: GalleryContextType["openHiddenSection"] = ( callback, ) => { - authenticateUser(() => { + authenticateUser().then(() => { dispatch({ type: "showHidden" }); callback?.(); }); }; + const handleMarkUnsyncedFavoriteUpdate = useCallback( + (fileID: number, isFavorite: boolean) => + dispatch({ + type: "markUnsyncedFavoriteUpdate", + fileID, + isFavorite, + }), + [], + ); + + const handleMarkTempDeleted = useCallback( + (files: EnteFile[]) => dispatch({ type: "markTempDeleted", files }), + [], + ); + + const handleSelectCollection = useCallback( + (collectionID: number) => + dispatch({ + type: "showNormalOrHiddenCollectionSummary", + collectionSummaryID: collectionID, + }), + [], + ); + + const handleSelectPerson = useCallback( + (personID: string) => dispatch({ type: "showPerson", personID }), + [], + ); + const handleOpenCollectionSelector = useCallback( (attributes: CollectionSelectorAttributes) => { setCollectionSelectorAttributes(attributes); @@ -860,6 +852,8 @@ const Page: React.FC = () => { if (!user) { // Don't render until we dispatch "mount" with the logged in user. + // + // Tag: [Note: Gallery children can assume user] return
; } @@ -867,18 +861,10 @@ const Page: React.FC = () => { - dispatch({ - type: "showNormalOrHiddenCollectionSummary", - collectionSummaryID: id, - }), syncWithRemote, setBlockingLoad, photoListHeader, - openExportModal: showExport, - authenticateUser, userIDToEmailMap, user, emailList, @@ -889,21 +875,14 @@ const Page: React.FC = () => { }} > - {blockingLoad && } { /> ) : ( - dispatch({ type: "enterSearchMode" }), - onSelectSearchOption: handleSelectSearchOption, - onSelectPeople: () => - dispatch({ type: "showPeople" }), - onSelectPerson: (personID) => - dispatch({ - type: "showPerson", - personID, - }), - }} + {...{ isInSearchMode }} + onSidebar={showSidebar} + onUpload={openUploader} + onShowSearchInput={() => + dispatch({ type: "enterSearchMode" }) + } + onSelectSearchOption={handleSelectSearchOption} + onSelectPeople={() => + dispatch({ type: "showPeople" }) + } + onSelectPerson={handleSelectPerson} /> )} @@ -1023,8 +998,7 @@ const Page: React.FC = () => { ? state.view.visiblePeople : undefined) ?? [], activePerson, - onSelectPerson: (personID) => - dispatch({ type: "showPerson", personID }), + onSelectPerson: handleSelectPerson, setCollectionNamerAttributes, setPhotoListHeader, setFilesDownloadProgressAttributesCreator, @@ -1032,7 +1006,7 @@ const Page: React.FC = () => { }} /> - { onUploadFile={(file) => dispatch({ type: "uploadFile", file }) } + onShowPlanSelector={showPlanSelector} setCollections={(collections) => dispatch({ type: "setNormalCollections", collections }) } @@ -1056,20 +1031,16 @@ const Page: React.FC = () => { showSessionExpiredMessage={showSessionExpiredDialog} {...{ dragAndDropFiles, - openFileSelector, - fileSelectorFiles, - openFolderSelector, - folderSelectorFiles, - openZipFileSelector, - fileSelectorZipFiles, uploadTypeSelectorIntent, uploadTypeSelectorView, }} /> {!isInSearchMode && @@ -1088,47 +1059,45 @@ const Page: React.FC = () => { mode={barMode} modePlus={isInSearchMode ? "search" : barMode} files={filteredFiles} - syncWithRemote={syncWithRemote} setSelected={setSelected} selected={selected} favoriteFileIDs={state.favoriteFileIDs} - markUnsyncedFavoriteUpdate={(fileID, isFavorite) => - dispatch({ - type: "markUnsyncedFavoriteUpdate", - fileID, - isFavorite, - }) - } - markTempDeleted={(files) => - dispatch({ type: "markTempDeleted", files }) - } setIsPhotoSwipeOpen={setIsPhotoSwipeOpen} activeCollectionID={activeCollectionID} activePersonID={activePerson?.id} enableDownload={true} - fileToCollectionsMap={state.fileCollectionIDs} - collectionNameMap={state.allCollectionNameByID} + fileCollectionIDs={state.fileCollectionIDs} + allCollectionsNameByID={state.allCollectionsNameByID} showAppDownloadBanner={ files.length < 30 && !isInSearchMode } + isInIncomingSharedCollection={ + collectionSummaries.get(activeCollectionID)?.type == + "incomingShareCollaborator" || + collectionSummaries.get(activeCollectionID)?.type == + "incomingShareViewer" + } isInHiddenSection={barMode == "hidden-albums"} setFilesDownloadProgressAttributesCreator={ setFilesDownloadProgressAttributesCreator } selectable={true} - onSelectPerson={(personID) => { - dispatch({ type: "showPerson", personID }); - }} + onMarkUnsyncedFavoriteUpdate={ + handleMarkUnsyncedFavoriteUpdate + } + onMarkTempDeleted={handleMarkTempDeleted} + onSyncWithRemote={handleSyncWithRemote} + onSelectCollection={handleSelectCollection} + onSelectPerson={handleSelectPerson} /> )} - @@ -1164,19 +1133,25 @@ const preloadImage = (imgBasePath: string) => { }; type NormalNavbarContentsProps = SearchBarProps & { - openSidebar: () => void; - openUploader: () => void; + /** + * Called when the user activates the sidebar icon. + */ + onSidebar: () => void; + /** + * Called when the user activates the upload button. + */ + onUpload: () => void; }; const NormalNavbarContents: React.FC = ({ - openSidebar, - openUploader, + onSidebar, + onUpload, ...props }) => ( <> - {!props.isInSearchMode && } + {!props.isInSearchMode && } - {!props.isInSearchMode && } + {!props.isInSearchMode && } ); diff --git a/web/apps/photos/src/pages/generate.tsx b/web/apps/photos/src/pages/generate.tsx index 244da239bb..bf45ab3f5a 100644 --- a/web/apps/photos/src/pages/generate.tsx +++ b/web/apps/photos/src/pages/generate.tsx @@ -1,6 +1 @@ -import Page_ from "@/accounts/pages/generate"; -import { useAppContext } from "@/new/photos/types/context"; - -const Page = () => ; - -export default Page; +export { default } from "@/accounts/pages/generate"; diff --git a/web/apps/photos/src/pages/index.tsx b/web/apps/photos/src/pages/index.tsx index 695fdec9d6..e223425a18 100644 --- a/web/apps/photos/src/pages/index.tsx +++ b/web/apps/photos/src/pages/index.tsx @@ -4,10 +4,10 @@ import { CenteredFill, CenteredRow } from "@/base/components/containers"; import { EnteLogo } from "@/base/components/EnteLogo"; import { ActivityIndicator } from "@/base/components/mui/ActivityIndicator"; import { FocusVisibleButton } from "@/base/components/mui/FocusVisibleButton"; +import { useBaseContext } from "@/base/context"; import log from "@/base/log"; import { albumsAppOrigin, customAPIHost } from "@/base/origins"; import { DevSettings } from "@/new/photos/components/DevSettings"; -import { useAppContext } from "@/new/photos/types/context"; import { PHOTOS_PAGES as PAGES } from "@ente/shared/constants/pages"; import { saveKeyInSessionStore } from "@ente/shared/crypto/helpers"; import localForage from "@ente/shared/storage/localForage"; @@ -21,7 +21,7 @@ import React, { useCallback, useEffect, useRef, useState } from "react"; import { Trans } from "react-i18next"; const Page: React.FC = () => { - const { showMiniDialog } = useAppContext(); + const { showMiniDialog } = useBaseContext(); const [loading, setLoading] = useState(true); const [showLogin, setShowLogin] = useState(true); diff --git a/web/apps/photos/src/pages/login.tsx b/web/apps/photos/src/pages/login.tsx index 46a8d69531..91d1d3fd41 100644 --- a/web/apps/photos/src/pages/login.tsx +++ b/web/apps/photos/src/pages/login.tsx @@ -1,3 +1 @@ -import Page from "@/accounts/pages/login"; - -export default Page; +export { default } from "@/accounts/pages/login"; diff --git a/web/apps/photos/src/pages/passkeys/finish.tsx b/web/apps/photos/src/pages/passkeys/finish.tsx index cc9baa5214..0601be631e 100644 --- a/web/apps/photos/src/pages/passkeys/finish.tsx +++ b/web/apps/photos/src/pages/passkeys/finish.tsx @@ -1,3 +1 @@ -import Page from "@/accounts/pages/passkeys/finish"; - -export default Page; +export { default } from "@/accounts/pages/passkeys/finish"; diff --git a/web/apps/photos/src/pages/passkeys/recover.tsx b/web/apps/photos/src/pages/passkeys/recover.tsx index 6584f5bacc..647a9d4aa2 100644 --- a/web/apps/photos/src/pages/passkeys/recover.tsx +++ b/web/apps/photos/src/pages/passkeys/recover.tsx @@ -1,8 +1,5 @@ import Page_ from "@/accounts/pages/two-factor/recover"; -import { useAppContext } from "@/new/photos/types/context"; -const Page = () => ( - -); +const Page = () => ; export default Page; diff --git a/web/apps/photos/src/pages/recover.tsx b/web/apps/photos/src/pages/recover.tsx index 1bb15511cb..e1398fabb4 100644 --- a/web/apps/photos/src/pages/recover.tsx +++ b/web/apps/photos/src/pages/recover.tsx @@ -1,6 +1 @@ -import Page_ from "@/accounts/pages/recover"; -import { useAppContext } from "@/new/photos/types/context"; - -const Page = () => ; - -export default Page; +export { default } from "@/accounts/pages/recover"; diff --git a/web/apps/photos/src/pages/shared-albums.tsx b/web/apps/photos/src/pages/shared-albums.tsx index 8923448ce8..25812ee9a3 100644 --- a/web/apps/photos/src/pages/shared-albums.tsx +++ b/web/apps/photos/src/pages/shared-albums.tsx @@ -19,15 +19,15 @@ import { useIsSmallWidth, useIsTouchscreen, } from "@/base/components/utils/hooks"; +import { useBaseContext } from "@/base/context"; import { isHTTP401Error, PublicAlbumsCredentials } from "@/base/http"; import log from "@/base/log"; import { FullScreenDropZone } from "@/gallery/components/FullScreenDropZone"; -import { useFileInput } from "@/gallery/components/utils/use-file-input"; import { downloadManager } from "@/gallery/services/download"; import { extractCollectionKeyFromShareURL } from "@/gallery/services/share"; import { updateShouldDisableCFUploadProxy } from "@/gallery/services/upload"; import type { Collection } from "@/media/collection"; -import { type EnteFile, mergeMetadata } from "@/media/file"; +import { mergeMetadata, type EnteFile } from "@/media/file"; import { verifyPublicAlbumPassword } from "@/new/albums/services/publicCollection"; import { GalleryItemsHeaderAdapter, @@ -38,7 +38,7 @@ import { isHiddenCollection, } from "@/new/photos/services/collection"; import { sortFiles } from "@/new/photos/services/files"; -import { useAppContext } from "@/new/photos/types/context"; +import { usePhotosAppContext } from "@/new/photos/types/context"; import { CenteredFlex } from "@ente/shared/components/Container"; import SingleInputForm, { type SingleInputFormProps, @@ -57,12 +57,11 @@ import { } from "components/FilesDownloadProgress"; import PhotoFrame from "components/PhotoFrame"; import { ITEM_TYPE, TimeStampListItem } from "components/PhotoList"; -import Uploader from "components/Upload/Uploader"; -import { UploadSelectorInputs } from "components/UploadSelectorInputs"; +import { Upload } from "components/Upload"; import { t } from "i18next"; import { useRouter } from "next/router"; -import { useEffect, useMemo, useRef, useState } from "react"; -import { useDropzone } from "react-dropzone"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { type FileWithPath } from "react-dropzone"; import { getLocalPublicCollection, getLocalPublicCollectionPassword, @@ -93,7 +92,8 @@ export default function PublicCollectionGallery() { const [publicFiles, setPublicFiles] = useState(null); const [publicCollection, setPublicCollection] = useState(null); const [errorMessage, setErrorMessage] = useState(null); - const { showLoadingBar, hideLoadingBar, showMiniDialog } = useAppContext(); + const { showMiniDialog } = useBaseContext(); + const { showLoadingBar, hideLoadingBar } = usePhotosAppContext(); const [loading, setLoading] = useState(true); const router = useRouter(); const [isPasswordProtected, setIsPasswordProtected] = @@ -108,6 +108,9 @@ export default function PublicCollectionGallery() { const [uploadTypeSelectorView, setUploadTypeSelectorView] = useState(false); const [blockingLoad, setBlockingLoad] = useState(false); const [shouldDisableDropzone, setShouldDisableDropzone] = useState(false); + const [dragAndDropFiles, setDragAndDropFiles] = useState( + [], + ); const [selected, setSelected] = useState({ ownCount: 0, count: 0, @@ -115,38 +118,14 @@ export default function PublicCollectionGallery() { context: undefined, }); - const { - getRootProps: getDragAndDropRootProps, - getInputProps: getDragAndDropInputProps, - acceptedFiles: dragAndDropFilesReadOnly, - } = useDropzone({ - noClick: true, - noKeyboard: true, - disabled: shouldDisableDropzone, - }); - const { - getInputProps: getFileSelectorInputProps, - openSelector: openFileSelector, - selectedFiles: fileSelectorFiles, - } = useFileInput({ - directory: false, - }); - const { - getInputProps: getFolderSelectorInputProps, - openSelector: openFolderSelector, - selectedFiles: folderSelectorFiles, - } = useFileInput({ - directory: true, - }); - const [ filesDownloadProgressAttributesList, setFilesDownloadProgressAttributesList, ] = useState([]); const setFilesDownloadProgressAttributesCreator: SetFilesDownloadProgressAttributesCreator = - (folderName, collectionID, isHidden) => { - const id = filesDownloadProgressAttributesList?.length ?? 0; + useCallback((folderName, collectionID, isHidden) => { + const id = Math.random(); const updater: SetFilesDownloadProgressAttributes = (value) => { setFilesDownloadProgressAttributesList((prev) => { const attributes = prev?.find((attr) => attr.id === id); @@ -175,13 +154,7 @@ export default function PublicCollectionGallery() { downloadDirPath: null, }); return updater; - }; - - // Create a regular array from the readonly array returned by dropzone. - const dragAndDropFiles = useMemo( - () => [...dragAndDropFilesReadOnly], - [dragAndDropFilesReadOnly], - ); + }, []); const onAddPhotos = useMemo(() => { return publicCollection?.publicURLs?.[0]?.enableCollect @@ -316,7 +289,7 @@ export default function PublicCollectionGallery() { ); }, [onAddPhotos]); - const syncWithRemote = async () => { + const handleSyncWithRemote = useCallback(async () => { const collectionUID = getPublicCollectionUID( credentials.current.accessToken, ); @@ -396,7 +369,10 @@ export default function PublicCollectionGallery() { hideLoadingBar(); setLoading(false); } - }; + }, [showLoadingBar, hideLoadingBar]); + + // TODO: See gallery + const syncWithRemote = handleSyncWithRemote; const verifyLinkPassword: SingleInputFormProps["callback"] = async ( password, @@ -504,14 +480,10 @@ export default function PublicCollectionGallery() { return ( - - + {blockingLoad && } - ; +const Page = () => ; export default Page; diff --git a/web/apps/photos/src/pages/two-factor/setup.tsx b/web/apps/photos/src/pages/two-factor/setup.tsx index bab74ddd4b..ee74c573b6 100644 --- a/web/apps/photos/src/pages/two-factor/setup.tsx +++ b/web/apps/photos/src/pages/two-factor/setup.tsx @@ -1,3 +1 @@ -import Page from "@/accounts/pages/two-factor/setup"; - -export default Page; +export { default } from "@/accounts/pages/two-factor/setup"; diff --git a/web/apps/photos/src/pages/two-factor/verify.tsx b/web/apps/photos/src/pages/two-factor/verify.tsx index f6c9b6a494..5f3fbef9ce 100644 --- a/web/apps/photos/src/pages/two-factor/verify.tsx +++ b/web/apps/photos/src/pages/two-factor/verify.tsx @@ -1,6 +1 @@ -import Page_ from "@/accounts/pages/two-factor/verify"; -import { useAppContext } from "@/new/photos/types/context"; - -const Page = () => ; - -export default Page; +export { default } from "@/accounts/pages/two-factor/verify"; diff --git a/web/apps/photos/src/pages/verify.tsx b/web/apps/photos/src/pages/verify.tsx index c778bab6e8..b5860d3a53 100644 --- a/web/apps/photos/src/pages/verify.tsx +++ b/web/apps/photos/src/pages/verify.tsx @@ -1,6 +1 @@ -import Page_ from "@/accounts/pages/verify"; -import { useAppContext } from "@/new/photos/types/context"; - -const Page = () => ; - -export default Page; +export { default } from "@/accounts/pages/verify"; diff --git a/web/apps/photos/src/services/collectionService.ts b/web/apps/photos/src/services/collectionService.ts index a8dc225337..211021c33b 100644 --- a/web/apps/photos/src/services/collectionService.ts +++ b/web/apps/photos/src/services/collectionService.ts @@ -1,6 +1,8 @@ import { encryptMetadataJSON, sharedCryptoWorker } from "@/base/crypto"; import log from "@/base/log"; import { apiURL } from "@/base/origins"; +import { UpdateMagicMetadataRequest } from "@/gallery/services/file"; +import { updateMagicMetadata } from "@/gallery/services/magic-metadata"; import { Collection, CollectionMagicMetadata, @@ -35,7 +37,6 @@ import { groupFilesByCollectionID, sortFiles, } from "@/new/photos/services/files"; -import { updateMagicMetadata } from "@/new/photos/services/magic-metadata"; import type { FamilyData } from "@/new/photos/services/user-details"; import { batch } from "@/utils/array"; import HTTPService from "@ente/shared/network/HTTPService"; @@ -48,7 +49,6 @@ import { isQuickLinkCollection, isValidMoveTarget, } from "utils/collection"; -import { UpdateMagicMetadataRequest } from "./fileService"; import { getPublicKey } from "./userService"; const UNCATEGORIZED_COLLECTION_NAME = "Uncategorized"; @@ -139,11 +139,17 @@ export const createFavoritesCollection = () => { return createCollection(FAVORITE_COLLECTION_NAME, CollectionType.favorites); }; -export const addToFavorites = async (file: EnteFile) => { - await addMultipleToFavorites([file]); +export const addToFavorites = async ( + file: EnteFile, + disableOldWorkaround?: boolean, +) => { + await addMultipleToFavorites([file], disableOldWorkaround); }; -export const addMultipleToFavorites = async (files: EnteFile[]) => { +export const addMultipleToFavorites = async ( + files: EnteFile[], + disableOldWorkaround?: boolean, +) => { try { let favCollection = await getFavCollection(); if (!favCollection) { @@ -152,10 +158,19 @@ export const addMultipleToFavorites = async (files: EnteFile[]) => { await addToCollection(favCollection, files); } catch (e) { log.error("failed to add to favorite", e); + // Old code swallowed the error here. This isn't good, but to + // avoid changing existing behaviour only new code will set the + // disableOldWorkaround flag to instead rethrow it. + // + // TODO: Migrate old code, remove this flag, always throw. + if (disableOldWorkaround) throw e; } }; -export const removeFromFavorites = async (file: EnteFile) => { +export const removeFromFavorites = async ( + file: EnteFile, + disableOldWorkaround?: boolean, +) => { try { const favCollection = await getFavCollection(); if (!favCollection) { @@ -164,6 +179,8 @@ export const removeFromFavorites = async (file: EnteFile) => { await removeFromCollection(favCollection.id, [file]); } catch (e) { log.error("remove from favorite failed", e); + // TODO: See disableOldWorkaround in addMultipleToFavorites. + if (disableOldWorkaround) throw e; } }; diff --git a/web/apps/photos/src/services/export/index.ts b/web/apps/photos/src/services/export/index.ts index bd5394c792..4ffc035aaa 100644 --- a/web/apps/photos/src/services/export/index.ts +++ b/web/apps/photos/src/services/export/index.ts @@ -888,7 +888,7 @@ class ExportService { try { const exportRecord = await this.getExportRecord(folder); const newRecord: ExportRecord = { ...exportRecord, ...newData }; - await ensureElectron().fs.writeFile( + await ensureElectron().fs.writeFileViaBackup( joinPath(folder, exportRecordFileName), JSON.stringify(newRecord, null, 2), ); diff --git a/web/apps/photos/src/services/upload/takeout.ts b/web/apps/photos/src/services/upload/takeout.ts index 78527f9325..4d8c740832 100644 --- a/web/apps/photos/src/services/upload/takeout.ts +++ b/web/apps/photos/src/services/upload/takeout.ts @@ -5,8 +5,8 @@ import { ensureElectron } from "@/base/electron"; import { nameAndExtension } from "@/base/file-name"; import log from "@/base/log"; import { type Location } from "@/base/types"; +import type { UploadItem } from "@/gallery/services/upload"; import { readStream } from "@/gallery/utils/native-stream"; -import type { UploadItem } from "@/new/photos/services/upload/types"; import { maybeParseInt } from "@/utils/parse"; /** diff --git a/web/apps/photos/src/services/upload/thumbnail.ts b/web/apps/photos/src/services/upload/thumbnail.ts index fd7c81e852..943d724b38 100644 --- a/web/apps/photos/src/services/upload/thumbnail.ts +++ b/web/apps/photos/src/services/upload/thumbnail.ts @@ -1,14 +1,14 @@ import log from "@/base/log"; import { type Electron } from "@/base/types/ipc"; import * as ffmpeg from "@/gallery/services/ffmpeg"; +import { + toDataOrPathOrZipEntry, + type DesktopUploadItem, +} from "@/gallery/services/upload"; import { FileType, type FileTypeInfo } from "@/media/file-type"; import { isHEICExtension } from "@/media/formats"; import { heicToJPEG } from "@/media/heic-convert"; import { scaledImageDimensions } from "@/media/image"; -import { - toDataOrPathOrZipEntry, - type DesktopUploadItem, -} from "@/new/photos/services/upload/types"; import { withTimeout } from "@/utils/promise"; /** Maximum width or height of the generated thumbnail */ diff --git a/web/apps/photos/src/services/upload/upload-service.ts b/web/apps/photos/src/services/upload/upload-service.ts index dc8208cbdb..87603866f3 100644 --- a/web/apps/photos/src/services/upload/upload-service.ts +++ b/web/apps/photos/src/services/upload/upload-service.ts @@ -5,7 +5,17 @@ import { ensureElectron } from "@/base/electron"; import { basename, nameAndExtension } from "@/base/file-name"; import type { PublicAlbumsCredentials } from "@/base/http"; import log from "@/base/log"; +import { extractExif } from "@/gallery/services/exif"; import { extractVideoMetadata } from "@/gallery/services/ffmpeg"; +import { + getNonEmptyMagicMetadataProps, + updateMagicMetadata, +} from "@/gallery/services/magic-metadata"; +import type { UploadItem } from "@/gallery/services/upload"; +import { + RANDOM_PERCENTAGE_PROGRESS_FOR_PUT, + UPLOAD_RESULT, +} from "@/gallery/services/upload"; import { detectFileTypeInfoFromChunk, isFileTypeNotSupportedError, @@ -29,16 +39,6 @@ import { import { FileType, type FileTypeInfo } from "@/media/file-type"; import { encodeLivePhoto } from "@/media/live-photo"; import { addToCollection } from "@/new/photos/services/collection"; -import { extractExif } from "@/new/photos/services/exif"; -import { - getNonEmptyMagicMetadataProps, - updateMagicMetadata, -} from "@/new/photos/services/magic-metadata"; -import type { UploadItem } from "@/new/photos/services/upload/types"; -import { - RANDOM_PERCENTAGE_PROGRESS_FOR_PUT, - UPLOAD_RESULT, -} from "@/new/photos/services/upload/types"; import { mergeUint8Arrays } from "@/utils/array"; import { ensureInteger, ensureNumber } from "@/utils/ensure"; import { CustomError, handleUploadError } from "@ente/shared/error"; diff --git a/web/apps/photos/src/services/upload/uploadManager.ts b/web/apps/photos/src/services/upload/uploadManager.ts index 6e006797ac..42f05604f8 100644 --- a/web/apps/photos/src/services/upload/uploadManager.ts +++ b/web/apps/photos/src/services/upload/uploadManager.ts @@ -6,7 +6,13 @@ import type { PublicAlbumsCredentials } from "@/base/http"; import log from "@/base/log"; import type { Electron } from "@/base/types/ipc"; import { ComlinkWorker } from "@/base/worker/comlink-worker"; -import { shouldDisableCFUploadProxy } from "@/gallery/services/upload"; +import type { UploadItem } from "@/gallery/services/upload"; +import { + RANDOM_PERCENTAGE_PROGRESS_FOR_PUT, + UPLOAD_RESULT, + shouldDisableCFUploadProxy, + type UploadPhase, +} from "@/gallery/services/upload"; import type { Collection } from "@/media/collection"; import { decryptFile, @@ -18,12 +24,6 @@ import { FileType } from "@/media/file-type"; import { potentialFileTypeFromExtension } from "@/media/live-photo"; import { getLocalFiles } from "@/new/photos/services/files"; import { indexNewUpload } from "@/new/photos/services/ml"; -import type { UploadItem } from "@/new/photos/services/upload/types"; -import { - RANDOM_PERCENTAGE_PROGRESS_FOR_PUT, - UPLOAD_RESULT, - type UploadPhase, -} from "@/new/photos/services/upload/types"; import { wait } from "@/utils/promise"; import { CustomError } from "@ente/shared/error"; import { Canceler } from "axios"; @@ -844,8 +844,15 @@ const markUploaded = async (electron: Electron, item: ClusteredUploadItem) => { } else if (p && typeof p == "object" && "path" in p) { electron.markUploadedFiles([p.path]); } else { - throw new Error( - "Attempting to mark upload completion of unexpected desktop upload items", + // We can come here when the user saves an image they've edited, in + // which case `item` will be a web File object which won't have a + // path. Such a la carte uploads don't mark the file as pending + // anyways, so there isn't anything to do also. + // + // Keeping a log here, though really the upper layers of the code + // need to be reworked so that we don't even get here in such cases. + log.info( + "Ignoring attempt to mark upload completion of (likely edited) item", ); } } diff --git a/web/apps/photos/src/services/userService.ts b/web/apps/photos/src/services/userService.ts index bb70ec8b87..64a9a0c8d8 100644 --- a/web/apps/photos/src/services/userService.ts +++ b/web/apps/photos/src/services/userService.ts @@ -80,51 +80,3 @@ export const getUserDetailsV2 = async (): Promise => { throw e; } }; - -export interface DeleteChallengeResponse { - allowDelete: boolean; - encryptedChallenge: string; -} - -export const getAccountDeleteChallenge = async () => { - try { - const token = getToken(); - - const resp = await HTTPService.get( - await apiURL("/users/delete-challenge"), - null, - { - "X-Auth-Token": token, - }, - ); - return resp.data as DeleteChallengeResponse; - } catch (e) { - log.error("failed to get account delete challenge", e); - throw e; - } -}; - -export const deleteAccount = async ( - challenge: string, - reason: string, - feedback: string, -) => { - try { - const token = getToken(); - if (!token) { - return; - } - - await HTTPService.delete( - await apiURL("/users/delete"), - { challenge, reason, feedback }, - null, - { - "X-Auth-Token": token, - }, - ); - } catch (e) { - log.error("deleteAccount api call failed", e); - throw e; - } -}; diff --git a/web/apps/photos/src/services/watch.ts b/web/apps/photos/src/services/watch.ts index af090d347f..8266a5e0f3 100644 --- a/web/apps/photos/src/services/watch.ts +++ b/web/apps/photos/src/services/watch.ts @@ -11,13 +11,13 @@ import type { FolderWatch, FolderWatchSyncedFile, } from "@/base/types/ipc"; +import { UPLOAD_RESULT } from "@/gallery/services/upload"; import type { Collection } from "@/media/collection"; import { EncryptedEnteFile } from "@/media/file"; import { getLocalFiles, groupFilesByCollectionID, } from "@/new/photos/services/files"; -import { UPLOAD_RESULT } from "@/new/photos/services/upload/types"; import { ensureString } from "@/utils/ensure"; import debounce from "debounce"; import uploadManager, { diff --git a/web/apps/photos/src/styles/global.css b/web/apps/photos/src/styles/global.css index d480d0dc6b..9d61f12f42 100644 --- a/web/apps/photos/src/styles/global.css +++ b/web/apps/photos/src/styles/global.css @@ -114,3 +114,115 @@ body { .pswp__caption--empty { display: none; } + +.pswp-ente { + /* The default z-index for PhotoSwipe is 10k, way beyond everything else. + Give it a more moderate value so that MUI elements can be used with it. */ + z-index: calc(var(--mui-zIndex-drawer) - 1); +} + +/* Shift the top bar by a fraction of the desktop title bar, if any. The top bar + contents already have a decent top padding, so we don't need to shift by the + entire amount. See also: [Note: Customize the desktop title bar] */ +.pswp-ente .pswp__top-bar { + top: calc(env(titlebar-area-height, 0px) * 0.4); +} + +/* + Make the controllable video elements we render as custom PhotoSwipe content + take up the entire container. + */ +.pswp-ente video[controls] { + width: 100%; + height: 100%; +} + +.pswp-ente .pswp__preloader--active .pswp__icn { + opacity: 0.4; +} + +/* + Error indicator on the file viewer. + + It is styled similar to the loading indicator provided by PhotoSwipe since it + is meant to occupy the same space. Only one of these will be shown at the same + time, so it can also set the auto right margin. + */ +.pswp-ente .pswp__error { + position: relative; + overflow: hidden; + width: 50px; + height: 60px; + /* Unlike the loading indicator, "display" is used to toggle visibility, and + the opacity is fixed to be similar to that of the counter. */ + display: none; + opacity: 0.85; +} + +.pswp-ente .pswp__error .pswp__icn { + /* Use a warning color for the error icon */ + fill: var(--mui-palette-fixed-golden); +} + +.pswp-ente .pswp__error .pswp__icn-shadow { + /* Reduce the stroke from default (2px) to make it look better with the + golden icon outline */ + stroke-width: 1px; +} + +/* The ".pswp--ui-visible .pswp__hide-on-close" selector in PhotoSwipe's CSS + sets the opacity of the arrows to 1, which doesn't match the rest of the + controls (0.85). */ +.pswp-ente.pswp--ui-visible .pswp__hide-on-close.pswp__button--arrow { + opacity: 0.85; +} + +.pswp-ente .pswp__error--active { + display: initial; +} + +/* Scale the built in controls to better fit our requirements */ +.pswp-ente .pswp__button--zoom .pswp__icn { + transform: scale(0.85); +} + +.pswp-ente .pswp__button--close .pswp__icn { + transform: translate(-6px, 0) scale(0.925); +} + +.pswp-ente .pswp__button--close { + margin-right: 2px; +} + +.pswp-ente .pswp__button--arrow--prev .pswp__icn { + transform: scale(0.8); +} + +.pswp-ente .pswp__button--arrow--next .pswp__icn { + /* default is a horizontal flip, transform: scale(-1, 1); */ + transform: scale(-0.8, 0.8); +} + +.pswp-ente .pswp__caption { + position: absolute; + bottom: 0px; + right: 0; + margin: 20px 24px; + padding: 6px 16px; + border-radius: 2px; + /* Same opacity as the other controls. */ + color: rgba(255 255 255 / 0.85); + background-color: rgba(0 0 0 / 0.2); + backdrop-filter: blur(10px); + /* 4 lines max, ellipsis on overflow. */ + word-break: break-word; + text-align: right; + max-width: 375px; + max-height: 200px; + overflow: hidden; + text-overflow: ellipsis; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 4; + line-clamp: 4; +} diff --git a/web/apps/photos/src/types/gallery/index.ts b/web/apps/photos/src/types/gallery/index.ts index dffe171cc3..264dd314cf 100644 --- a/web/apps/photos/src/types/gallery/index.ts +++ b/web/apps/photos/src/types/gallery/index.ts @@ -39,15 +39,10 @@ export interface MergedSourceURL { } export interface GalleryContextType { - showPlanSelectorModal: () => void; setActiveCollectionID: (collectionID: number) => void; - /** Newer and almost equivalent alternative to setActiveCollectionID. */ - onShowCollection: (collectionID: number) => void; syncWithRemote: (force?: boolean, silent?: boolean) => Promise; setBlockingLoad: (value: boolean) => void; photoListHeader: TimeStampListItem; - openExportModal: () => void; - authenticateUser: (callback: () => void) => void; user: User; userIDToEmailMap: Map; emailList: string[]; diff --git a/web/apps/photos/src/utils/collection.ts b/web/apps/photos/src/utils/collection.ts index f2e64d0b90..2cf35df026 100644 --- a/web/apps/photos/src/utils/collection.ts +++ b/web/apps/photos/src/utils/collection.ts @@ -1,6 +1,7 @@ import { ensureElectron } from "@/base/electron"; import { joinPath } from "@/base/file-name"; import log from "@/base/log"; +import { updateMagicMetadata } from "@/gallery/services/magic-metadata"; import { COLLECTION_ROLE, type Collection, @@ -26,7 +27,6 @@ import { getLocalCollections, } from "@/new/photos/services/collections"; import { getAllLocalFiles, getLocalFiles } from "@/new/photos/services/files"; -import { updateMagicMetadata } from "@/new/photos/services/magic-metadata"; import { safeDirectoryName } from "@/new/photos/utils/native-fs"; import { LS_KEYS, getData } from "@ente/shared/storage/localStorage"; import type { User } from "@ente/shared/user/types"; @@ -348,14 +348,6 @@ export function isValidReplacementAlbum( ); } -export function getCollectionNameMap( - collections: Collection[], -): Map { - return new Map( - collections.map((collection) => [collection.id, collection.name]), - ); -} - export const getOrCreateAlbum = async ( albumName: string, existingCollections: Collection[], diff --git a/web/apps/photos/src/utils/file/index.ts b/web/apps/photos/src/utils/file/index.ts index 1c2bdc2826..d3aa10b471 100644 --- a/web/apps/photos/src/utils/file/index.ts +++ b/web/apps/photos/src/utils/file/index.ts @@ -3,24 +3,22 @@ import log from "@/base/log"; import { type Electron } from "@/base/types/ipc"; import { downloadAndRevokeObjectURL } from "@/base/utils/web"; import { downloadManager } from "@/gallery/services/download"; +import { updateFileMagicMetadata } from "@/gallery/services/file"; +import { + isArchivedFile, + updateMagicMetadata, +} from "@/gallery/services/magic-metadata"; import { detectFileTypeInfo } from "@/gallery/utils/detect-type"; import { writeStream } from "@/gallery/utils/native-stream"; import { EnteFile, FileMagicMetadataProps, - FilePublicMagicMetadata, - FilePublicMagicMetadataProps, FileWithUpdatedMagicMetadata, - mergeMetadata, } from "@/media/file"; import { ItemVisibility } from "@/media/file-metadata"; import { FileType } from "@/media/file-type"; import { decodeLivePhoto } from "@/media/live-photo"; import { deleteFromTrash, moveToTrash } from "@/new/photos/services/collection"; -import { - isArchivedFile, - updateMagicMetadata, -} from "@/new/photos/services/magic-metadata"; import { safeFileName } from "@/new/photos/utils/native-fs"; import { withTimeout } from "@/utils/promise"; import { LS_KEYS, getData } from "@ente/shared/storage/localStorage"; @@ -30,10 +28,6 @@ import { addMultipleToFavorites, moveToHiddenCollection, } from "services/collectionService"; -import { - updateFileMagicMetadata, - updateFilePublicMagicMetadata, -} from "services/fileService"; import { SelectedState, SetFilesDownloadProgressAttributes, @@ -122,46 +116,6 @@ export async function changeFilesVisibility( return await updateFileMagicMetadata(fileWithUpdatedMagicMetadataList); } -export async function changeFileName( - file: EnteFile, - editedName: string, -): Promise { - const updatedPublicMagicMetadataProps: FilePublicMagicMetadataProps = { - editedName, - }; - - const updatedPublicMagicMetadata: FilePublicMagicMetadata = - await updateMagicMetadata( - updatedPublicMagicMetadataProps, - file.pubMagicMetadata, - file.key, - ); - const updateResult = await updateFilePublicMagicMetadata([ - { file, updatedPublicMagicMetadata }, - ]); - return updateResult[0]; -} - -export async function changeCaption( - file: EnteFile, - caption: string, -): Promise { - const updatedPublicMagicMetadataProps: FilePublicMagicMetadataProps = { - caption, - }; - - const updatedPublicMagicMetadata: FilePublicMagicMetadata = - await updateMagicMetadata( - updatedPublicMagicMetadataProps, - file.pubMagicMetadata, - file.key, - ); - const updateResult = await updateFilePublicMagicMetadata([ - { file, updatedPublicMagicMetadata }, - ]); - return updateResult[0]; -} - export function isSharedFile(user: User, file: EnteFile) { if (!user?.id || !file?.ownerID) { return false; @@ -169,14 +123,6 @@ export function isSharedFile(user: User, file: EnteFile) { return file.ownerID !== user.id; } -export function updateExistingFilePubMetadata( - existingFile: EnteFile, - updatedFile: EnteFile, -) { - existingFile.pubMagicMetadata = updatedFile.pubMagicMetadata; - existingFile.metadata = mergeMetadata([existingFile])[0].metadata; -} - export async function getFileFromURL(fileURL: string, name: string) { const fileBlob = await (await fetch(fileURL)).blob(); const fileFile = new File([fileBlob], name); diff --git a/web/apps/photos/src/utils/photoFrame/index.ts b/web/apps/photos/src/utils/photoFrame/index.ts index aff8c83512..6965b535bf 100644 --- a/web/apps/photos/src/utils/photoFrame/index.ts +++ b/web/apps/photos/src/utils/photoFrame/index.ts @@ -1,16 +1,18 @@ import type { SelectionContext } from "@/new/photos/components/gallery"; import type { GalleryBarMode } from "@/new/photos/components/gallery/reducer"; -import { SetSelectedState } from "types/gallery"; +import { SetSelectedState, type SelectedState } from "types/gallery"; +// TODO: All this is unnecessarily complex, and needs reworking. export const handleSelectCreator = ( setSelected: SetSelectedState, mode: GalleryBarMode | undefined, + userID: number | undefined, activeCollectionID: number, activePersonID: string | undefined, setRangeStart?, ) => - (id: number, isOwnFile: boolean, index?: number) => + ({ id, ownerID }: { id: number; ownerID: number }, index?: number) => (checked: boolean) => { if (typeof index !== "undefined") { if (checked) { @@ -19,84 +21,13 @@ export const handleSelectCreator = setRangeStart(undefined); } } - setSelected((selected) => { - if (!mode) { - // Retain older behavior for non-gallery call sites. - if (selected.collectionID !== activeCollectionID) { - selected = { - ownCount: 0, - count: 0, - collectionID: 0, - context: undefined, - }; - } - } else if (!selected.context) { - // Gallery will specify a mode, but a fresh selection starts off - // without a context, so fill it in with the current context. - selected = { - ...selected, - context: - mode == "people" - ? { mode, personID: activePersonID! } - : { - mode, - collectionID: activeCollectionID!, - }, - }; - } else { - // Both mode and context are defined. - if (selected.context.mode != mode) { - // Clear selection if mode has changed. - selected = { - ownCount: 0, - count: 0, - collectionID: 0, - context: - mode == "people" - ? { mode, personID: activePersonID! } - : { - mode, - collectionID: activeCollectionID!, - }, - }; - } else { - if (selected.context?.mode == "people") { - if (selected.context.personID != activePersonID) { - // Clear selection if person has changed. - selected = { - ownCount: 0, - count: 0, - collectionID: 0, - context: { - mode: selected.context?.mode, - personID: activePersonID!, - }, - }; - } - } else { - if ( - selected.context.collectionID != activeCollectionID - ) { - // Clear selection if collection has changed. - selected = { - ownCount: 0, - count: 0, - collectionID: 0, - context: { - mode: selected.context?.mode, - collectionID: activeCollectionID!, - }, - }; - } - } - } - } - - const newContext: SelectionContext | undefined = !mode - ? undefined - : mode == "people" - ? { mode, personID: activePersonID! } - : { mode, collectionID: activeCollectionID! }; + setSelected((_selected) => { + const { selected, newContext } = createSelectedAndContext( + mode, + activeCollectionID, + activePersonID, + _selected, + ); const handleCounterChange = (count: number) => { if (selected[id] === checked) { @@ -110,7 +41,7 @@ export const handleSelectCreator = }; const handleAllCounterChange = () => { - if (isOwnFile) { + if (ownerID === userID) { return { ownCount: handleCounterChange(selected.ownCount), count: handleCounterChange(selected.count), @@ -130,3 +61,139 @@ export const handleSelectCreator = }; }); }; + +export const handleSelectCreatorMulti = + ( + setSelected: SetSelectedState, + mode: GalleryBarMode | undefined, + userID: number | undefined, + activeCollectionID: number, + activePersonID: string | undefined, + ) => + (files: { id: number; ownerID: number }[]) => + (checked: boolean) => { + setSelected((_selected) => { + const { selected, newContext } = createSelectedAndContext( + mode, + activeCollectionID, + activePersonID, + _selected, + ); + + const newSelected = { ...selected }; + let newCount = selected.count; + let newOwnCount = selected.ownCount; + + if (checked) { + for (const file of files) { + if (!newSelected[file.id]) { + newSelected[file.id] = true; + newCount++; + if (file.ownerID === userID) newOwnCount++; + } + } + } else { + for (const file of files) { + if (newSelected[file.id]) { + newSelected[file.id] = false; + newCount--; + if (file.ownerID === userID) newOwnCount--; + } + } + } + + return { + ...newSelected, + count: newCount, + ownCount: newOwnCount, + collectionID: activeCollectionID, + context: newContext, + }; + }); + }; + +const createSelectedAndContext = ( + mode: GalleryBarMode | undefined, + + activeCollectionID: number, + activePersonID: string | undefined, + selected: SelectedState, +) => { + if (!mode) { + // Retain older behavior for non-gallery call sites. + if (selected.collectionID !== activeCollectionID) { + selected = { + ownCount: 0, + count: 0, + collectionID: 0, + context: undefined, + }; + } + } else if (!selected.context) { + // Gallery will specify a mode, but a fresh selection starts off + // without a context, so fill it in with the current context. + selected = { + ...selected, + context: + mode == "people" + ? { mode, personID: activePersonID! } + : { + mode, + collectionID: activeCollectionID!, + }, + }; + } else { + // Both mode and context are defined. + if (selected.context.mode != mode) { + // Clear selection if mode has changed. + selected = { + ownCount: 0, + count: 0, + collectionID: 0, + context: + mode == "people" + ? { mode, personID: activePersonID! } + : { + mode, + collectionID: activeCollectionID!, + }, + }; + } else { + if (selected.context?.mode == "people") { + if (selected.context.personID != activePersonID) { + // Clear selection if person has changed. + selected = { + ownCount: 0, + count: 0, + collectionID: 0, + context: { + mode: selected.context?.mode, + personID: activePersonID!, + }, + }; + } + } else { + if (selected.context.collectionID != activeCollectionID) { + // Clear selection if collection has changed. + selected = { + ownCount: 0, + count: 0, + collectionID: 0, + context: { + mode: selected.context?.mode, + collectionID: activeCollectionID!, + }, + }; + } + } + } + } + + const newContext: SelectionContext | undefined = !mode + ? undefined + : mode == "people" + ? { mode, personID: activePersonID! } + : { mode, collectionID: activeCollectionID! }; + + return { selected, newContext }; +}; diff --git a/web/apps/photos/tsconfig.json b/web/apps/photos/tsconfig.json index 2866412ee5..b56ff75f1b 100644 --- a/web/apps/photos/tsconfig.json +++ b/web/apps/photos/tsconfig.json @@ -3,12 +3,9 @@ "compilerOptions": { /* Set the base directory from which to resolve bare module names. */ "baseUrl": "./src", - /* MUI doesn't work with exactOptionalPropertyTypes yet. */ - "exactOptionalPropertyTypes": false, - /* Override tsconfig-next.json (TODO: Remove me) */ + /* Override tsconfig-next.json (TODO: Remove all of us) */ "verbatimModuleSyntax": false, - /* Override tsconfig-next.json (TODO: Remove me) */ "noImplicitAny": false, "noUnusedLocals": false, "noUnusedParameters": false, diff --git a/web/docs/dev.md b/web/docs/dev.md index a719c678f1..7ab1ef22c0 100644 --- a/web/docs/dev.md +++ b/web/docs/dev.md @@ -34,14 +34,6 @@ that can be then deployed to any web server. There is one `yarn build:foo` for each app, e.g. `yarn build:auth`. The output will be placed in `apps//out`, e.g. `apps/auth/out`. -### yarn preview:\* - -Build a production export and start a local web server to serve it. This uses -Python's built in web server, and is okay for quick testing but should not be -used in production. - -The ports are the same as that for `yarn dev:*` - ### lint, lint-fix Use `yarn lint` to check that your code formatting is as expected, and that diff --git a/web/package.json b/web/package.json index 35c30fbe31..7a8d9fd00b 100644 --- a/web/package.json +++ b/web/package.json @@ -21,13 +21,7 @@ "dev:payments": "yarn workspace payments dev", "dev:photos": "yarn workspace photos next dev -p 3000", "lint": "concurrently --names 'prettier,eslint,tsc' \"yarn prettier --check --log-level warn .\" \"yarn workspaces run eslint\" \"yarn workspaces run tsc\"", - "lint-fix": "concurrently --names 'prettier,eslint,tsc' \"yarn prettier --write --log-level warn .\" \"yarn workspaces run eslint --fix\" \"yarn workspaces run tsc\"", - "preview": "yarn preview:photos", - "preview:accounts": "yarn build:accounts && python3 -m http.server -d apps/accounts/out 3001", - "preview:auth": "yarn build:auth && python3 -m http.server -d apps/auth/out 3000", - "preview:cast": "yarn build:cast && python3 -m http.server -d apps/accounts/out 3001", - "preview:payments": "yarn workspace payments preview", - "preview:photos": "yarn build:photos && python3 -m http.server -d apps/photos/out 3000" + "lint-fix": "concurrently --names 'prettier,eslint,tsc' \"yarn prettier --write --log-level warn .\" \"yarn workspaces run eslint --fix\" \"yarn workspaces run tsc\"" }, "devDependencies": { "concurrently": "^9.1.1", diff --git a/web/packages/accounts/components/RecoveryKey.tsx b/web/packages/accounts/components/RecoveryKey.tsx index 67f69f1675..94deda54f0 100644 --- a/web/packages/accounts/components/RecoveryKey.tsx +++ b/web/packages/accounts/components/RecoveryKey.tsx @@ -1,12 +1,12 @@ import { type MiniDialogAttributes } from "@/base/components/MiniDialog"; import { SpacedRow } from "@/base/components/containers"; +import { DialogCloseIconButton } from "@/base/components/mui/DialogCloseIconButton"; import { FocusVisibleButton } from "@/base/components/mui/FocusVisibleButton"; import { errorDialogAttributes } from "@/base/components/utils/dialog"; import { useIsSmallWidth } from "@/base/components/utils/hooks"; import type { ModalVisibilityProps } from "@/base/components/utils/modal"; import log from "@/base/log"; import { downloadString } from "@/base/utils/web"; -import { DialogCloseIconButton } from "@/new/photos/components/mui/Dialog"; import { getRecoveryKey } from "@ente/shared/crypto/helpers"; import { Dialog, diff --git a/web/packages/accounts/components/Verify2FACodeForm.tsx b/web/packages/accounts/components/Verify2FACodeForm.tsx new file mode 100644 index 0000000000..b52ab6a3cb --- /dev/null +++ b/web/packages/accounts/components/Verify2FACodeForm.tsx @@ -0,0 +1,127 @@ +import { LoadingButton } from "@/base/components/mui/LoadingButton"; +import { isHTTP401Error } from "@/base/http"; +import log from "@/base/log"; +import { Stack, styled, Typography } from "@mui/material"; +import { useFormik } from "formik"; +import { t } from "i18next"; +import React, { useEffect, useState } from "react"; +import OtpInput from "react-otp-input"; + +interface Verify2FACodeFormProps { + /** + * Called when the user submits the OTP. + * + * The submission can happen in two ways: + * 1. The fill-in all the required 6 digits, or + * 2. They press the "Enable" button. + * + * The form will stay in a submitting state until this callback returns. + * + * @param otp The OTP that the user entered. + */ + onSubmit: (otp: string) => Promise; + /** + * The label for the submit button. + */ + submitButtonText: string; +} + +/** + * A form that can be used to ask the user to fill in a 6 digit OTP that their + * authenticator app is providing them with. + */ +export const Verify2FACodeForm: React.FC = ({ + onSubmit, + submitButtonText, +}) => { + const [shouldAutoFocus, setShouldAutoFocus] = useState(true); + + const { + values, + errors, + handleChange, + handleSubmit, + submitForm, + isSubmitting, + } = useFormik<{ otp: string }>({ + initialValues: { otp: "" }, + validateOnBlur: false, + validateOnChange: false, + onSubmit: async ({ otp }, { setFieldError, resetForm }) => { + try { + await onSubmit(otp); + resetForm(); // Prevent resubmission via the useEffect. + } catch (e) { + log.error("Failed to submit 2FA code", e); + resetForm(); + setFieldError( + "otp", + isHTTP401Error(e) + ? t("incorrect_code") + : t("generic_error"), + ); + // Workaround (toggling shouldAutoFocus) to reset the focus back + // to the first input field in case of errors. + // https://github.com/devfolioco/react-otp-input/issues/420 + setShouldAutoFocus(false); + setTimeout(() => setShouldAutoFocus(true), 100); + } + }, + }); + + useEffect(() => { + if (values.otp.length == 6 && !isSubmitting) void submitForm(); + }, [values, isSubmitting, submitForm]); + + return ( +
+ + + {t("enter_two_factor_otp")} + + -} + renderInput={(props) => } + /> + {errors.otp && ( + + {errors.otp} + + )} + + {submitButtonText} + + +
+ ); +}; + +const IndividualInput = styled("input")( + ({ theme }) => ` + font-size: 1.5rem; + padding: 4px; + width: 40px !important; + aspect-ratio: 1; + margin-inline: 6px; + border: 1px solid ${theme.vars.palette.accent.main}; + border-radius: 1px; + outline-color: ${theme.vars.palette.accent.light}; + transition: 0.5s; + ${theme.breakpoints.down("sm")} { + font-size: 1rem; + padding: 4px; + width: 32px !important; + } +`, +); diff --git a/web/packages/accounts/components/two-factor/TwoFactorSetup.tsx b/web/packages/accounts/components/two-factor/TwoFactorSetup.tsx deleted file mode 100644 index 1732cf57d7..0000000000 --- a/web/packages/accounts/components/two-factor/TwoFactorSetup.tsx +++ /dev/null @@ -1,105 +0,0 @@ -import { CodeBlock } from "@/accounts/components/CodeBlock"; -import { LinkButton } from "@/base/components/LinkButton"; -import { ActivityIndicator } from "@/base/components/mui/ActivityIndicator"; -import { VerticallyCentered } from "@ente/shared/components/Container"; -import { Stack, styled, Typography } from "@mui/material"; -import { t } from "i18next"; -import { useState } from "react"; -import { type SetupMode } from "../../pages/two-factor/setup"; -import type { TwoFactorSecret } from "../../services/user"; - -interface TwoFactorSetupProps { - twoFactorSecret?: TwoFactorSecret; -} - -export function TwoFactorSetup({ twoFactorSecret }: TwoFactorSetupProps) { - const [setupMode, setSetupMode] = useState("qrCode"); - - const changeToManualMode = () => setSetupMode("manualCode"); - - const changeToQRMode = () => setSetupMode("qrCode"); - - return ( - - {setupMode == "qrCode" ? ( - - ) : ( - - )} - - ); -} - -interface SetupManualModeProps { - twoFactorSecret?: TwoFactorSecret; - changeToQRMode: () => void; -} -function SetupManualMode({ - twoFactorSecret, - changeToQRMode, -}: SetupManualModeProps) { - return ( - - - {t("two_factor_manual_entry_message")} - - - - {t("scan_qr_title")} - - - ); -} - -interface SetupQRModeProps { - twoFactorSecret?: TwoFactorSecret; - changeToManualMode: () => void; -} - -function SetupQRMode({ - twoFactorSecret, - changeToManualMode, -}: SetupQRModeProps) { - return ( - <> - - {t("two_factor_qr_help")} - - {!twoFactorSecret ? ( - - - - ) : ( - - )} - - {t("two_factor_manual_entry_title")} - - - ); -} - -const QRCode = styled("img")( - ({ theme }) => ` - height: 200px; - width: 200px; - margin: ${theme.spacing(2)}; -`, -); - -const LoadingQRCode = styled(VerticallyCentered)( - ({ theme }) => ` - width: 200px; - aspect-ratio:1; - border: 1px solid ${theme.vars.palette.stroke.muted}; - margin: ${theme.spacing(2)}; - `, -); diff --git a/web/packages/accounts/components/two-factor/VerifyTwoFactor.tsx b/web/packages/accounts/components/two-factor/VerifyTwoFactor.tsx deleted file mode 100644 index 9f37cc52a3..0000000000 --- a/web/packages/accounts/components/two-factor/VerifyTwoFactor.tsx +++ /dev/null @@ -1,140 +0,0 @@ -import { LoadingButton } from "@/base/components/mui/LoadingButton"; -import { - CenteredFlex, - VerticallyCentered, -} from "@ente/shared/components/Container"; -import { Box, Typography, styled } from "@mui/material"; -import { Formik, type FormikHelpers } from "formik"; -import { t } from "i18next"; -import React, { useState } from "react"; -import OtpInput from "react-otp-input"; - -interface formValues { - otp: string; -} -interface Props { - onSubmit: VerifyTwoFactorCallback; - buttonText: string; -} - -export type VerifyTwoFactorCallback = ( - otp: string, - markSuccessful: () => void, -) => Promise; - -export function VerifyTwoFactor(props: Props) { - const [waiting, setWaiting] = useState(false); - const [shouldAutoFocus, setShouldAutoFocus] = useState(true); - - const markSuccessful = () => setWaiting(false); - - const submitForm = async ( - { otp }: formValues, - { setFieldError, resetForm }: FormikHelpers, - ) => { - try { - setWaiting(true); - await props.onSubmit(otp, markSuccessful); - } catch (e) { - resetForm(); - const message = e instanceof Error ? e.message : ""; - setFieldError("otp", `${t("generic_error_retry")} ${message}`); - // Workaround (toggling shouldAutoFocus) to reset the focus back to - // the first input field in case of errors. - // https://github.com/devfolioco/react-otp-input/issues/420 - setShouldAutoFocus(false); - setTimeout(() => setShouldAutoFocus(true), 100); - } - setWaiting(false); - }; - - const onChange = - // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type - (callback: Function, triggerSubmit: Function) => (otp: string) => { - callback(otp); - if (otp.length === 6) { - triggerSubmit(otp); - } - }; - return ( - - initialValues={{ otp: "" }} - validateOnChange={false} - validateOnBlur={false} - onSubmit={submitForm} - > - {({ values, errors, handleChange, handleSubmit, submitForm }) => ( - -
- - {t("enter_two_factor_otp")} - - - -} - renderInput={(props) => ( - - )} - /> - {errors.otp && ( - - - {t("incorrect_code")} - - - )} - - - {props.buttonText} - -
-
- )} - - ); -} - -const IndividualInput = styled("input")( - ({ theme }) => ` - font-size: 1.5rem; - padding: 4px; - width: 40px !important; - aspect-ratio: 1; - margin-inline: 6px; - border: 1px solid ${theme.vars.palette.accent.main}; - border-radius: 1px; - outline-color: ${theme.vars.palette.accent.light}; - transition: 0.5s; - ${theme.breakpoints.down("sm")} { - font-size: 1rem; - padding: 4px; - width: 32px !important; - } -`, -); - -const InvalidInputMessage: React.FC = ({ - children, -}) => ( - - {children} - -); diff --git a/web/packages/accounts/components/utils/dialog.ts b/web/packages/accounts/components/utils/dialog.ts index c17c171219..75036947a2 100644 --- a/web/packages/accounts/components/utils/dialog.ts +++ b/web/packages/accounts/components/utils/dialog.ts @@ -16,6 +16,7 @@ export const sessionExpiredDialogAttributes = ( title: t("session_expired"), message: t("session_expired_message"), nonClosable: true, + nonReplaceable: true, continue: { text: t("login"), action: onLogin, diff --git a/web/packages/accounts/components/utils/second-factor-choice.ts b/web/packages/accounts/components/utils/second-factor-choice.ts index 6694568b48..d31fc9a6a5 100644 --- a/web/packages/accounts/components/utils/second-factor-choice.ts +++ b/web/packages/accounts/components/utils/second-factor-choice.ts @@ -51,10 +51,10 @@ export const useSecondFactorChoiceIfNeeded = () => { // ID will be in a V2 attribute during a transient migration period. // // Note the use of || instead of ?? since _twoFactorSessionIDV1 will - // be an empty string, not undefined, if it is unset. We might need - // to add a `xxx-eslint-disable - // @typescript-eslint/prefer-nullish-coalescing` here too later. + // be an empty string, not undefined, if it is unset. This is + // intentional, so disable the eslint rule too. const _twoFactorSessionID = + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing _twoFactorSessionIDV1 || _twoFactorSessionIDV2; let passkeySessionID: string | undefined; diff --git a/web/packages/accounts/components/utils/use-redirect.ts b/web/packages/accounts/components/utils/use-redirect.ts index 981debc5bd..6307dc439e 100644 --- a/web/packages/accounts/components/utils/use-redirect.ts +++ b/web/packages/accounts/components/utils/use-redirect.ts @@ -1,4 +1,4 @@ -import { haveCredentialsInSession } from "@/base/session-store"; +import { haveCredentialsInSession } from "@/base/session"; import { useRouter } from "next/router"; import { useEffect } from "react"; import { stashRedirect } from "../../services/redirect"; diff --git a/web/packages/accounts/eslint.config.mjs b/web/packages/accounts/eslint.config.mjs index 447b0f36d4..c2a6fbb566 100644 --- a/web/packages/accounts/eslint.config.mjs +++ b/web/packages/accounts/eslint.config.mjs @@ -4,11 +4,7 @@ export default [ ...config, { rules: { - /* TODO: - * "This rule requires the `strictNullChecks` compiler option to be - * turned on to function correctly" - */ - "@typescript-eslint/prefer-nullish-coalescing": "off", + /* TODO: */ "@typescript-eslint/no-unnecessary-condition": "off", "@typescript-eslint/no-unsafe-assignment": "off", "@typescript-eslint/no-explicit-any": "off", @@ -16,8 +12,6 @@ export default [ "@typescript-eslint/no-unsafe-member-access": "off", "@typescript-eslint/no-unsafe-argument": "off", "@typescript-eslint/no-unsafe-call": "off", - /** TODO: Disabled as we migrate, try to prune these again */ - "react-hooks/exhaustive-deps": "off", }, }, ]; diff --git a/web/packages/accounts/package.json b/web/packages/accounts/package.json index 8ab3abed3f..129ae6002a 100644 --- a/web/packages/accounts/package.json +++ b/web/packages/accounts/package.json @@ -6,6 +6,8 @@ "@/base": "*", "@ente/shared": "*", "@types/zxcvbn": "^4.4.5", + "bip39": "^3.0.4", + "fast-srp-hap": "^2.0.4", "react-otp-input": "^3.1.1", "uuid": "^11.0.5", "zxcvbn": "^4.4.2" diff --git a/web/packages/accounts/pages/change-email.tsx b/web/packages/accounts/pages/change-email.tsx index cbf6618f9e..cc965f02b7 100644 --- a/web/packages/accounts/pages/change-email.tsx +++ b/web/packages/accounts/pages/change-email.tsx @@ -27,7 +27,7 @@ const Page: React.FC = () => { if (!user?.token) { void router.push("/"); } - }, []); + }, [router]); return ( diff --git a/web/packages/accounts/pages/change-password.tsx b/web/packages/accounts/pages/change-password.tsx index d8cc197faa..e19d6aff7a 100644 --- a/web/packages/accounts/pages/change-password.tsx +++ b/web/packages/accounts/pages/change-password.tsx @@ -50,7 +50,7 @@ const Page: React.FC = () => { } else { setToken(user.token); } - }, []); + }, [router]); const onSubmit: SetPasswordFormProps["callback"] = async ( passphrase, diff --git a/web/packages/accounts/pages/credentials.tsx b/web/packages/accounts/pages/credentials.tsx index 4b92b116ad..db8cd75a7a 100644 --- a/web/packages/accounts/pages/credentials.tsx +++ b/web/packages/accounts/pages/credentials.tsx @@ -25,9 +25,9 @@ import { } from "@/accounts/services/srp"; import type { SRPAttributes } from "@/accounts/services/srp-remote"; import { getSRPAttributes } from "@/accounts/services/srp-remote"; -import type { PageProps } from "@/accounts/types/page"; import { LinkButton } from "@/base/components/LinkButton"; import { LoadingIndicator } from "@/base/components/loaders"; +import { useBaseContext } from "@/base/context"; import { sharedCryptoWorker } from "@/base/crypto"; import type { B64EncryptionResult } from "@/base/crypto/libsodium"; import { clearLocalStorage } from "@/base/local-storage"; @@ -64,8 +64,8 @@ import { t } from "i18next"; import { useRouter } from "next/router"; import { useCallback, useEffect, useState } from "react"; -const Page: React.FC = ({ appContext }) => { - const { logout, showMiniDialog } = appContext; +const Page: React.FC = () => { + const { logout, showMiniDialog } = useBaseContext(); const [srpAttributes, setSrpAttributes] = useState(); const [keyAttributes, setKeyAttributes] = useState(); @@ -117,7 +117,7 @@ const Page: React.FC = ({ appContext }) => { // potentially transient issues. log.warn("Ignoring error when determining session validity", e); } - }, [showMiniDialog, logout]); + }, [logout, showMiniDialog]); useEffect(() => { const main = async () => { @@ -199,9 +199,10 @@ const Page: React.FC = ({ appContext }) => { } }; void main(); + // TODO: validateSession is a dependency, but add that only after we've + // wrapped items from the callback (like logout) in useCallback too. + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - // TODO: ^ validateSession is a dependency, but add that only after we've - // wrapped items from the callback (like logout) in useCallback too. const getKeyAttributes: VerifyMasterPasswordFormProps["getKeyAttributes"] = async (kek: string) => { diff --git a/web/packages/accounts/pages/generate.tsx b/web/packages/accounts/pages/generate.tsx index 17056805d8..8010e7e378 100644 --- a/web/packages/accounts/pages/generate.tsx +++ b/web/packages/accounts/pages/generate.tsx @@ -14,9 +14,9 @@ import { generateKeyAndSRPAttributes, } from "@/accounts/services/srp"; import { putAttributes } from "@/accounts/services/user"; -import type { PageProps } from "@/accounts/types/page"; import { LinkButton } from "@/base/components/LinkButton"; import { LoadingIndicator } from "@/base/components/loaders"; +import { useBaseContext } from "@/base/context"; import log from "@/base/log"; import { generateAndSaveIntermediateKeyAttributes, @@ -33,8 +33,8 @@ import { t } from "i18next"; import { useRouter } from "next/router"; import { useEffect, useState } from "react"; -const Page: React.FC = ({ appContext }) => { - const { logout, showMiniDialog } = appContext; +const Page: React.FC = () => { + const { logout, showMiniDialog } = useBaseContext(); const [token, setToken] = useState(); const [user, setUser] = useState(); @@ -65,7 +65,7 @@ const Page: React.FC = ({ appContext }) => { setToken(user.token); setLoading(false); } - }, []); + }, [router]); const onSubmit: SetPasswordFormProps["callback"] = async ( passphrase, diff --git a/web/packages/accounts/pages/login.tsx b/web/packages/accounts/pages/login.tsx index 5e26b9cd2a..a4927b61c9 100644 --- a/web/packages/accounts/pages/login.tsx +++ b/web/packages/accounts/pages/login.tsx @@ -20,7 +20,7 @@ const Page: React.FC = () => { void router.push(PAGES.VERIFY); } setLoading(false); - }, []); + }, [router]); const onSignUp = () => void router.push(PAGES.SIGNUP); diff --git a/web/packages/accounts/pages/passkeys/finish.tsx b/web/packages/accounts/pages/passkeys/finish.tsx index 1ef91aaad8..258d062b94 100644 --- a/web/packages/accounts/pages/passkeys/finish.tsx +++ b/web/packages/accounts/pages/passkeys/finish.tsx @@ -33,7 +33,7 @@ const Page: React.FC = () => { void saveCredentialsAndNavigateTo(passkeySessionID, response).then( (slug: string) => router.push(slug), ); - }, []); + }, [router]); return ; }; diff --git a/web/packages/accounts/pages/passkeys/recover.tsx b/web/packages/accounts/pages/passkeys/recover.tsx index d36df9c557..6d29d53c6a 100644 --- a/web/packages/accounts/pages/passkeys/recover.tsx +++ b/web/packages/accounts/pages/passkeys/recover.tsx @@ -1,9 +1,5 @@ -import React from "react"; -import type { PageProps } from "../../types/page"; -import TwoFactorRecoverPage from "../two-factor/recover"; +import Page_ from "../two-factor/recover"; -const Page: React.FC = ({ appContext }) => ( - -); +const Page = () => ; export default Page; diff --git a/web/packages/accounts/pages/recover.tsx b/web/packages/accounts/pages/recover.tsx index 9c5c2a7540..7c69ea44a6 100644 --- a/web/packages/accounts/pages/recover.tsx +++ b/web/packages/accounts/pages/recover.tsx @@ -6,8 +6,8 @@ import { import { PAGES } from "@/accounts/constants/pages"; import { appHomeRoute, stashRedirect } from "@/accounts/services/redirect"; import { sendOTT } from "@/accounts/services/user"; -import type { PageProps } from "@/accounts/types/page"; import { LinkButton } from "@/base/components/LinkButton"; +import { useBaseContext } from "@/base/context"; import { sharedCryptoWorker } from "@/base/crypto"; import log from "@/base/log"; import SingleInputForm, { @@ -29,8 +29,8 @@ const bip39 = require("bip39"); // mobile client library only supports english. bip39.setDefaultWordlist("english"); -const Page: React.FC = ({ appContext }) => { - const { showMiniDialog } = appContext; +const Page: React.FC = () => { + const { showMiniDialog } = useBaseContext(); const [keyAttributes, setKeyAttributes] = useState< KeyAttributes | undefined @@ -59,7 +59,7 @@ const Page: React.FC = ({ appContext }) => { } else { setKeyAttributes(keyAttributes); } - }, []); + }, [router]); const recover: SingleInputFormProps["callback"] = async ( recoveryKey: string, diff --git a/web/packages/accounts/pages/signup.tsx b/web/packages/accounts/pages/signup.tsx index 89033339b5..4920053c24 100644 --- a/web/packages/accounts/pages/signup.tsx +++ b/web/packages/accounts/pages/signup.tsx @@ -20,7 +20,7 @@ const Page: React.FC = () => { void router.push(PAGES.VERIFY); } setLoading(false); - }, []); + }, [router]); const onLogin = () => void router.push(PAGES.LOGIN); diff --git a/web/packages/accounts/pages/two-factor/recover.tsx b/web/packages/accounts/pages/two-factor/recover.tsx index 071c713623..274042ecdd 100644 --- a/web/packages/accounts/pages/two-factor/recover.tsx +++ b/web/packages/accounts/pages/two-factor/recover.tsx @@ -9,9 +9,9 @@ import { removeTwoFactor, type TwoFactorType, } from "@/accounts/services/user"; -import type { AccountsContextT } from "@/accounts/types/context"; import { LinkButton } from "@/base/components/LinkButton"; import type { MiniDialogAttributes } from "@/base/components/MiniDialog"; +import { useBaseContext } from "@/base/context"; import { sharedCryptoWorker } from "@/base/crypto"; import type { B64EncryptionResult } from "@/base/crypto/libsodium"; import log from "@/base/log"; @@ -38,12 +38,11 @@ const bip39 = require("bip39"); bip39.setDefaultWordlist("english"); export interface RecoverPageProps { - appContext: AccountsContextT; twoFactorType: TwoFactorType; } -const Page: React.FC = ({ appContext, twoFactorType }) => { - const { showMiniDialog, logout } = appContext; +const Page: React.FC = ({ twoFactorType }) => { + const { logout, showMiniDialog } = useBaseContext(); const [encryptedTwoFactorSecret, setEncryptedTwoFactorSecret] = useState | null>(null); @@ -93,6 +92,8 @@ const Page: React.FC = ({ appContext, twoFactorType }) => { } }; void main(); + // TODO: + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const recover: SingleInputFormProps["callback"] = async ( diff --git a/web/packages/accounts/pages/two-factor/setup.tsx b/web/packages/accounts/pages/two-factor/setup.tsx index fe23e30b8d..c12495d175 100644 --- a/web/packages/accounts/pages/two-factor/setup.tsx +++ b/web/packages/accounts/pages/two-factor/setup.tsx @@ -1,22 +1,18 @@ -import { - VerifyTwoFactor, - type VerifyTwoFactorCallback, -} from "@/accounts/components/two-factor/VerifyTwoFactor"; +import { CodeBlock } from "@/accounts/components/CodeBlock"; +import { Verify2FACodeForm } from "@/accounts/components/Verify2FACodeForm"; import { appHomeRoute } from "@/accounts/services/redirect"; import type { TwoFactorSecret } from "@/accounts/services/user"; import { enableTwoFactor, setupTwoFactor } from "@/accounts/services/user"; import { CenteredFill } from "@/base/components/containers"; +import { LinkButton } from "@/base/components/LinkButton"; +import { ActivityIndicator } from "@/base/components/mui/ActivityIndicator"; import { FocusVisibleButton } from "@/base/components/mui/FocusVisibleButton"; -import log from "@/base/log"; import { encryptWithRecoveryKey } from "@ente/shared/crypto/helpers"; import { getData, LS_KEYS, setLSUser } from "@ente/shared/storage/localStorage"; import { Paper, Stack, styled, Typography } from "@mui/material"; import { t } from "i18next"; import { useRouter } from "next/router"; -import { useEffect, useState } from "react"; -import { TwoFactorSetup } from "../../components/two-factor/TwoFactorSetup"; - -export type SetupMode = "qrCode" | "manualCode"; +import React, { useEffect, useState } from "react"; const Page: React.FC = () => { const [twoFactorSecret, setTwoFactorSecret] = useState< @@ -26,34 +22,24 @@ const Page: React.FC = () => { const router = useRouter(); useEffect(() => { - if (twoFactorSecret) { - return; - } - const main = async () => { - try { - const twoFactorSecret = await setupTwoFactor(); - setTwoFactorSecret(twoFactorSecret); - } catch (e) { - log.error("failed to get two factor setup code", e); - } - }; - void main(); + void setupTwoFactor().then(setTwoFactorSecret); }, []); - const onSubmit: VerifyTwoFactorCallback = async ( - otp: string, - markSuccessful, - ) => { - const recoveryEncryptedTwoFactorSecret = await encryptWithRecoveryKey( - twoFactorSecret!.secretCode, - ); - await enableTwoFactor(otp, recoveryEncryptedTwoFactorSecret); - markSuccessful(); + const handleSubmit = async (otp: string) => { + const { + encryptedData: encryptedTwoFactorSecret, + nonce: twoFactorSecretDecryptionNonce, + } = await encryptWithRecoveryKey(twoFactorSecret!.secretCode); + await enableTwoFactor({ + code: otp, + encryptedTwoFactorSecret, + twoFactorSecretDecryptionNonce, + }); await setLSUser({ ...getData(LS_KEYS.USER), isTwoFactorEnabled: true, }); - void router.push(appHomeRoute); + await router.push(appHomeRoute); }; return ( @@ -63,20 +49,18 @@ const Page: React.FC = () => { {t("two_factor")} - - - - - - {t("go_back")} - - + + + + + {t("go_back")} + @@ -84,6 +68,8 @@ const Page: React.FC = () => { ); }; +export default Page; + const ContentsPaper = styled(Paper)(({ theme }) => ({ marginBlock: theme.spacing(2), padding: theme.spacing(4, 2), @@ -94,4 +80,85 @@ const ContentsPaper = styled(Paper)(({ theme }) => ({ gap: theme.spacing(4), })); -export default Page; +interface InstructionsProps { + twoFactorSecret: TwoFactorSecret | undefined; +} + +const Instructions: React.FC = ({ twoFactorSecret }) => { + const [setupMode, setSetupMode] = useState<"qr" | "manual">("qr"); + + return ( + + {setupMode == "qr" ? ( + setSetupMode("manual")} + /> + ) : ( + setSetupMode("qr")} + /> + )} + + ); +}; + +interface SetupManualModeProps { + twoFactorSecret: TwoFactorSecret | undefined; + onChangeMode: () => void; +} + +const SetupManualMode: React.FC = ({ + twoFactorSecret, + onChangeMode, +}) => ( + <> + + {t("two_factor_manual_entry_message")} + + + {t("scan_qr_title")} + +); + +interface SetupQRModeProps { + twoFactorSecret?: TwoFactorSecret; + onChangeMode: () => void; +} + +const SetupQRMode: React.FC = ({ + twoFactorSecret, + onChangeMode, +}) => ( + <> + + {t("two_factor_qr_help")} + + {!twoFactorSecret ? ( + + + + ) : ( + + )} + + {t("two_factor_manual_entry_title")} + + +); + +const QRCode = styled("img")(` + width: 200px; + height: 200px; +`); + +const LoadingQRCode = styled(Stack)( + ({ theme }) => ` + width: 200px; + height: 200px; + border: 1px solid ${theme.vars.palette.stroke.muted}; + align-items: center; + justify-content: center; + `, +); diff --git a/web/packages/accounts/pages/two-factor/verify.tsx b/web/packages/accounts/pages/two-factor/verify.tsx index 796187424a..8434969600 100644 --- a/web/packages/accounts/pages/two-factor/verify.tsx +++ b/web/packages/accounts/pages/two-factor/verify.tsx @@ -1,7 +1,9 @@ +import { Verify2FACodeForm } from "@/accounts/components/Verify2FACodeForm"; import { PAGES } from "@/accounts/constants/pages"; import { verifyTwoFactor } from "@/accounts/services/user"; import { LinkButton } from "@/base/components/LinkButton"; -import { ApiError } from "@ente/shared/error"; +import { useBaseContext } from "@/base/context"; +import { HTTPError } from "@/base/http"; import { LS_KEYS, getData, @@ -9,7 +11,6 @@ import { setLSUser, } from "@ente/shared/storage/localStorage"; import type { User } from "@ente/shared/user/types"; -import { HttpStatusCode } from "axios"; import { t } from "i18next"; import { useRouter } from "next/router"; import { useEffect, useState } from "react"; @@ -18,15 +19,10 @@ import { AccountsPageFooter, AccountsPageTitle, } from "../../components/layouts/centered-paper"; -import { - VerifyTwoFactor, - type VerifyTwoFactorCallback, -} from "../../components/two-factor/VerifyTwoFactor"; import { unstashRedirect } from "../../services/redirect"; -import type { PageProps } from "../../types/page"; -const Page: React.FC = ({ appContext }) => { - const { logout } = appContext; +const Page: React.FC = () => { + const { logout } = useBaseContext(); const [sessionID, setSessionID] = useState(""); @@ -44,9 +40,9 @@ const Page: React.FC = ({ appContext }) => { } else { setSessionID(user.twoFactorSessionID); } - }, []); + }, [router]); - const onSubmit: VerifyTwoFactorCallback = async (otp) => { + const handleSubmit = async (otp: string) => { try { const resp = await verifyTwoFactor(otp, sessionID); const { keyAttributes, encryptedToken, token, id } = resp; @@ -57,13 +53,9 @@ const Page: React.FC = ({ appContext }) => { id, }); setData(LS_KEYS.KEY_ATTRIBUTES, keyAttributes!); - void router.push(unstashRedirect() ?? PAGES.CREDENTIALS); + await router.push(unstashRedirect() ?? PAGES.CREDENTIALS); } catch (e) { - if ( - e instanceof ApiError && - // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison - e.httpStatusCode === HttpStatusCode.NotFound - ) { + if (e instanceof HTTPError && e.res.status == 404) { logout(); } else { throw e; @@ -74,7 +66,10 @@ const Page: React.FC = ({ appContext }) => { return ( {t("two_factor")} - + router.push(PAGES.TWO_FACTOR_RECOVER)} diff --git a/web/packages/accounts/pages/verify.tsx b/web/packages/accounts/pages/verify.tsx index 83a00e63bb..12eef1ac20 100644 --- a/web/packages/accounts/pages/verify.tsx +++ b/web/packages/accounts/pages/verify.tsx @@ -19,9 +19,9 @@ import type { } from "@/accounts/services/srp-remote"; import { getSRPAttributes } from "@/accounts/services/srp-remote"; import { putAttributes, sendOTT, verifyEmail } from "@/accounts/services/user"; -import type { PageProps } from "@/accounts/types/page"; import { LinkButton } from "@/base/components/LinkButton"; import { LoadingIndicator } from "@/base/components/loaders"; +import { useBaseContext } from "@/base/context"; import log from "@/base/log"; import SingleInputForm, { type SingleInputFormProps, @@ -47,8 +47,8 @@ import { useRouter } from "next/router"; import { useEffect, useState } from "react"; import { Trans } from "react-i18next"; -const Page: React.FC = ({ appContext }) => { - const { logout, showMiniDialog } = appContext; +const Page: React.FC = () => { + const { logout, showMiniDialog } = useBaseContext(); const [email, setEmail] = useState(""); const [resend, setResend] = useState(0); @@ -74,7 +74,7 @@ const Page: React.FC = ({ appContext }) => { } }; void main(); - }, []); + }, [router]); const onSubmit: SingleInputFormProps["callback"] = async ( ott, diff --git a/web/packages/accounts/services/srp-remote.ts b/web/packages/accounts/services/srp-remote.ts index 1aad4ddb2a..4d860d2de3 100644 --- a/web/packages/accounts/services/srp-remote.ts +++ b/web/packages/accounts/services/srp-remote.ts @@ -1,3 +1,4 @@ +import { ensureOk, publicRequestHeaders } from "@/base/http"; import log from "@/base/log"; import { apiURL } from "@/base/origins"; import { ApiError, CustomError } from "@ente/shared/error"; @@ -131,19 +132,15 @@ export const completeSRPSetup = async ( }; export const createSRPSession = async (srpUserID: string, srpA: string) => { - try { - const resp = await HTTPService.post( - await apiURL("/users/srp/create-session"), - { - srpUserID, - srpA, - }, - ); - return resp.data as CreateSRPSessionResponse; - } catch (e) { - log.error("createSRPSession failed", e); - throw e; - } + const res = await fetch(await apiURL("/users/srp/create-session"), { + method: "POST", + headers: publicRequestHeaders(), + body: JSON.stringify({ srpUserID, srpA }), + }); + ensureOk(res); + const data = await res.json(); + // TODO: Use zod + return data as CreateSRPSessionResponse; }; export const verifySRPSession = async ( diff --git a/web/packages/accounts/services/user.ts b/web/packages/accounts/services/user.ts index 44bda1ed21..9034872d56 100644 --- a/web/packages/accounts/services/user.ts +++ b/web/packages/accounts/services/user.ts @@ -1,4 +1,3 @@ -import type { B64EncryptionResult } from "@/base/crypto/libsodium"; import { authenticatedRequestHeaders, ensureOk, @@ -39,10 +38,12 @@ export interface TwoFactorVerificationResponse { token?: string; } -export interface TwoFactorSecret { - secretCode: string; - qrCode: string; -} +const TwoFactorSecret = z.object({ + secretCode: z.string(), + qrCode: z.string(), +}); + +export type TwoFactorSecret = z.infer; export interface TwoFactorRecoveryResponse { encryptedSecret: string; @@ -235,14 +236,15 @@ export const remoteLogoutIfNeeded = async () => { }; export const verifyTwoFactor = async (code: string, sessionID: string) => { - const resp = await HTTPService.post( - await apiURL("/users/two-factor/verify"), - { - code, - sessionID, - }, - ); - return resp.data as UserVerificationResponse; + const res = await fetch(await apiURL("/users/two-factor/verify"), { + method: "POST", + headers: publicRequestHeaders(), + body: JSON.stringify({ code, sessionID }), + }); + ensureOk(res); + const json = await res.json(); + // TODO: Use zod here + return json as UserVerificationResponse; }; /** The type of the second factor we're trying to act on */ @@ -292,37 +294,37 @@ export const changeEmail = async (email: string, ott: string) => { ); }; +/** + * Start the two factor setup process by fetching a secret code (and the + * corresponding QR code) from remote. + */ export const setupTwoFactor = async () => { - const resp = await HTTPService.post( - await apiURL("/users/two-factor/setup"), - null, - undefined, - { - "X-Auth-Token": getToken(), - }, - ); - return resp.data as TwoFactorSecret; + const res = await fetch(await apiURL("/users/two-factor/setup"), { + method: "POST", + headers: await authenticatedRequestHeaders(), + }); + ensureOk(res); + return TwoFactorSecret.parse(await res.json()); }; -export const enableTwoFactor = async ( - code: string, - recoveryEncryptedTwoFactorSecret: B64EncryptionResult, -) => { - await HTTPService.post( - await apiURL("/users/two-factor/enable"), - { - code, - encryptedTwoFactorSecret: - recoveryEncryptedTwoFactorSecret.encryptedData, - twoFactorSecretDecryptionNonce: - recoveryEncryptedTwoFactorSecret.nonce, - }, - undefined, - { - "X-Auth-Token": getToken(), - }, +interface EnableTwoFactorRequest { + code: string; + encryptedTwoFactorSecret: string; + twoFactorSecretDecryptionNonce: string; +} + +/** + * Enable two factor for the user by providing the 2FA code and the encrypted + * secret from a previous call to {@link setupTwoFactor}. + */ +export const enableTwoFactor = async (req: EnableTwoFactorRequest) => + ensureOk( + await fetch(await apiURL("/users/two-factor/enable"), { + method: "POST", + headers: await authenticatedRequestHeaders(), + body: JSON.stringify(req), + }), ); -}; export const setRecoveryKey = async (token: string, recoveryKey: RecoveryKey) => HTTPService.put( diff --git a/web/packages/accounts/types/context.ts b/web/packages/accounts/types/context.ts deleted file mode 100644 index 12f9584802..0000000000 --- a/web/packages/accounts/types/context.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { MiniDialogAttributes } from "@/base/components/MiniDialog"; - -/** - * Properties expected to be present in the AppContext types for pages that - * defer to the pages provided by the accounts package. - */ -export interface AccountsContextT { - /** - * Perform the (possibly app specific) logout sequence. - */ - logout: () => void; - /** - * Show a "mini dialog" with the given attributes. - * - * Mini dialogs (see {@link AttributedMiniDialog}) are meant for simple - * confirmation or notications. Their appearance and functionality can be - * customized by providing appropriate {@link MiniDialogAttributes}. - */ - showMiniDialog: (attributes: MiniDialogAttributes) => void; -} diff --git a/web/packages/accounts/types/page.ts b/web/packages/accounts/types/page.ts deleted file mode 100644 index 92a5096562..0000000000 --- a/web/packages/accounts/types/page.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { AccountsContextT } from "./context"; - -/** - * The default type for pages exposed by this package. - * - * Some specific pages might extend this further (e.g. the two-factor/recover). - */ -export interface PageProps { - /** - * The common denominator AppContext. - * - * Within this package we do not have access to the context object declared - * with the app's code, so we need to take the context as a parameter. - */ - appContext: AccountsContextT; -} diff --git a/web/packages/base/components/Head.tsx b/web/packages/base/components/Head.tsx index 0373bd3aea..3ada4645a5 100644 --- a/web/packages/base/components/Head.tsx +++ b/web/packages/base/components/Head.tsx @@ -11,20 +11,15 @@ interface CustomHeadProps { * * This assumes the existence of `public/images/favicon.png`. */ -export const CustomHead: React.FC = ({ title }) => { - return ( - - {title} - - - - - - ); -}; +export const CustomHead: React.FC = ({ title }) => ( + + {title} + + + + + +); diff --git a/web/packages/base/components/MiniDialog.tsx b/web/packages/base/components/MiniDialog.tsx index c9ffd04692..b6b920126f 100644 --- a/web/packages/base/components/MiniDialog.tsx +++ b/web/packages/base/components/MiniDialog.tsx @@ -43,6 +43,22 @@ export interface MiniDialogAttributes { * actions. */ nonClosable?: boolean; + /** + * If `true`, then the dialog cannot be replaced by another dialog while it + * is being displayed. + * + * The app uses a single component to render mini dialogs, and it is + * possible that new dialog contents might preempt and replace contents in a + * dialog that is already being shown. Usually if such preemption is + * expected then both dialogs use differing mechanisms, but it can happen in + * rare unforeseen cases. + * + * This flag allow a dialog to indicate that it should not be preempted, as + * it contains some critical information. + * + * Use this flag sparingly. + */ + nonReplaceable?: boolean; /** * Customize the primary action button shown in the dialog. * diff --git a/web/packages/base/components/OverflowMenu.tsx b/web/packages/base/components/OverflowMenu.tsx index 99b7692b1f..8f61ba9cbf 100644 --- a/web/packages/base/components/OverflowMenu.tsx +++ b/web/packages/base/components/OverflowMenu.tsx @@ -59,7 +59,7 @@ export const OverflowMenu: React.FC< [], ); return ( - + setAnchorEl(event.currentTarget)} aria-controls={anchorEl ? ariaID : undefined} @@ -74,18 +74,20 @@ export const OverflowMenu: React.FC< {...(anchorEl ? { anchorEl } : {})} open={!!anchorEl} onClose={() => setAnchorEl(undefined)} - MenuListProps={{ - // Disable padding at the top and bottom of the menu list. - disablePadding: true, - "aria-labelledby": ariaID, + slotProps={{ + paper: { sx: menuPaperSxProps }, + list: { + // Disable padding at the top and bottom of the menu list. + disablePadding: true, + "aria-labelledby": ariaID, + }, }} - slotProps={{ paper: { sx: menuPaperSxProps } }} anchorOrigin={{ vertical: "bottom", horizontal: "right" }} transformOrigin={{ vertical: "top", horizontal: "right" }} > {children} - + ); }; diff --git a/web/packages/base/components/SingleInputDialog.tsx b/web/packages/base/components/SingleInputDialog.tsx index aa45ab65ad..6e9d42724c 100644 --- a/web/packages/base/components/SingleInputDialog.tsx +++ b/web/packages/base/components/SingleInputDialog.tsx @@ -13,8 +13,8 @@ type SingleInputDialogProps = ModalVisibilityProps & * A dialog that can be used to ask for a single text input using a * {@link SingleInputForm}. * - * If the submission handler provided to this component resolves successfully, - * then the dialog is closed. + * The dialog closes when the promise returned by the {@link onSubmit} callback + * fulfills. * * See also: {@link CollectionNamer}, its older sibling. */ diff --git a/web/packages/base/components/SingleInputForm.tsx b/web/packages/base/components/SingleInputForm.tsx index 5b97c50109..8ab4adbb60 100644 --- a/web/packages/base/components/SingleInputForm.tsx +++ b/web/packages/base/components/SingleInputForm.tsx @@ -8,7 +8,7 @@ import React from "react"; export type SingleInputFormProps = Pick< TextFieldProps, - "label" | "placeholder" | "autoComplete" | "autoFocus" + "label" | "placeholder" | "autoComplete" | "autoFocus" | "slotProps" > & { /** * The initial value, if any, to prefill in the input. @@ -43,9 +43,8 @@ export type SingleInputFormProps = Pick< * A TextField and two buttons. * * A common requirement is taking a single textual input from the user. This is - * a form suitable for that purpose - it is form containing a single MUI - * {@link TextField}, with two accompanying buttons; one to submit, and one to - * cancel. + * a form suitable for that purpose. It contains a single MUI {@link TextField} + * and two accompanying buttons; one to submit, and one to cancel. * * Submission is handled as an async function, during which the input is * disabled and a loading indicator is shown. Errors during submission are shown @@ -107,7 +106,7 @@ export const SingleInputForm: React.FC = ({ diff --git a/web/packages/new/photos/components/mui/Dialog.tsx b/web/packages/base/components/mui/DialogCloseIconButton.tsx similarity index 100% rename from web/packages/new/photos/components/mui/Dialog.tsx rename to web/packages/base/components/mui/DialogCloseIconButton.tsx diff --git a/web/packages/base/components/mui/SidebarDrawer.tsx b/web/packages/base/components/mui/SidebarDrawer.tsx index 6c96b47859..97f321e427 100644 --- a/web/packages/base/components/mui/SidebarDrawer.tsx +++ b/web/packages/base/components/mui/SidebarDrawer.tsx @@ -23,16 +23,23 @@ import React from "react"; * It also does some trickery with a sticky opaque bar to ensure that the * content scrolls below our inline title bar on desktop. */ -export const SidebarDrawer: React.FC = ({ children, ...rest }) => ( +export const SidebarDrawer: React.FC = ({ + slotProps, + children, + ...rest +}) => ( diff --git a/web/packages/base/components/utils/dialog.tsx b/web/packages/base/components/utils/dialog.tsx index 0232b6fba3..04cdc48711 100644 --- a/web/packages/base/components/utils/dialog.tsx +++ b/web/packages/base/components/utils/dialog.tsx @@ -15,10 +15,15 @@ export const useAttributedMiniDialog = () => { const [open, setOpen] = useState(false); - const showMiniDialog = useCallback((attributes: MiniDialogAttributes) => { - setAttributes(attributes); - setOpen(true); - }, []); + const showMiniDialog = useCallback( + (newAttributes: MiniDialogAttributes) => { + setAttributes((attributes) => + attributes?.nonReplaceable ? attributes : newAttributes, + ); + setOpen(true); + }, + [], + ); const handleClose = useCallback(() => setOpen(false), []); diff --git a/web/packages/base/components/utils/mui-theme.d.ts b/web/packages/base/components/utils/mui-theme.d.ts index e2d11c1756..6a82f67043 100644 --- a/web/packages/base/components/utils/mui-theme.d.ts +++ b/web/packages/base/components/utils/mui-theme.d.ts @@ -140,6 +140,15 @@ declare module "@mui/material/styles" { * The color of a switch when it is enabled. */ switchOn: string; + /** + * A subset of the dark mode colors, fixed so that they don't change + * even when the app is in light mode. + */ + dark: { + background: Omit; + text: Omit; + divider: string; + }; }; /** * MUI as of v6 does not allow customizing shadows easily. This is due diff --git a/web/packages/base/components/utils/theme.ts b/web/packages/base/components/utils/theme.ts index 1e511b4ac7..99cb3d86e8 100644 --- a/web/packages/base/components/utils/theme.ts +++ b/web/packages/base/components/utils/theme.ts @@ -41,7 +41,7 @@ const getTheme = (appName: AppName): Theme => { * * 2. These can be groups of color values that have roughly the same hue, but * different levels of saturation. Such hue groups are arranged together into - * a "Colors" exported by "@/mui/material": + * a "Colors" exported by "@mui/material": * * export interface Color { * 50: string; @@ -117,6 +117,16 @@ const getTheme = (appName: AppName): Theme => { */ const getColors = (appName: AppName) => ({ ..._colors, + ...{ + fixed: { + ..._colors.fixed, + dark: { + background: _colors.dark.background, + text: _colors.dark.text, + divider: _colors.dark.stroke.faint, + }, + }, + }, ...{ accent: appName == "auth" ? _colors.accentAuth : _colors.accentPhotos, }, @@ -541,10 +551,13 @@ const components: Components = { MuiDialog: { defaultProps: { + // [Note: Overzealous Chrome? Complicated ARIA?] + // // This is required to prevent console errors about aria-hiding a // focused button when the dialog is closed. // - // https://github.com/mui/material-ui/issues/43106#issuecomment-2314809028 + // - https://github.com/mui/material-ui/issues/43106#issuecomment-2314809028 + // - https://issues.chromium.org/issues/392121909 closeAfterTransition: false, }, styleOverrides: { diff --git a/web/packages/base/context.ts b/web/packages/base/context.ts new file mode 100644 index 0000000000..073202846a --- /dev/null +++ b/web/packages/base/context.ts @@ -0,0 +1,67 @@ +import type { MiniDialogAttributes } from "@/base/components/MiniDialog"; +import { createContext, useContext } from "react"; +import { genericErrorDialogAttributes } from "./components/utils/dialog"; +import log from "./log"; + +/** + * The type of the context expected to be present in the React tree for all apps + * that use the base package. + * + * It is usually provided by the _app.tsx for the corresponding app. + */ +export interface BaseContextT { + /** + * Perform the (possibly app specific) logout sequence. + */ + logout: () => void; + /** + * Show a "mini dialog" with the given attributes. + * + * Mini dialogs (see {@link AttributedMiniDialog}) are meant for simple + * confirmation or notications. Their appearance and functionality can be + * customized by providing appropriate {@link MiniDialogAttributes}. + */ + showMiniDialog: (attributes: MiniDialogAttributes) => void; + /** + * Log the given error and show a generic error {@link MiniDialog}. + */ + onGenericError: (error: unknown) => void; +} + +/** + * The React {@link Context} of type {@link BaseContextT} available to all React + * components that refer to the base package. + */ +export const BaseContext = createContext(undefined); + +/** + * Utility hook to get the required {@link BaseContextT} that is expected to be + * available to all React components that refer to the base package. + */ +export const useBaseContext = (): BaseContextT => useContext(BaseContext)!; + +/** + * A helper function to create a {@link BaseContext} by deriving derivable + * context values from the minimal subset. + * + * In simpler words, it automatically provides a definition of + * {@link onGenericError} using the given {@link showMiniDialog} prop. + */ +export const deriveBaseContext = ({ + logout, + showMiniDialog, +}: Omit): BaseContextT => { + const onGenericError = (e: unknown) => { + log.error(e); + // The generic error handler is sometimes called in the context of + // actions that were initiated by a confirmation dialog action handler + // themselves, then we need to let the current one close. + // + // See: [Note: Chained MiniDialogs] + setTimeout(() => { + showMiniDialog(genericErrorDialogAttributes()); + }, 0); + }; + + return { logout, showMiniDialog, onGenericError }; +}; diff --git a/web/packages/base/crypto/worker.ts b/web/packages/base/crypto/worker.ts index 693ade2be1..429451d27c 100644 --- a/web/packages/base/crypto/worker.ts +++ b/web/packages/base/crypto/worker.ts @@ -1,3 +1,4 @@ +import { logUnhandledErrorsAndRejectionsInWorker } from "@/base/log-web"; import { expose } from "comlink"; import type { StateAddress } from "libsodium-wrappers-sumo"; import * as ei from "./ente-impl"; @@ -98,3 +99,5 @@ export class CryptoWorker { } expose(CryptoWorker); + +logUnhandledErrorsAndRejectionsInWorker(); diff --git a/web/packages/base/date.ts b/web/packages/base/date.ts index f08084f79a..c44fc5b234 100644 --- a/web/packages/base/date.ts +++ b/web/packages/base/date.ts @@ -1,6 +1,8 @@ /** * Convert an epoch microsecond value to a JavaScript date. * + * [Note: Remote timestamps are epoch microseconds] + * * This is a convenience API for dealing with optional epoch microseconds in * various data structures. Remote talks in terms of epoch microseconds, but * JavaScript dates are underlain by epoch milliseconds, and this does a @@ -12,3 +14,11 @@ export const dateFromEpochMicroseconds = ( epochMicroseconds === undefined ? undefined : new Date(epochMicroseconds / 1000); + +/** + * Return `true` if both the given dates have the same day. + */ +export const isSameDay = (first: Date, second: Date) => + first.getFullYear() === second.getFullYear() && + first.getMonth() === second.getMonth() && + first.getDate() === second.getDate(); diff --git a/web/packages/base/http.ts b/web/packages/base/http.ts index 91f9b412dd..3eb98d9bae 100644 --- a/web/packages/base/http.ts +++ b/web/packages/base/http.ts @@ -72,6 +72,7 @@ export const authenticatedPublicAlbumsRequestHeaders = ({ */ export class HTTPError extends Error { res: Response; + details: Record; constructor(res: Response) { // Trim off any query parameters from the URL before logging, it may @@ -81,9 +82,10 @@ export class HTTPError extends Error { // necessarily the same as the request's URL. const url = new URL(res.url); url.search = ""; - super( - `Fetch failed: ${url.href}: HTTP ${res.status} ${res.statusText}`, - ); + super(`HTTP ${res.status} ${res.statusText} (${url.pathname})`); + + const requestID = res.headers.get("x-request-id"); + const details = { url: url.href, ...(requestID ? { requestID } : {}) }; // Cargo culted from // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error#custom_error_types @@ -92,6 +94,7 @@ export class HTTPError extends Error { this.name = this.constructor.name; this.res = res; + this.details = details; } } @@ -100,7 +103,11 @@ export class HTTPError extends Error { * {@link Response} does not have a HTTP 2xx status. */ export const ensureOk = (res: Response) => { - if (!res.ok) throw new HTTPError(res); + if (!res.ok) { + const e = new HTTPError(res); + log.error(`${e.message} ${JSON.stringify(e.details)}`); + throw e; + } }; /** diff --git a/web/packages/base/i18n-date.ts b/web/packages/base/i18n-date.ts index 1c60e56d1e..bd49fb118c 100644 --- a/web/packages/base/i18n-date.ts +++ b/web/packages/base/i18n-date.ts @@ -4,7 +4,73 @@ * Note that we rely on the current behaviour of a full reload on changing the * language. See: [Note: Changing locale causes a full reload]. */ -import i18n from "i18next"; +import i18n, { t } from "i18next"; + +const _dateFormat = new Intl.DateTimeFormat(i18n.language, { + weekday: "short", + day: "numeric", + month: "short", + year: "numeric", +}); + +const _dateWithoutYearFormat = new Intl.DateTimeFormat(i18n.language, { + weekday: "short", + day: "numeric", + month: "short", +}); + +const _timeFormat = new Intl.DateTimeFormat(i18n.language, { + timeStyle: "short", +}); + +/** + * Return a locale aware formatted date from the given {@link Date}. + * + * The behaviour depends upon whether the given {@link date} falls within the + * current calendar year. + * + * - For dates in the current year, year is omitted, e.g, "Fri, 21 Feb". + * + * - Otherwise, the year is included, e.g., "Fri, 21 Feb 2025". + */ +export const formattedDate = (date: Date) => + (isSameYear(date) ? _dateWithoutYearFormat : _dateFormat).format(date); + +const isSameYear = (date: Date) => + new Date().getFullYear() === date.getFullYear(); + +/** + * Return a locale aware formatted time from the given {@link Date}. + * + * Example: "11:51 AM" + */ +export const formattedTime = (date: Date) => _timeFormat.format(date); + +/** + * Return a locale aware formatted date and time from the given {@link Date}, + * using the year omission behavior as documented in {@link formattedDate}. + * + * Example: + * - If within year: "Fri, 21 Feb at 11:51 AM". + * - Otherwise: "Fri, 21 Feb 2025 at 11:51 AM" + * + * @param dateOrEpochMicroseconds A JavaScript Date or a numeric epoch + * microseconds value. + * + * As a convenience, this function can be either be directly passed a JavaScript + * date, or it can be given the raw epoch microseconds value and it'll convert + * internally. + * + * See: [Note: Remote timestamps are epoch microseconds] + */ +export const formattedDateTime = (dateOrEpochMicroseconds: Date | number) => + _formattedDateTime(toDate(dateOrEpochMicroseconds)); + +const _formattedDateTime = (date: Date) => + [formattedDate(date), t("at"), formattedTime(date)].join(" "); + +const toDate = (dm: Date | number) => + typeof dm == "number" ? new Date(dm / 1000) : dm; let _relativeTimeFormat: Intl.RelativeTimeFormat | undefined; diff --git a/web/packages/base/i18n.ts b/web/packages/base/i18n.ts index 80407383ec..6949de2eef 100644 --- a/web/packages/base/i18n.ts +++ b/web/packages/base/i18n.ts @@ -33,6 +33,7 @@ export const supportedLocales = [ "lt-LT" /* Lithuanian */, "uk-UA" /* Ukrainian */, "vi-VN" /* Vietnamese */, + "ja-JP" /* Japanese */, ] as const; /** The type of {@link supportedLocales}. */ @@ -134,6 +135,8 @@ export const setupI18n = async () => { // Value is an epoch microsecond so that we can directly pass the // timestamps we get from our API responses. The formatter expects // milliseconds, so divide by 1000. + // + // See [Note: Remote timestamps are epoch microseconds]. return (val) => formatter.format(val / 1000); }); }; @@ -190,6 +193,8 @@ const closestSupportedLocale = ( return "uk-UA"; } else if (ls.startsWith("vi")) { return "vi-VN"; + } else if (ls.startsWith("ja")) { + return "ja-JP"; } } diff --git a/web/packages/base/locales/ar-SA/translation.json b/web/packages/base/locales/ar-SA/translation.json index 89b4e7dcd1..ea5e46f77d 100644 --- a/web/packages/base/locales/ar-SA/translation.json +++ b/web/packages/base/locales/ar-SA/translation.json @@ -45,7 +45,6 @@ "create_albums": "إنشاء ألبومات", "enter_album_name": "اسم الألبوم", "close_key": "إغلاق (Esc)", - "enter_file_name": "إسم الملف", "close": "إغلاق", "yes": "", "no": "لا", @@ -490,8 +489,8 @@ "update_available_message": "", "download_and_install": "", "ignore_this_version": "", - "TODAY": "", - "YESTERDAY": "", + "today": "", + "yesterday": "", "enter_name": "أدخل الاسم", "uploader_name_hint": "أضف اسما حتى يتمكن أصدقاؤك من معرفة من يشكرون على هذه الصور الرائعة!", "name_placeholder": "", @@ -529,15 +528,15 @@ "gb": "", "tb": "" }, - "STOP_EXPORT": "", - "EXPORT_PROGRESS": "", - "MIGRATING_EXPORT": "", - "RENAMING_COLLECTION_FOLDERS": "", - "TRASHING_DELETED_FILES": "", - "TRASHING_DELETED_COLLECTIONS": "", - "CONTINUOUS_EXPORT": "", - "PENDING_ITEMS": "", - "EXPORT_STARTING": "", + "stop": "", + "sync_continuously": "", + "export_starting": "", + "preparing": "", + "renaming_album_folders": "", + "trashing_deleted_files": "", + "trashing_deleted_albums": "", + "export_progress": "", + "pending_items": "", "delete_account_reason_label": "", "delete_account_reason_placeholder": "", "delete_reason": { @@ -570,10 +569,10 @@ "downloading_album": "", "download_failed": "", "download_progress": "", - "CHRISTMAS": "", - "CHRISTMAS_EVE": "", - "NEW_YEAR": "", - "NEW_YEAR_EVE": "", + "christmas": "", + "christmas_eve": "", + "new_year": "", + "new_year_eve": "", "image": "", "video": "", "live_photo": "", diff --git a/web/packages/base/locales/be-BY/translation.json b/web/packages/base/locales/be-BY/translation.json index 933199338a..9da59ce6e9 100644 --- a/web/packages/base/locales/be-BY/translation.json +++ b/web/packages/base/locales/be-BY/translation.json @@ -45,7 +45,6 @@ "create_albums": "", "enter_album_name": "", "close_key": "", - "enter_file_name": "Назва файла", "close": "Закрыць", "yes": "", "no": "Не", @@ -250,7 +249,7 @@ "people_suggestions_empty": "", "info": "", "info_key": "", - "file_name": "", + "file_name": "Назва файла", "caption_placeholder": "", "location": "", "view_on_map": "", @@ -490,8 +489,8 @@ "update_available_message": "", "download_and_install": "", "ignore_this_version": "", - "TODAY": "", - "YESTERDAY": "", + "today": "", + "yesterday": "", "enter_name": "Увядзіце імя", "uploader_name_hint": "", "name_placeholder": "", @@ -529,15 +528,15 @@ "gb": "", "tb": "" }, - "STOP_EXPORT": "", - "EXPORT_PROGRESS": "", - "MIGRATING_EXPORT": "", - "RENAMING_COLLECTION_FOLDERS": "", - "TRASHING_DELETED_FILES": "", - "TRASHING_DELETED_COLLECTIONS": "", - "CONTINUOUS_EXPORT": "", - "PENDING_ITEMS": "", - "EXPORT_STARTING": "", + "stop": "", + "sync_continuously": "", + "export_starting": "", + "preparing": "", + "renaming_album_folders": "", + "trashing_deleted_files": "", + "trashing_deleted_albums": "", + "export_progress": "", + "pending_items": "", "delete_account_reason_label": "", "delete_account_reason_placeholder": "", "delete_reason": { @@ -570,10 +569,10 @@ "downloading_album": "", "download_failed": "", "download_progress": "", - "CHRISTMAS": "", - "CHRISTMAS_EVE": "", - "NEW_YEAR": "", - "NEW_YEAR_EVE": "", + "christmas": "", + "christmas_eve": "", + "new_year": "", + "new_year_eve": "", "image": "", "video": "", "live_photo": "", diff --git a/web/packages/base/locales/bg-BG/translation.json b/web/packages/base/locales/bg-BG/translation.json index b484e2c955..51f0fe3746 100644 --- a/web/packages/base/locales/bg-BG/translation.json +++ b/web/packages/base/locales/bg-BG/translation.json @@ -45,7 +45,6 @@ "create_albums": "", "enter_album_name": "", "close_key": "", - "enter_file_name": "", "close": "", "yes": "", "no": "", @@ -490,8 +489,8 @@ "update_available_message": "", "download_and_install": "", "ignore_this_version": "", - "TODAY": "", - "YESTERDAY": "", + "today": "", + "yesterday": "", "enter_name": "", "uploader_name_hint": "", "name_placeholder": "", @@ -529,15 +528,15 @@ "gb": "", "tb": "" }, - "STOP_EXPORT": "", - "EXPORT_PROGRESS": "", - "MIGRATING_EXPORT": "", - "RENAMING_COLLECTION_FOLDERS": "", - "TRASHING_DELETED_FILES": "", - "TRASHING_DELETED_COLLECTIONS": "", - "CONTINUOUS_EXPORT": "", - "PENDING_ITEMS": "", - "EXPORT_STARTING": "", + "stop": "", + "sync_continuously": "", + "export_starting": "", + "preparing": "", + "renaming_album_folders": "", + "trashing_deleted_files": "", + "trashing_deleted_albums": "", + "export_progress": "", + "pending_items": "", "delete_account_reason_label": "", "delete_account_reason_placeholder": "", "delete_reason": { @@ -570,10 +569,10 @@ "downloading_album": "", "download_failed": "", "download_progress": "", - "CHRISTMAS": "", - "CHRISTMAS_EVE": "", - "NEW_YEAR": "", - "NEW_YEAR_EVE": "", + "christmas": "", + "christmas_eve": "", + "new_year": "", + "new_year_eve": "", "image": "", "video": "", "live_photo": "", diff --git a/web/packages/base/locales/ca-ES/translation.json b/web/packages/base/locales/ca-ES/translation.json index e6f4c65fd6..a429ed19f7 100644 --- a/web/packages/base/locales/ca-ES/translation.json +++ b/web/packages/base/locales/ca-ES/translation.json @@ -45,7 +45,6 @@ "create_albums": "", "enter_album_name": "", "close_key": "", - "enter_file_name": "", "close": "", "yes": "", "no": "", @@ -490,8 +489,8 @@ "update_available_message": "", "download_and_install": "", "ignore_this_version": "", - "TODAY": "", - "YESTERDAY": "", + "today": "", + "yesterday": "", "enter_name": "", "uploader_name_hint": "", "name_placeholder": "", @@ -529,15 +528,15 @@ "gb": "", "tb": "" }, - "STOP_EXPORT": "", - "EXPORT_PROGRESS": "", - "MIGRATING_EXPORT": "", - "RENAMING_COLLECTION_FOLDERS": "", - "TRASHING_DELETED_FILES": "", - "TRASHING_DELETED_COLLECTIONS": "", - "CONTINUOUS_EXPORT": "", - "PENDING_ITEMS": "", - "EXPORT_STARTING": "", + "stop": "", + "sync_continuously": "", + "export_starting": "", + "preparing": "", + "renaming_album_folders": "", + "trashing_deleted_files": "", + "trashing_deleted_albums": "", + "export_progress": "", + "pending_items": "", "delete_account_reason_label": "", "delete_account_reason_placeholder": "", "delete_reason": { @@ -570,10 +569,10 @@ "downloading_album": "", "download_failed": "", "download_progress": "", - "CHRISTMAS": "", - "CHRISTMAS_EVE": "", - "NEW_YEAR": "", - "NEW_YEAR_EVE": "", + "christmas": "", + "christmas_eve": "", + "new_year": "", + "new_year_eve": "", "image": "", "video": "", "live_photo": "", diff --git a/web/packages/base/locales/cs-CZ/translation.json b/web/packages/base/locales/cs-CZ/translation.json new file mode 100644 index 0000000000..a429ed19f7 --- /dev/null +++ b/web/packages/base/locales/cs-CZ/translation.json @@ -0,0 +1,660 @@ +{ + "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": "", + "pick_password_hint": "", + "pick_password_caution": "", + "key_generation_in_progress": "", + "confirm_password": "", + "referral_source_hint": "", + "referral_source_info": "", + "password_mismatch_error": "", + "welcome_to_ente_title": "", + "welcome_to_ente_subtitle": "", + "new_album": "", + "create_albums": "", + "enter_album_name": "", + "close_key": "", + "close": "", + "yes": "", + "no": "", + "nothing_here": "", + "upload": "", + "import": "", + "add_photos": "", + "add_more_photos": "", + "add_photos_count_one": "", + "add_photos_count": "", + "select_photos": "", + "FILE_UPLOAD": "", + "UPLOAD_STAGE_MESSAGE": { + "0": "", + "1": "", + "3": "", + "4": "", + "5": "" + }, + "FILE_NOT_UPLOADED_LIST": "", + "initial_load_delay_warning": "", + "no_account": "", + "existing_account": "", + "create": "", + "files_count": "", + "download": "", + "download_album": "", + "download_favorites": "", + "download_uncategorized": "", + "download_hidden_items": "", + "download_key": "", + "copy_key": "", + "toggle_fullscreen_key": "", + "zoom_in_out_key": "", + "previous_key": "", + "next_key": "", + "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": "", + "delete_key": "", + "favorite": "", + "favorite_key": "", + "unfavorite_key": "", + "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": "", + "info_key": "", + "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_MESSAGE": "", + "email_already_taken": "", + "ETAGS_BLOCKED": "", + "LIVE_PHOTOS_DETECTED": "", + "RETRY_FAILED": "", + "FAILED_UPLOADS": "", + "failed_uploads_hint": "", + "SKIPPED_FILES": "", + "THUMBNAIL_GENERATION_FAILED_UPLOADS": "", + "UNSUPPORTED_FILES": "", + "SUCCESSFUL_UPLOADS": "", + "SKIPPED_INFO": "", + "UNSUPPORTED_INFO": "", + "BLOCKED_UPLOADS": "", + "INPROGRESS_UPLOADS": "", + "TOO_LARGE_UPLOADS": "", + "LARGER_THAN_AVAILABLE_STORAGE_UPLOADS": "", + "LARGER_THAN_AVAILABLE_STORAGE_INFO": "", + "TOO_LARGE_INFO": "", + "THUMBNAIL_GENERATION_FAILED_INFO": "", + "upload_to_album": "", + "add_to_album": "", + "move_to_album": "", + "unhide_to_album": "", + "restore_to_album": "", + "section_all": "", + "section_uncategorized": "", + "section_archive": "", + "section_hidden": "", + "section_trash": "", + "favorites": "", + "archive": "", + "archive_album": "", + "unarchive": "", + "unarchive_album": "", + "hide_collection": "", + "unhide_collection": "", + "move": "", + "add": "", + "remove": "", + "yes_remove": "", + "remove_from_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": "", + "live_photo_indicator": "", + "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": "", + "root_level_file_with_folder_not_allowed": "", + "root_level_file_with_folder_not_allowed_message": "", + "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": "", + "preparing": "", + "renaming_album_folders": "", + "trashing_deleted_files": "", + "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_notification": "", + "download_complete": "", + "downloading_album": "", + "download_failed": "", + "download_progress": "", + "christmas": "", + "christmas_eve": "", + "new_year": "", + "new_year_eve": "", + "image": "", + "video": "", + "live_photo": "", + "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": "", + "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": "" +} diff --git a/web/packages/base/locales/da-DK/translation.json b/web/packages/base/locales/da-DK/translation.json index aaf64f342b..32ca2ba14b 100644 --- a/web/packages/base/locales/da-DK/translation.json +++ b/web/packages/base/locales/da-DK/translation.json @@ -45,7 +45,6 @@ "create_albums": "", "enter_album_name": "", "close_key": "", - "enter_file_name": "", "close": "", "yes": "", "no": "", @@ -490,8 +489,8 @@ "update_available_message": "", "download_and_install": "", "ignore_this_version": "", - "TODAY": "", - "YESTERDAY": "", + "today": "", + "yesterday": "", "enter_name": "", "uploader_name_hint": "", "name_placeholder": "", @@ -529,15 +528,15 @@ "gb": "", "tb": "" }, - "STOP_EXPORT": "", - "EXPORT_PROGRESS": "", - "MIGRATING_EXPORT": "", - "RENAMING_COLLECTION_FOLDERS": "", - "TRASHING_DELETED_FILES": "", - "TRASHING_DELETED_COLLECTIONS": "", - "CONTINUOUS_EXPORT": "", - "PENDING_ITEMS": "", - "EXPORT_STARTING": "", + "stop": "", + "sync_continuously": "", + "export_starting": "", + "preparing": "", + "renaming_album_folders": "", + "trashing_deleted_files": "", + "trashing_deleted_albums": "", + "export_progress": "", + "pending_items": "", "delete_account_reason_label": "", "delete_account_reason_placeholder": "", "delete_reason": { @@ -570,10 +569,10 @@ "downloading_album": "", "download_failed": "", "download_progress": "", - "CHRISTMAS": "", - "CHRISTMAS_EVE": "", - "NEW_YEAR": "", - "NEW_YEAR_EVE": "", + "christmas": "", + "christmas_eve": "", + "new_year": "", + "new_year_eve": "", "image": "", "video": "", "live_photo": "", diff --git a/web/packages/base/locales/de-DE/translation.json b/web/packages/base/locales/de-DE/translation.json index c57b16b41e..0e758fc41a 100644 --- a/web/packages/base/locales/de-DE/translation.json +++ b/web/packages/base/locales/de-DE/translation.json @@ -45,7 +45,6 @@ "create_albums": "Alben erstellen", "enter_album_name": "Albumname", "close_key": "Schließen (Esc)", - "enter_file_name": "Dateiname", "close": "Schließen", "yes": "", "no": "Nein", @@ -490,8 +489,8 @@ "update_available_message": "Eine neue Version von Ente wurde veröffentlicht, aber sie kann nicht automatisch heruntergeladen und installiert werden.", "download_and_install": "Herunterladen und installieren", "ignore_this_version": "Diese Version ignorieren", - "TODAY": "Heute", - "YESTERDAY": "Gestern", + "today": "Heute", + "yesterday": "Gestern", "enter_name": "Name eingeben", "uploader_name_hint": "Füge einen Namen hinzu, damit deine Freunde wissen, wem sie für diese tollen Fotos zu danken haben!", "name_placeholder": "Name...", @@ -529,15 +528,15 @@ "gb": "GB", "tb": "TB" }, - "STOP_EXPORT": "Stop", - "EXPORT_PROGRESS": "{{progress.success, number}} / {{progress.total, number}} Dateien synchronisiert", - "MIGRATING_EXPORT": "Vorbereiten...", - "RENAMING_COLLECTION_FOLDERS": "Albumordner umbenennen...", - "TRASHING_DELETED_FILES": "Verschiebe gelöschte Dateien in den Trash-Ordner...", - "TRASHING_DELETED_COLLECTIONS": "Verschiebe gelöschte Alben in den Trash-Ordner...", - "CONTINUOUS_EXPORT": "Stets aktuell halten", - "PENDING_ITEMS": "Ausstehende Dateien", - "EXPORT_STARTING": "Starte Export...", + "stop": "", + "sync_continuously": "Stets aktuell halten", + "export_starting": "Starte Export...", + "preparing": "Vorbereiten...", + "renaming_album_folders": "Albumordner umbenennen...", + "trashing_deleted_files": "Verschiebe gelöschte Dateien in den Trash-Ordner...", + "trashing_deleted_albums": "Verschiebe gelöschte Alben in den Trash-Ordner...", + "export_progress": "{{progress.success, number}} / {{progress.total, number}} Dateien synchronisiert", + "pending_items": "Ausstehende Dateien", "delete_account_reason_label": "Was ist der Hauptgrund für die Löschung deines Kontos?", "delete_account_reason_placeholder": "Wähle einen Grund aus", "delete_reason": { @@ -570,10 +569,10 @@ "downloading_album": "Lade {{name}} herunter", "download_failed": "Herunterladen fehlgeschlagen", "download_progress": "{{count, number}} / {{total, number}} Dateien", - "CHRISTMAS": "Weihnachten", - "CHRISTMAS_EVE": "Heiligabend", - "NEW_YEAR": "Neujahr", - "NEW_YEAR_EVE": "Silvester", + "christmas": "Weihnachten", + "christmas_eve": "Heiligabend", + "new_year": "Neujahr", + "new_year_eve": "Silvester", "image": "Bild", "video": "Video", "live_photo": "Live-foto", diff --git a/web/packages/base/locales/el-GR/translation.json b/web/packages/base/locales/el-GR/translation.json index 85f3ac1af4..d3e4edb38b 100644 --- a/web/packages/base/locales/el-GR/translation.json +++ b/web/packages/base/locales/el-GR/translation.json @@ -45,7 +45,6 @@ "create_albums": "", "enter_album_name": "Όνομα άλμπουμ", "close_key": "Κλείσιμο (Esc)", - "enter_file_name": "Όνομα αρχείου", "close": "Κλείσιμο", "yes": "", "no": "Όχι", @@ -490,8 +489,8 @@ "update_available_message": "", "download_and_install": "", "ignore_this_version": "", - "TODAY": "Σήμερα", - "YESTERDAY": "Χθες", + "today": "Σήμερα", + "yesterday": "Χθες", "enter_name": "Εισάγετε όνομα", "uploader_name_hint": "Προσθέστε ένα όνομα, ώστε οι φίλοι σας να γνωρίζουν ποιον να ευχαριστήσουν για αυτές τις υπέροχες φωτογραφίες!", "name_placeholder": "Όνομα...", @@ -529,15 +528,15 @@ "gb": "GB", "tb": "TB" }, - "STOP_EXPORT": "Διακοπή", - "EXPORT_PROGRESS": "", - "MIGRATING_EXPORT": "Προετοιμασία...", - "RENAMING_COLLECTION_FOLDERS": "Μετονομασία φακέλων άλμπουμ...", - "TRASHING_DELETED_FILES": "", - "TRASHING_DELETED_COLLECTIONS": "", - "CONTINUOUS_EXPORT": "Συνεχής συγχρονισμός", - "PENDING_ITEMS": "Εκκρεμή αντικείμενα", - "EXPORT_STARTING": "Έναρξη εξαγωγής...", + "stop": "Διακοπή", + "sync_continuously": "Συνεχής συγχρονισμός", + "export_starting": "Έναρξη εξαγωγής...", + "preparing": "Προετοιμασία...", + "renaming_album_folders": "Μετονομασία φακέλων άλμπουμ...", + "trashing_deleted_files": "", + "trashing_deleted_albums": "", + "export_progress": "", + "pending_items": "Εκκρεμή αντικείμενα", "delete_account_reason_label": "Ποιος είναι ο κύριος λόγος που διαγράφετε το λογαριασμό σας;", "delete_account_reason_placeholder": "Επιλέξτε ένα λόγο", "delete_reason": { @@ -570,10 +569,10 @@ "downloading_album": "Λήψη {{name}}", "download_failed": "Η λήψη απέτυχε", "download_progress": "{{count, number}} / {{total, number}} αρχεία", - "CHRISTMAS": "Χριστούγεννα", - "CHRISTMAS_EVE": "Παραμονή Χριστουγέννων", - "NEW_YEAR": "Πρωτοχρονιά", - "NEW_YEAR_EVE": "Παραμονή Πρωτοχρονιάς", + "christmas": "Χριστούγεννα", + "christmas_eve": "Παραμονή Χριστουγέννων", + "new_year": "Πρωτοχρονιά", + "new_year_eve": "Παραμονή Πρωτοχρονιάς", "image": "Εικόνα", "video": "Βίντεο", "live_photo": "Ζωντανή φωτογραφία", diff --git a/web/packages/base/locales/en-US/translation.json b/web/packages/base/locales/en-US/translation.json index 5def46e704..d0f1c2c557 100644 --- a/web/packages/base/locales/en-US/translation.json +++ b/web/packages/base/locales/en-US/translation.json @@ -45,7 +45,6 @@ "create_albums": "Create albums", "enter_album_name": "Album name", "close_key": "Close (Esc)", - "enter_file_name": "File name", "close": "Close", "yes": "Yes", "no": "No", @@ -490,8 +489,8 @@ "update_available_message": "A new version of Ente has been released, but it cannot be automatically downloaded and installed.", "download_and_install": "Download and install", "ignore_this_version": "Ignore this version", - "TODAY": "Today", - "YESTERDAY": "Yesterday", + "today": "Today", + "yesterday": "Yesterday", "enter_name": "Enter name", "uploader_name_hint": "Add a name so that your friends know who to thank for these great photos!", "name_placeholder": "Name...", @@ -529,15 +528,15 @@ "gb": "GB", "tb": "TB" }, - "STOP_EXPORT": "Stop", - "EXPORT_PROGRESS": "{{progress.success, number}} / {{progress.total, number}} items synced", - "MIGRATING_EXPORT": "Preparing...", - "RENAMING_COLLECTION_FOLDERS": "Renaming album folders...", - "TRASHING_DELETED_FILES": "Trashing deleted files...", - "TRASHING_DELETED_COLLECTIONS": "Trashing deleted albums...", - "CONTINUOUS_EXPORT": "Sync continuously", - "PENDING_ITEMS": "Pending items", - "EXPORT_STARTING": "Export starting...", + "stop": "Stop", + "sync_continuously": "Sync continuously", + "export_starting": "Export starting...", + "preparing": "Preparing...", + "renaming_album_folders": "Renaming album folders...", + "trashing_deleted_files": "Trashing deleted files...", + "trashing_deleted_albums": "Trashing deleted albums...", + "export_progress": "{{progress.success, number}} / {{progress.total, number}} items synced", + "pending_items": "Pending items", "delete_account_reason_label": "What is the main reason you are deleting your account?", "delete_account_reason_placeholder": "Select a reason", "delete_reason": { @@ -570,10 +569,10 @@ "downloading_album": "Downloading {{name}}", "download_failed": "Download failed", "download_progress": "{{count, number}} / {{total, number}} files", - "CHRISTMAS": "Christmas", - "CHRISTMAS_EVE": "Christmas Eve", - "NEW_YEAR": "New Year", - "NEW_YEAR_EVE": "New Year's Eve", + "christmas": "Christmas", + "christmas_eve": "Christmas Eve", + "new_year": "New Year", + "new_year_eve": "New Year's Eve", "image": "Image", "video": "Video", "live_photo": "Live photo", diff --git a/web/packages/base/locales/es-ES/translation.json b/web/packages/base/locales/es-ES/translation.json index 625e2d4883..e749f4c2d2 100644 --- a/web/packages/base/locales/es-ES/translation.json +++ b/web/packages/base/locales/es-ES/translation.json @@ -45,7 +45,6 @@ "create_albums": "Crear álbumes", "enter_album_name": "Nombre del álbum", "close_key": "Cerrar (Esc)", - "enter_file_name": "Nombre del archivo", "close": "Cerrar", "yes": "Sí", "no": "No", @@ -490,8 +489,8 @@ "update_available_message": "Una nueva versión de ente ha sido lanzada, pero no se puede descargar e instalar automáticamente.", "download_and_install": "Descargar e instalar", "ignore_this_version": "Ignorar esta versión", - "TODAY": "Hoy", - "YESTERDAY": "Ayer", + "today": "Hoy", + "yesterday": "Ayer", "enter_name": "Introducir nombre", "uploader_name_hint": "¡Añade un nombre para que tus amigos sepan a quién dar las gracias por estas fotos geniales!", "name_placeholder": "Nombre...", @@ -529,15 +528,15 @@ "gb": "GB", "tb": "TB" }, - "STOP_EXPORT": "Stop", - "EXPORT_PROGRESS": "{{progress.success}} / {{progress.total}} archivos exportados", - "MIGRATING_EXPORT": "Preparando...", - "RENAMING_COLLECTION_FOLDERS": "Renombrando carpetas del álbum...", - "TRASHING_DELETED_FILES": "Eliminando ficheros borrados...", - "TRASHING_DELETED_COLLECTIONS": "Eliminando álbumes borrados...", - "CONTINUOUS_EXPORT": "Sincronizar continuamente", - "PENDING_ITEMS": "Elementos pendientes", - "EXPORT_STARTING": "Exportar iniciando...", + "stop": "", + "sync_continuously": "Sincronizar continuamente", + "export_starting": "Exportar iniciando...", + "preparing": "Preparando...", + "renaming_album_folders": "Renombrando carpetas del álbum...", + "trashing_deleted_files": "Eliminando ficheros borrados...", + "trashing_deleted_albums": "Eliminando álbumes borrados...", + "export_progress": "{{progress.success}} / {{progress.total}} archivos exportados", + "pending_items": "Elementos pendientes", "delete_account_reason_label": "¿Cuál es la razón principal por la que eliminas tu cuenta?", "delete_account_reason_placeholder": "Selecciona una razón", "delete_reason": { @@ -570,10 +569,10 @@ "downloading_album": "Descargando {{name}}", "download_failed": "Error al descargar", "download_progress": "{{count, number}} / {{total, number}} archivos", - "CHRISTMAS": "Navidad", - "CHRISTMAS_EVE": "Nochebuena", - "NEW_YEAR": "Año Nuevo", - "NEW_YEAR_EVE": "Nochevieja", + "christmas": "Navidad", + "christmas_eve": "Nochebuena", + "new_year": "Año Nuevo", + "new_year_eve": "Nochevieja", "image": "Imagen", "video": "Video", "live_photo": "Foto en vivo", diff --git a/web/packages/base/locales/et-EE/translation.json b/web/packages/base/locales/et-EE/translation.json index e6f4c65fd6..a429ed19f7 100644 --- a/web/packages/base/locales/et-EE/translation.json +++ b/web/packages/base/locales/et-EE/translation.json @@ -45,7 +45,6 @@ "create_albums": "", "enter_album_name": "", "close_key": "", - "enter_file_name": "", "close": "", "yes": "", "no": "", @@ -490,8 +489,8 @@ "update_available_message": "", "download_and_install": "", "ignore_this_version": "", - "TODAY": "", - "YESTERDAY": "", + "today": "", + "yesterday": "", "enter_name": "", "uploader_name_hint": "", "name_placeholder": "", @@ -529,15 +528,15 @@ "gb": "", "tb": "" }, - "STOP_EXPORT": "", - "EXPORT_PROGRESS": "", - "MIGRATING_EXPORT": "", - "RENAMING_COLLECTION_FOLDERS": "", - "TRASHING_DELETED_FILES": "", - "TRASHING_DELETED_COLLECTIONS": "", - "CONTINUOUS_EXPORT": "", - "PENDING_ITEMS": "", - "EXPORT_STARTING": "", + "stop": "", + "sync_continuously": "", + "export_starting": "", + "preparing": "", + "renaming_album_folders": "", + "trashing_deleted_files": "", + "trashing_deleted_albums": "", + "export_progress": "", + "pending_items": "", "delete_account_reason_label": "", "delete_account_reason_placeholder": "", "delete_reason": { @@ -570,10 +569,10 @@ "downloading_album": "", "download_failed": "", "download_progress": "", - "CHRISTMAS": "", - "CHRISTMAS_EVE": "", - "NEW_YEAR": "", - "NEW_YEAR_EVE": "", + "christmas": "", + "christmas_eve": "", + "new_year": "", + "new_year_eve": "", "image": "", "video": "", "live_photo": "", diff --git a/web/packages/base/locales/fa-IR/translation.json b/web/packages/base/locales/fa-IR/translation.json index 9fa96168e8..7ee1eff8e6 100644 --- a/web/packages/base/locales/fa-IR/translation.json +++ b/web/packages/base/locales/fa-IR/translation.json @@ -45,7 +45,6 @@ "create_albums": "", "enter_album_name": "", "close_key": "", - "enter_file_name": "", "close": "", "yes": "", "no": "", @@ -490,8 +489,8 @@ "update_available_message": "", "download_and_install": "", "ignore_this_version": "", - "TODAY": "", - "YESTERDAY": "", + "today": "", + "yesterday": "", "enter_name": "", "uploader_name_hint": "", "name_placeholder": "", @@ -529,15 +528,15 @@ "gb": "", "tb": "" }, - "STOP_EXPORT": "", - "EXPORT_PROGRESS": "", - "MIGRATING_EXPORT": "", - "RENAMING_COLLECTION_FOLDERS": "", - "TRASHING_DELETED_FILES": "", - "TRASHING_DELETED_COLLECTIONS": "", - "CONTINUOUS_EXPORT": "", - "PENDING_ITEMS": "", - "EXPORT_STARTING": "", + "stop": "", + "sync_continuously": "", + "export_starting": "", + "preparing": "", + "renaming_album_folders": "", + "trashing_deleted_files": "", + "trashing_deleted_albums": "", + "export_progress": "", + "pending_items": "", "delete_account_reason_label": "", "delete_account_reason_placeholder": "", "delete_reason": { @@ -570,10 +569,10 @@ "downloading_album": "", "download_failed": "", "download_progress": "", - "CHRISTMAS": "", - "CHRISTMAS_EVE": "", - "NEW_YEAR": "", - "NEW_YEAR_EVE": "", + "christmas": "", + "christmas_eve": "", + "new_year": "", + "new_year_eve": "", "image": "", "video": "", "live_photo": "", diff --git a/web/packages/base/locales/fi-FI/translation.json b/web/packages/base/locales/fi-FI/translation.json index 31688b3bf6..4d3d45c598 100644 --- a/web/packages/base/locales/fi-FI/translation.json +++ b/web/packages/base/locales/fi-FI/translation.json @@ -45,7 +45,6 @@ "create_albums": "Luo albumit", "enter_album_name": "Albumin nimi", "close_key": "Sulje (Esc)", - "enter_file_name": "Tiedoston nimi", "close": "Sulje", "yes": "Kyllä", "no": "Ei", @@ -250,7 +249,7 @@ "people_suggestions_empty": "", "info": "", "info_key": "", - "file_name": "", + "file_name": "Tiedoston nimi", "caption_placeholder": "", "location": "", "view_on_map": "Näytä OpenStreetMapissa", @@ -490,8 +489,8 @@ "update_available_message": "", "download_and_install": "", "ignore_this_version": "", - "TODAY": "", - "YESTERDAY": "", + "today": "", + "yesterday": "", "enter_name": "Lisää nimi", "uploader_name_hint": "Lisää nimi, jotta ystäväsi tietävät, ketä kiittää näistä hienoista kuvista!", "name_placeholder": "", @@ -529,15 +528,15 @@ "gb": "", "tb": "" }, - "STOP_EXPORT": "", - "EXPORT_PROGRESS": "", - "MIGRATING_EXPORT": "", - "RENAMING_COLLECTION_FOLDERS": "", - "TRASHING_DELETED_FILES": "", - "TRASHING_DELETED_COLLECTIONS": "", - "CONTINUOUS_EXPORT": "", - "PENDING_ITEMS": "", - "EXPORT_STARTING": "", + "stop": "", + "sync_continuously": "", + "export_starting": "", + "preparing": "", + "renaming_album_folders": "", + "trashing_deleted_files": "", + "trashing_deleted_albums": "", + "export_progress": "", + "pending_items": "", "delete_account_reason_label": "", "delete_account_reason_placeholder": "", "delete_reason": { @@ -570,10 +569,10 @@ "downloading_album": "", "download_failed": "", "download_progress": "", - "CHRISTMAS": "", - "CHRISTMAS_EVE": "", - "NEW_YEAR": "", - "NEW_YEAR_EVE": "", + "christmas": "", + "christmas_eve": "", + "new_year": "", + "new_year_eve": "", "image": "", "video": "", "live_photo": "", diff --git a/web/packages/base/locales/fr-FR/translation.json b/web/packages/base/locales/fr-FR/translation.json index 4f49c9e60c..47878e670b 100644 --- a/web/packages/base/locales/fr-FR/translation.json +++ b/web/packages/base/locales/fr-FR/translation.json @@ -45,7 +45,6 @@ "create_albums": "Créer des albums", "enter_album_name": "Nom de l'album", "close_key": "Fermer (Échap)", - "enter_file_name": "Nom du fichier", "close": "Fermer", "yes": "Oui", "no": "Non", @@ -490,8 +489,8 @@ "update_available_message": "Une nouvelle version de Ente est sortie, mais elle ne peut pas être automatiquement téléchargée puis installée.", "download_and_install": "Télécharger et installer", "ignore_this_version": "Ignorer cette version", - "TODAY": "Aujourd'hui", - "YESTERDAY": "Hier", + "today": "Aujourd'hui", + "yesterday": "Hier", "enter_name": "Saisir un nom", "uploader_name_hint": "Ajouter un nom afin que vos amis sachent qui remercier pour ces magnifiques photos!", "name_placeholder": "Nom...", @@ -529,15 +528,15 @@ "gb": "Go", "tb": "To" }, - "STOP_EXPORT": "Stop", - "EXPORT_PROGRESS": "{{progress.success}} / {{progress.total}} fichiers exportés", - "MIGRATING_EXPORT": "Préparations...", - "RENAMING_COLLECTION_FOLDERS": "Renommage des dossiers de l'album en cours...", - "TRASHING_DELETED_FILES": "Mise à la corbeille des fichiers supprimés...", - "TRASHING_DELETED_COLLECTIONS": "Mise à la corbeille des albums supprimés...", - "CONTINUOUS_EXPORT": "Synchronisation en continu", - "PENDING_ITEMS": "Objets en attente", - "EXPORT_STARTING": "Démarrage de l'export...", + "stop": "Arrêter", + "sync_continuously": "Synchronisation en continu", + "export_starting": "Démarrage de l'export...", + "preparing": "En cours de préparation...", + "renaming_album_folders": "Renommage des dossiers de l'album en cours...", + "trashing_deleted_files": "Mise à la corbeille des fichiers supprimés...", + "trashing_deleted_albums": "Mise à la corbeille des albums supprimés...", + "export_progress": "{{progress.success}} / {{progress.total}} fichiers exportés", + "pending_items": "Objets en attente", "delete_account_reason_label": "Quelle est la raison principale de la suppression de votre compte ?", "delete_account_reason_placeholder": "Choisir une raison", "delete_reason": { @@ -570,10 +569,10 @@ "downloading_album": "Téléchargement de {{name}}", "download_failed": "Échec du téléchargement", "download_progress": "{{count, number}} / {{total, number}} fichiers", - "CHRISTMAS": "Noël", - "CHRISTMAS_EVE": "Réveillon de Noël", - "NEW_YEAR": "Nouvel an", - "NEW_YEAR_EVE": "Réveillon de Nouvel An", + "christmas": "Noël", + "christmas_eve": "Réveillon de Noël", + "new_year": "Nouvel an", + "new_year_eve": "Réveillon du Nouvel An", "image": "Image", "video": "Vidéo", "live_photo": "Photos en direct", diff --git a/web/packages/base/locales/gu-IN/translation.json b/web/packages/base/locales/gu-IN/translation.json index e6f4c65fd6..a429ed19f7 100644 --- a/web/packages/base/locales/gu-IN/translation.json +++ b/web/packages/base/locales/gu-IN/translation.json @@ -45,7 +45,6 @@ "create_albums": "", "enter_album_name": "", "close_key": "", - "enter_file_name": "", "close": "", "yes": "", "no": "", @@ -490,8 +489,8 @@ "update_available_message": "", "download_and_install": "", "ignore_this_version": "", - "TODAY": "", - "YESTERDAY": "", + "today": "", + "yesterday": "", "enter_name": "", "uploader_name_hint": "", "name_placeholder": "", @@ -529,15 +528,15 @@ "gb": "", "tb": "" }, - "STOP_EXPORT": "", - "EXPORT_PROGRESS": "", - "MIGRATING_EXPORT": "", - "RENAMING_COLLECTION_FOLDERS": "", - "TRASHING_DELETED_FILES": "", - "TRASHING_DELETED_COLLECTIONS": "", - "CONTINUOUS_EXPORT": "", - "PENDING_ITEMS": "", - "EXPORT_STARTING": "", + "stop": "", + "sync_continuously": "", + "export_starting": "", + "preparing": "", + "renaming_album_folders": "", + "trashing_deleted_files": "", + "trashing_deleted_albums": "", + "export_progress": "", + "pending_items": "", "delete_account_reason_label": "", "delete_account_reason_placeholder": "", "delete_reason": { @@ -570,10 +569,10 @@ "downloading_album": "", "download_failed": "", "download_progress": "", - "CHRISTMAS": "", - "CHRISTMAS_EVE": "", - "NEW_YEAR": "", - "NEW_YEAR_EVE": "", + "christmas": "", + "christmas_eve": "", + "new_year": "", + "new_year_eve": "", "image": "", "video": "", "live_photo": "", diff --git a/web/packages/base/locales/hi-IN/translation.json b/web/packages/base/locales/hi-IN/translation.json index e6f4c65fd6..a429ed19f7 100644 --- a/web/packages/base/locales/hi-IN/translation.json +++ b/web/packages/base/locales/hi-IN/translation.json @@ -45,7 +45,6 @@ "create_albums": "", "enter_album_name": "", "close_key": "", - "enter_file_name": "", "close": "", "yes": "", "no": "", @@ -490,8 +489,8 @@ "update_available_message": "", "download_and_install": "", "ignore_this_version": "", - "TODAY": "", - "YESTERDAY": "", + "today": "", + "yesterday": "", "enter_name": "", "uploader_name_hint": "", "name_placeholder": "", @@ -529,15 +528,15 @@ "gb": "", "tb": "" }, - "STOP_EXPORT": "", - "EXPORT_PROGRESS": "", - "MIGRATING_EXPORT": "", - "RENAMING_COLLECTION_FOLDERS": "", - "TRASHING_DELETED_FILES": "", - "TRASHING_DELETED_COLLECTIONS": "", - "CONTINUOUS_EXPORT": "", - "PENDING_ITEMS": "", - "EXPORT_STARTING": "", + "stop": "", + "sync_continuously": "", + "export_starting": "", + "preparing": "", + "renaming_album_folders": "", + "trashing_deleted_files": "", + "trashing_deleted_albums": "", + "export_progress": "", + "pending_items": "", "delete_account_reason_label": "", "delete_account_reason_placeholder": "", "delete_reason": { @@ -570,10 +569,10 @@ "downloading_album": "", "download_failed": "", "download_progress": "", - "CHRISTMAS": "", - "CHRISTMAS_EVE": "", - "NEW_YEAR": "", - "NEW_YEAR_EVE": "", + "christmas": "", + "christmas_eve": "", + "new_year": "", + "new_year_eve": "", "image": "", "video": "", "live_photo": "", diff --git a/web/packages/base/locales/hu-HU/translation.json b/web/packages/base/locales/hu-HU/translation.json index e6f4c65fd6..a429ed19f7 100644 --- a/web/packages/base/locales/hu-HU/translation.json +++ b/web/packages/base/locales/hu-HU/translation.json @@ -45,7 +45,6 @@ "create_albums": "", "enter_album_name": "", "close_key": "", - "enter_file_name": "", "close": "", "yes": "", "no": "", @@ -490,8 +489,8 @@ "update_available_message": "", "download_and_install": "", "ignore_this_version": "", - "TODAY": "", - "YESTERDAY": "", + "today": "", + "yesterday": "", "enter_name": "", "uploader_name_hint": "", "name_placeholder": "", @@ -529,15 +528,15 @@ "gb": "", "tb": "" }, - "STOP_EXPORT": "", - "EXPORT_PROGRESS": "", - "MIGRATING_EXPORT": "", - "RENAMING_COLLECTION_FOLDERS": "", - "TRASHING_DELETED_FILES": "", - "TRASHING_DELETED_COLLECTIONS": "", - "CONTINUOUS_EXPORT": "", - "PENDING_ITEMS": "", - "EXPORT_STARTING": "", + "stop": "", + "sync_continuously": "", + "export_starting": "", + "preparing": "", + "renaming_album_folders": "", + "trashing_deleted_files": "", + "trashing_deleted_albums": "", + "export_progress": "", + "pending_items": "", "delete_account_reason_label": "", "delete_account_reason_placeholder": "", "delete_reason": { @@ -570,10 +569,10 @@ "downloading_album": "", "download_failed": "", "download_progress": "", - "CHRISTMAS": "", - "CHRISTMAS_EVE": "", - "NEW_YEAR": "", - "NEW_YEAR_EVE": "", + "christmas": "", + "christmas_eve": "", + "new_year": "", + "new_year_eve": "", "image": "", "video": "", "live_photo": "", diff --git a/web/packages/base/locales/id-ID/translation.json b/web/packages/base/locales/id-ID/translation.json index e05baf1b6b..63352f91c8 100644 --- a/web/packages/base/locales/id-ID/translation.json +++ b/web/packages/base/locales/id-ID/translation.json @@ -45,7 +45,6 @@ "create_albums": "", "enter_album_name": "Nama album", "close_key": "Tutup (Esc)", - "enter_file_name": "Nama file", "close": "Tutup", "yes": "", "no": "Tidak", @@ -490,8 +489,8 @@ "update_available_message": "Versi baru Ente telah dirilis, namun tidak dapat diunduh dan diinstal secara otomatis.", "download_and_install": "Unduh dan instal", "ignore_this_version": "", - "TODAY": "", - "YESTERDAY": "Kemarin", + "today": "", + "yesterday": "Kemarin", "enter_name": "Masukkan nama", "uploader_name_hint": "Tambahkan nama agar teman Anda tahu kepada siapa harus berterima kasih atas foto-foto hebat ini!", "name_placeholder": "Nama...", @@ -529,15 +528,15 @@ "gb": "GB", "tb": "TB" }, - "STOP_EXPORT": "Hentikan", - "EXPORT_PROGRESS": "{{progress.success, number}} / {{progress.total, number}} item tersinkron", - "MIGRATING_EXPORT": "Menyiapkan...", - "RENAMING_COLLECTION_FOLDERS": "", - "TRASHING_DELETED_FILES": "", - "TRASHING_DELETED_COLLECTIONS": "", - "CONTINUOUS_EXPORT": "", - "PENDING_ITEMS": "", - "EXPORT_STARTING": "", + "stop": "Hentikan", + "sync_continuously": "", + "export_starting": "", + "preparing": "Menyiapkan...", + "renaming_album_folders": "", + "trashing_deleted_files": "", + "trashing_deleted_albums": "", + "export_progress": "{{progress.success, number}} / {{progress.total, number}} item tersinkron", + "pending_items": "", "delete_account_reason_label": "", "delete_account_reason_placeholder": "", "delete_reason": { @@ -570,10 +569,10 @@ "downloading_album": "Mengunduh {{name}}", "download_failed": "Gagal mengunduh", "download_progress": "{{count, number}} / {{total, number}} file", - "CHRISTMAS": "Natal", - "CHRISTMAS_EVE": "Malam Natal", - "NEW_YEAR": "Tahun Baru", - "NEW_YEAR_EVE": "Malam Tahun Baru", + "christmas": "Natal", + "christmas_eve": "Malam Natal", + "new_year": "Tahun Baru", + "new_year_eve": "Malam Tahun Baru", "image": "", "video": "", "live_photo": "", diff --git a/web/packages/base/locales/is-IS/translation.json b/web/packages/base/locales/is-IS/translation.json index b914a48c6d..29fa75d441 100644 --- a/web/packages/base/locales/is-IS/translation.json +++ b/web/packages/base/locales/is-IS/translation.json @@ -45,7 +45,6 @@ "create_albums": "", "enter_album_name": "", "close_key": "", - "enter_file_name": "", "close": "Loka", "yes": "", "no": "Nei", @@ -490,8 +489,8 @@ "update_available_message": "", "download_and_install": "", "ignore_this_version": "", - "TODAY": "", - "YESTERDAY": "", + "today": "", + "yesterday": "", "enter_name": "", "uploader_name_hint": "", "name_placeholder": "", @@ -529,15 +528,15 @@ "gb": "", "tb": "" }, - "STOP_EXPORT": "", - "EXPORT_PROGRESS": "", - "MIGRATING_EXPORT": "", - "RENAMING_COLLECTION_FOLDERS": "", - "TRASHING_DELETED_FILES": "", - "TRASHING_DELETED_COLLECTIONS": "", - "CONTINUOUS_EXPORT": "", - "PENDING_ITEMS": "", - "EXPORT_STARTING": "", + "stop": "", + "sync_continuously": "", + "export_starting": "", + "preparing": "", + "renaming_album_folders": "", + "trashing_deleted_files": "", + "trashing_deleted_albums": "", + "export_progress": "", + "pending_items": "", "delete_account_reason_label": "", "delete_account_reason_placeholder": "", "delete_reason": { @@ -570,10 +569,10 @@ "downloading_album": "", "download_failed": "", "download_progress": "", - "CHRISTMAS": "", - "CHRISTMAS_EVE": "", - "NEW_YEAR": "Nýtt ár", - "NEW_YEAR_EVE": "", + "christmas": "", + "christmas_eve": "", + "new_year": "Nýtt ár", + "new_year_eve": "", "image": "Mynd", "video": "Mynband", "live_photo": "", diff --git a/web/packages/base/locales/it-IT/translation.json b/web/packages/base/locales/it-IT/translation.json index f951e472b5..188d9d75b7 100644 --- a/web/packages/base/locales/it-IT/translation.json +++ b/web/packages/base/locales/it-IT/translation.json @@ -45,7 +45,6 @@ "create_albums": "Crea album", "enter_album_name": "Nome album", "close_key": "Chiudi (Esc)", - "enter_file_name": "Nome del file", "close": "Chiudi", "yes": "", "no": "No", @@ -490,8 +489,8 @@ "update_available_message": "Una nuova versione di Ente è stata rilasciata, ma non può essere scaricata e installata automaticamente.", "download_and_install": "Scarica e installa", "ignore_this_version": "Ignora questa versione", - "TODAY": "Oggi", - "YESTERDAY": "Ieri", + "today": "Oggi", + "yesterday": "Ieri", "enter_name": "Inserisci il nome", "uploader_name_hint": "Aggiungi un nome in modo che i tuoi amici sappiano chi ringraziare per queste fantastiche foto!", "name_placeholder": "Nome...", @@ -529,15 +528,15 @@ "gb": "GB", "tb": "TB" }, - "STOP_EXPORT": "Stop", - "EXPORT_PROGRESS": "{{progress.success, number}} / {{progress.total, number}} elementi sincronizzati", - "MIGRATING_EXPORT": "Preparazione...", - "RENAMING_COLLECTION_FOLDERS": "Rinomino cartelle album...", - "TRASHING_DELETED_FILES": "Sposto i file nel cestino...", - "TRASHING_DELETED_COLLECTIONS": "Sposto gli album nel cestino...", - "CONTINUOUS_EXPORT": "Sincronizza continuamente", - "PENDING_ITEMS": "Elementi in sospeso", - "EXPORT_STARTING": "Esportazione iniziata...", + "stop": "", + "sync_continuously": "Sincronizza continuamente", + "export_starting": "Esportazione iniziata...", + "preparing": "Preparazione...", + "renaming_album_folders": "Rinomino cartelle album...", + "trashing_deleted_files": "Sposto i file nel cestino...", + "trashing_deleted_albums": "Sposto gli album nel cestino...", + "export_progress": "{{progress.success, number}} / {{progress.total, number}} elementi sincronizzati", + "pending_items": "Elementi in sospeso", "delete_account_reason_label": "Qual è il motivo per cui stai cancellando il tuo account?", "delete_account_reason_placeholder": "Seleziona un motivo", "delete_reason": { @@ -570,10 +569,10 @@ "downloading_album": "Download {{name}}", "download_failed": "Download fallito", "download_progress": "{{count, number}} / {{total, number}} file", - "CHRISTMAS": "Natale", - "CHRISTMAS_EVE": "Vigilia di Natale", - "NEW_YEAR": "Capodanno", - "NEW_YEAR_EVE": "Vigilia di Capodanno", + "christmas": "Natale", + "christmas_eve": "Vigilia di Natale", + "new_year": "Capodanno", + "new_year_eve": "Vigilia di Capodanno", "image": "Immagine", "video": "Video", "live_photo": "", diff --git a/web/packages/base/locales/ja-JP/translation.json b/web/packages/base/locales/ja-JP/translation.json index 2be62cb46b..01e792e36c 100644 --- a/web/packages/base/locales/ja-JP/translation.json +++ b/web/packages/base/locales/ja-JP/translation.json @@ -45,7 +45,6 @@ "create_albums": "アルバムを作成", "enter_album_name": "アルバムの名前", "close_key": "閉じる (Esc)", - "enter_file_name": "ファイル名", "close": "閉じる", "yes": "はい", "no": "いいえ", @@ -66,7 +65,7 @@ "5": "バックアップが完了しました" }, "FILE_NOT_UPLOADED_LIST": "次のファイルはアップロードされませんでした", - "initial_load_delay_warning": "", + "initial_load_delay_warning": "最初の読み込みには時間がかかる場合があります", "no_account": "アカウントがありませんか", "existing_account": "既にアカウントをお持ちの方", "create": "作成", @@ -87,8 +86,8 @@ "title_accounts": "Ente アカウント", "upload_first_photo": "最初の写真をアップロード", "import_your_folders": "フォルダをインポート", - "upload_dropzone_hint": "", - "watch_folder_dropzone_hint": "", + "upload_dropzone_hint": "ファイルをバックアップするにはドロップします", + "watch_folder_dropzone_hint": "ドロップして監視フォルダを追加", "trash_files_title": "ファイルを削除しますか?", "trash_file_title": "ファイルを削除しますか?", "delete_files_title": "すぐに削除しますか?", @@ -99,10 +98,10 @@ "delete_key": "削除 (DEL)", "favorite": "お気に入り", "favorite_key": "お気に入り (L)", - "unfavorite_key": "", + "unfavorite_key": "Unfavorite (L)", "convert": "変換する", "multi_folder_upload": "複数のフォルダが検出されました", - "upload_to_choice": "", + "upload_to_choice": "アップロードしますか", "upload_to_single_album": "1つのアルバム", "upload_to_album_per_folder": "アルバムを分割", "session_expired": "セッション切れ", @@ -110,14 +109,14 @@ "password_generation_failed": "お使いのブラウザはEnteの暗号化基準を満たす強力なキーを生成できませんでした。モバイルアプリまたは別のブラウザを使用してみてください", "change_password": "パスワードを変更", "password_changed_elsewhere": "パスワードが他の場所で変更されました", - "password_changed_elsewhere_message": "", + "password_changed_elsewhere_message": "新しいパスワードを使用して認証するには、このデバイスで再度ログインしてください。", "go_back": "戻る", - "account": "", + "account": "アカウント", "recovery_key": "リカバリーキー", "do_this_later": "あとで行う", "save_key": "キーを保存", "recovery_key_description": "パスワードを忘れた場合、データを回復できる唯一の方法がこのキーです。", - "key_not_stored_note": "", + "key_not_stored_note": "私たちはこのキーを保持しません。そのためキーを安全な場所に保存してください", "recovery_key_generation_failed": "リカバリコードを生成できませんでした。もう一度やり直してください", "forgot_password": "パスワードを忘れた場合", "recover_account": "アカウントの復旧", @@ -125,26 +124,26 @@ "no_recovery_key_title": "リカバリーキーがありませんか?", "incorrect_recovery_key": "リカバリーキーが正しくありません", "sorry": "申し訳ありません", - "no_recovery_key_message": "", - "no_two_factor_recovery_key_message": "", + "no_recovery_key_message": "エンドツーエンドの暗号化プロトコルの性質上、あなたのデータはパスワードやリカバリキーなしで復号できません", + "no_two_factor_recovery_key_message": "あなたの登録したメールアドレスから{{emailID}} にメールをお送りください", "contact_support": "お問い合わせ", - "help": "", - "ente_help": "", - "blog": "", + "help": "Help", + "ente_help": "Ente Help", + "blog": "ブログ", "request_feature": "機能をリクエスト", "support": "サポート", "cancel": "キャンセル", "logout": "ログアウト", "logout_message": "本当にログアウトしてよろしいですか?", "delete_account": "アカウント削除", - "delete_account_manually_message": "", + "delete_account_manually_message": "

ご登録のメールアドレスから {{emailID}} にメールを送信してください。

あなたのリクエストは72時間以内に処理されます。

", "change_email": "メールアドレスを変更", "ok": "OK", "success": "成功", "error": "エラー", "offline_message": "オフラインです。キャッシュされた内容が表示されています", "install": "インストール", - "install_mobile_app": "", + "install_mobile_app": "Android または iOS アプリをインストールして、すべての写真を自動的にバックアップします", "download_app": "デスクトップアプリをダウンロード", "download_app_message": "申し訳ありませんが、この操作は現在デスクトップアプリでのみサポートされています", "subscription": "サブスクリプション", @@ -152,26 +151,26 @@ "manage_family": "ファミリー管理", "family_plan": "ファミリープラン", "leave_family_plan": "ファミリープランから退会", - "leave": "", + "leave": "離脱", "leave_family_plan_confirm": "本当にファミリープランを退会しますか?", "choose_plan": "プランを選択", "manage_plan": "サブスクリプションの管理", "current_usage": "現在の使用量は {{usage}}", - "two_months_free": "", + "two_months_free": "年次プランでは2ヶ月分無料", "free_plan_option": "無料プランを継続する", "free_plan_description": "{{storage}} は永久に無料です", "active": "アクティブ", "subscription_info_free": "無料プランをご利用いただいています", "subscription_info_family": "あなたはファミリープランを利用しています:", - "subscription_info_expired": "", + "subscription_info_expired": "サブスクリプションの有効期限が切れています。更新してください", "subscription_info_renewal_cancelled": "サブスクリプションは{{date, date}}でキャンセルされます", - "subscription_info_storage_quota_exceeded": "", + "subscription_info_storage_quota_exceeded": "ストレージの上限を超えました。アップグレードしてください。", "subscription_status_renewal_active": "{{date, date}}更新", "subscription_status_renewal_cancelled": "{{date, date}}終了", "add_on_valid_till": "あなたの {{storage}} アドオンは {{date, date}} まで有効です", "subscription_expired": "サブスクリプションが期限切れです", "storage_quota_exceeded": "ストレージの上限に達しました", - "subscription_purchase_success": "", + "subscription_purchase_success": "

お支払いを確認しました。

あなたのサブスクリプションは {{date, date}} まで有効です。

", "subscription_purchase_cancelled": "購入がキャンセルされました。購読する場合は、もう一度お試しください", "subscription_purchase_failed": "サブスクリプションの購入に失敗しました。もう一度お試しください", "subscription_verification_error": "サブスクリプションの検証に失敗しました", @@ -186,10 +185,10 @@ "update_subscription_title": "プラン変更を確認", "update_subscription_message": "プランを変更して良いですか?", "cancel_subscription": "サブスクリプションをキャンセル", - "cancel_subscription_message": "", - "cancel_subscription_with_addon_message": "", - "subscription_cancel_success": "", - "reactivate_subscription": "", + "cancel_subscription_message": "

請求期間の終了時に、お客様のデータはすべて弊社のサーバーから削除されます。

サブスクリプションをキャンセルしてもよろしいですか?

", + "cancel_subscription_with_addon_message": "

サブスクリプションを解約しますか?

", + "subscription_cancel_success": "サブスクリプションをキャンセルしました", + "reactivate_subscription": "サブスクリプションを再有効化", "reactivate_subscription_message": "再有効化すると、{{date, date}} に請求されます", "subscription_activate_success": "サブスクリプションが正常に有効になりました ", "thank_you": "Thank you", @@ -201,15 +200,15 @@ "rename_album": "アルバム名を変更", "delete_album": "アルバムを削除", "delete_album_title": "アルバムを削除しますか?", - "delete_album_message": "", + "delete_album_message": "このアルバムに含まれている写真 (およびビデオ) を すべて 他のアルバムからも削除しますか?", "delete_photos": "写真を削除", "keep_photos": "写真を残す", "share_album": "アルバムを共有", - "sharing_with_self": "", - "sharing_already_shared": "", + "sharing_with_self": "自分自身と共有することはできません", + "sharing_already_shared": "既に {{email}} と共有しています", "sharing_album_not_allowed": "アルバムの共有は許可されていません", "sharing_disabled_for_free_accounts": "無料アカウントでは共有は無効です", - "sharing_user_does_not_exist": "", + "sharing_user_does_not_exist": "そのメールアドレスのユーザーが見つかりませんでした", "search": "検索", "search_results": "検索結果", "no_results": "一致する結果が見つかりませんでした", @@ -219,27 +218,27 @@ "description": "説明", "file_type": "ファイルの種類", "magic": "マジック", - "photos_count_zero": "", - "photos_count_one": "", - "photos_count": "", + "photos_count_zero": "0枚", + "photos_count_one": "1枚", + "photos_count": "{{count, number}}枚", "terms_and_conditions": "規約 および プライバシー ポリシー に同意します", - "people": "", - "indexing_scheduled": "", - "indexing_photos": "", - "indexing_fetching": "", - "indexing_people": "", - "syncing_wait": "", - "people_empty_too_few": "", - "unnamed_person": "", - "add_a_name": "", + "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": "", + "reset_person_confirm": "リセットしますか?", + "reset_person_confirm_message": "この人の名前、顔のグループ化、提案はリセットされます", "ignore": "無視する", - "ignore_person_confirm": "", - "ignore_person_confirm_message": "", + "ignore_person_confirm": "無視しますか?", + "ignore_person_confirm_message": "この顔のグループ化は人リストには表示されません", "ignored": "無視", "show_person": "人物を表示", "review_suggestions": "提案を確認", @@ -257,63 +256,63 @@ "map": "マップ", "enable_map": "マップを有効にする", "enable_maps_confirm": "マップを有効にしますか?", - "enable_maps_confirm_message": "", + "enable_maps_confirm_message": "世界地図上にあなたの写真を表示します。\n\n地図はOpenStreetMapを利用しており、あなたの写真の位置情報が外部に共有されることはありません。\n\nこの機能は設定から無効にすることができます", "disable_map": "マップを無効にする", "disable_maps_confirm": "マップを無効にしますか?", - "disable_maps_confirm_message": "", + "disable_maps_confirm_message": "

ワールドマップでの写真の表示を無効にします。

設定からいつでもこの機能を有効にできます。

", "details": "詳細", "view_exif": "すべての Exif データを表示", "no_exif": "Exifデータがありません", "exif": "Exif", "two_factor": "二要素", "two_factor_authentication": "二要素認証", - "two_factor_qr_help": "", - "two_factor_manual_entry_title": "", - "two_factor_manual_entry_message": "", - "scan_qr_title": "", + "two_factor_qr_help": "以下のQRコードをお気に入りの認証アプリでスキャンしてください", + "two_factor_manual_entry_title": "コードを手動で入力", + "two_factor_manual_entry_message": "このコードをお気に入りの認証アプリに入力してください", + "scan_qr_title": "代わりにQRコードをスキャン", "enable_two_factor": "二要素を有効化", "enable": "有効化", "enabled": "有効", "lost_2fa_device": "紛失した二要素デバイス", "incorrect_code": "不正なコード", - "two_factor_info": "", + "two_factor_info": "セキュリティ強化のため、メールアドレスとパスワードに加えて追加の認証層を設定します", "disable": "無効化", "reconfigure": "再設定", "reconfigure_two_factor_hint": "認証デバイスを更新する", "update_two_factor": "二要素認証の更新", - "update_two_factor_message": "", + "update_two_factor_message": "転送を続行すると、以前に設定された認証情報が無効になります", "update": "更新", "disable_two_factor": "二要素認証の無効化", - "disable_two_factor_message": "", + "disable_two_factor_message": "2要素認証を無効にしてもよろしいですか?", "export_data": "データをエクスポート", "select_folder": "フォルダを選択", - "select_zips": "", + "select_zips": "Zipを選択", "faq": "FAQ", - "takeout_hint": "", + "takeout_hint": "すべてのzipを一つのフォルダに展開してアップロードします。または直接zipをアップロードします。詳細についてはFAQを参照してください。", "destination": "宛先", "start": "スタート", - "last_export_time": "", + "last_export_time": "最終エクスポート日時", "export_again": "再同期", "LOCAL_STORAGE_NOT_ACCESSIBLE_MESSAGE": "ブラウザまたはアドオンがEnteのローカルストレージへのデータ保存をブロックしています", "email_already_taken": "メールアドレスはすでに使用されています", - "ETAGS_BLOCKED": "", - "LIVE_PHOTOS_DETECTED": "", - "RETRY_FAILED": "", + "ETAGS_BLOCKED": "ブラウザまたはアドオンが eTags を使用して大きなファイルをアップロードすることを妨げています。", + "LIVE_PHOTOS_DETECTED": "Live Photos の写真とビデオファイルが1つのファイルに統合されました", + "RETRY_FAILED": "失敗したアップロードを再試行する", "FAILED_UPLOADS": "アップロード失敗 ", - "failed_uploads_hint": "", - "SKIPPED_FILES": "", - "THUMBNAIL_GENERATION_FAILED_UPLOADS": "", - "UNSUPPORTED_FILES": "", - "SUCCESSFUL_UPLOADS": "", - "SKIPPED_INFO": "", - "UNSUPPORTED_INFO": "", - "BLOCKED_UPLOADS": "", - "INPROGRESS_UPLOADS": "", - "TOO_LARGE_UPLOADS": "", - "LARGER_THAN_AVAILABLE_STORAGE_UPLOADS": "", - "LARGER_THAN_AVAILABLE_STORAGE_INFO": "", - "TOO_LARGE_INFO": "", - "THUMBNAIL_GENERATION_FAILED_INFO": "", + "failed_uploads_hint": "アップロードが完了すると、これらを再試行するオプションがあります", + "SKIPPED_FILES": "無視されたアップロード", + "THUMBNAIL_GENERATION_FAILED_UPLOADS": "サムネイルの生成に失敗しました", + "UNSUPPORTED_FILES": "サポートされないファイル", + "SUCCESSFUL_UPLOADS": "アップロード成功", + "SKIPPED_INFO": "同じアルバムに一致する名前と内容を持つファイルがあるため、これらをスキップしました", + "UNSUPPORTED_INFO": "Enteはこれらのファイル形式をサポートしていません", + "BLOCKED_UPLOADS": "アップロードがブロックされました", + "INPROGRESS_UPLOADS": "アップロード進行中", + "TOO_LARGE_UPLOADS": "ファイルサイズが大きい", + "LARGER_THAN_AVAILABLE_STORAGE_UPLOADS": "ストレージ容量が足りません", + "LARGER_THAN_AVAILABLE_STORAGE_INFO": "ストレージプランの最大サイズ制限を超えているため、これらのファイルはアップロードされませんでした", + "TOO_LARGE_INFO": "ファイルサイズの上限を超えているため、これらのファイルはアップロードされませんでした", + "THUMBNAIL_GENERATION_FAILED_INFO": "これらのファイルはアップロードされましたが、残念ながらサムネイルを生成できませんでした。", "upload_to_album": "アルバムにアップロード", "add_to_album": "アルバムに追加", "move_to_album": "アルバムに移動", @@ -344,66 +343,66 @@ "empty_trash": "ごみ箱を空にする", "empty_trash_title": "ごみ箱を空にしますか?", "empty_trash_message": "これらのファイルはEnteアカウントから完全に削除されます。", - "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": "", + "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:DateTimeOriginal", + "exif_date_time_digitized": "Exif:DateTimeDigitized", "exif_metadata_date": "Exif:メタデータ日付", - "custom_time": "", + "custom_time": "カスタムタイム", "caption_character_limit": "最大5000文字", "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": "", + "add_collaborators": "コラボレーターを追加", + "add_new_email": "新しいメールアドレスを追加", + "shared_with_people_count_zero": "特定の人と共有", + "shared_with_people_count_one": "1人と共有", + "shared_with_people_count": "{{count, number}} 人と共有", + "participants_count_zero": "参加者がいません", + "participants_count_one": "1人の参加者", + "participants_count": "{{count, number}} 人の参加者", + "add_viewers": "ビューアーを追加", + "change_permission_to_viewer": "{{selectedEmail}} は写真をアルバムに追加できなくなります※{user} が追加した写真は今後も{user} が削除できます", + "change_permission_to_collaborator": "{{selectedEmail}} はアルバムに写真を追加できるようになります", + "change_permission_title": "権限を変更しますか?", + "confirm_convert_to_viewer": "はい、ビューアに変換します", + "confirm_convert_to_collaborator": "はい、共同編集者に変換します", + "manage": "管理", + "added_as": "追加:", + "collaborator_hint": "コラボレーターは共有アルバムに写真やビデオを追加できます", + "remove_participant": "参加者を削除", + "remove_participant_title": "削除しますか?", + "remove_participant_message": "

{{selectedEmail}} はアルバムから削除されます。

また、そのユーザーが追加した写真もアルバムから削除されます。

", + "confirm_remove": "はい、削除します", "owner": "オーナー", "collaborators": "コラボレーター", "viewers": "ビューアー", "add_more": "さらに追加", - "or_add_existing": "", + "or_add_existing": "または既存のものを選択", "NOT_FOUND": "404 - 見つかりません", - "link_expired": "", - "link_expired_message": "", - "manage_link": "", - "link_request_limit_exceeded": "", + "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": "", + "allow_adding_photos": "写真の追加を許可", + "allow_adding_photos_hint": "リンクを持つ人が共有アルバムに写真を追加できるようにします。", + "device_limit": "デバイスの制限", + "none": "なし", + "link_expiry": "リンクの期限切れ", + "never": "なし", "after_time": { "hour": "1時間後", "day": "1日後", @@ -420,8 +419,8 @@ "public_link_enabled": "公開リンクが有効です", "collect_photos": "写真を収集する", "disable_file_download": "ダウンロードを無効化", - "disable_file_download_message": "", - "shared_using": "", + "disable_file_download_message": "

ファイルのダウンロードボタンを無効化しますか?

閲覧者は、スクリーンショットを撮ったり、外部ツールを使用して写真を保存したりすることが可能です。

", + "shared_using": "{{url}} を使用して共有", "sharing_referral_code": "コード {{referralCode}} を使用して、10 GB の空き領域を入手", "live_photo_indicator": "LIVE", "disable_password": "パスワードロックを無効化する", @@ -430,17 +429,17 @@ "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": "", + "google_takeout": "Google から取り出し", + "deduplicate_files": "重複ファイル", + "remove_duplicates": "重複を削除", + "total_size": "合計サイズ", + "count": "カウント", + "deselect_all": "選択解除", + "no_duplicates": "重複はありません", + "duplicate_group_description": "{{count}} 個のアイテム、 {{itemSize}}", + "remove_duplicates_button_count": "{{count, number}} 件を削除", + "stop_uploads_title": "アップロードを中止しますか?", + "stop_uploads_message": "進行中のすべてのアップロードを中止してもよろしいですか?", "yes_stop_uploads": "はい、アップロードを中止します", "stop_downloads_title": "ダウンロードを中止しますか?", "stop_downloads_message": "進行中のすべてのダウンロードを停止してもよろしいですか?", @@ -458,49 +457,49 @@ "upgrade_now": "今すぐアップグレード", "renew_now": "今すぐ更新", "storage": "ストレージ", - "used": "使用中", + "used": "used", "you": "あなた", "family": "ファミリー", "free": "free", "of": "of", - "watch_folders": "", - "watched_folders": "", - "no_folders_added": "", - "watch_folders_hint_1": "", - "watch_folders_hint_2": "", - "watch_folders_hint_3": "", + "watch_folders": "監視フォルダ", + "watched_folders": "監視中のフォルダ", + "no_folders_added": "フォルダはまだ追加されていません", + "watch_folders_hint_1": "ここで追加したフォルダは自動的に監視されます", + "watch_folders_hint_2": "Ente に新しいファイルをアップロード", + "watch_folders_hint_3": "削除されたファイルをEnteから削除", "add_folder": "フォルダを追加", "stop_watching": "ウォッチを停止", - "stop_watching_folder_title": "", - "stop_watching_folder_message": "", + "stop_watching_folder_title": "フォルダの監視を停止しますか?", + "stop_watching_folder_message": "既存のファイルは削除されませんが、Enteはこのフォルダ変更にリンクしたEnteアルバムへの自動更新を停止します。", "yes_stop": "はい、停止します", "change_folder": "フォルダを変更", - "view_logs": "", - "view_logs_message": "", - "weak_device_hint": "", - "drag_and_drop_hint": "", + "view_logs": "ログを表示", + "view_logs_message": "

デバッグログが表示されますので、問題を解決するためにメールでお問い合わせください。

問題の特定追跡のため、ファイル名が含まれることに注意してください。

", + "weak_device_hint": "使用しているWebブラウザは、写真を暗号化するのに十分な強力ではありません。 パソコンでEnteにログインするか、Enteモバイル/デスクトップアプリをダウンロードしてください。", + "drag_and_drop_hint": "または、Ente ウィンドウにドラッグ&ドロップします", "authenticate": "認証", - "uploaded_to_single_collection": "", - "uploaded_to_separate_collections": "", - "nevermind": "", + "uploaded_to_single_collection": "1つのコレクションにアップロードしました", + "uploaded_to_separate_collections": "別々のコレクションにアップロードしました", + "nevermind": "気にしない", "update_available": "アップデートがあります", "update_installable_message": "Enteの新しいバージョンをインストールする準備ができました。", "install_now": "今すぐインストール", "install_on_next_launch": "次回起動時にインストール", - "update_available_message": "", + "update_available_message": "Enteの新しいバージョンがリリースされましたが、自動的にダウンロードおよびインストールすることはできません。", "download_and_install": "ダウンロードしてインストール", "ignore_this_version": "このバージョンを無視する", - "TODAY": "今日", - "YESTERDAY": "昨日", + "today": "今日", + "yesterday": "昨日", "enter_name": "名前を入力してください", "uploader_name_hint": "友達がこの素晴らしい写真に感謝する相手を知るために、名前を追加してください!", "name_placeholder": "名前…", - "root_level_file_with_folder_not_allowed": "", - "root_level_file_with_folder_not_allowed_message": "", + "root_level_file_with_folder_not_allowed": "ファイル/フォルダのミックスからアルバムを作成できません", + "root_level_file_with_folder_not_allowed_message": "

ファイルとフォルダが混在した状態でドラッグ&ドロップされました。

別々のアルバムを作成するオプションを選択する場合は、ファイルのみ、またはフォルダのみを選択してください。

", "more_details": "さらに詳細を表示", "ml_search": "機械学習", - "ml_search_description": "", - "ml_search_footnote": "", + "ml_search_description": "Enteは顔認識、マジック検索、その他の高度な検索機能のため、あなたのデバイス上で機械学習をしています", + "ml_search_footnote": "マジック検索では、「車」、「赤い車」、「フェラーリ」などの写真に写っている内容で写真を検索できます", "indexing": "インデックス中", "processed": "処理完了", "indexing_status_running": "実行中", @@ -511,17 +510,17 @@ "ml_search_disable_confirm": "すべてのデバイスで機械学習を無効にしますか?", "ml_consent": "機械学習を有効にする", "ml_consent_title": "機械学習を有効にしますか?", - "ml_consent_description": "", - "ml_consent_confirmation": "", + "ml_consent_description": "

機械学習を有効にすると、Ente はファイル(共有されたものを含む)から顔の形状などの情報を抽出します。

これはお使いのデバイス上で行われ、生成された生体情報はエンドツーエンドで暗号化されます。

この機能の詳細については、プライバシーポリシーをご覧ください

", + "ml_consent_confirmation": "理解して、機械学習を有効にします", "labs": "Labs", "passphrase_strength_weak": "パスワード強度: 弱い", "passphrase_strength_moderate": "パスワード強度: 普通", "passphrase_strength_strong": "パスワード強度: 強い", "preferences": "設定", "language": "言語", - "advanced": "アドバンスド", + "advanced": "高度な設定", "export_directory_does_not_exist": "無効なエクスポートディレクトリ", - "export_directory_does_not_exist_message": "", + "export_directory_does_not_exist_message": "

選択したエクスポートディレクトリは存在しません。

有効なディレクトリを選択してください。

", "storage_unit": { "b": "B", "kb": "KB", @@ -529,15 +528,15 @@ "gb": "GB", "tb": "TB" }, - "STOP_EXPORT": "中止", - "EXPORT_PROGRESS": "", - "MIGRATING_EXPORT": "準備しています…", - "RENAMING_COLLECTION_FOLDERS": "", - "TRASHING_DELETED_FILES": "削除ファイルをごみ箱に移動中…", - "TRASHING_DELETED_COLLECTIONS": "削除アルバムをごみ箱に移動中…", - "CONTINUOUS_EXPORT": "", - "PENDING_ITEMS": "保留中アイテム", - "EXPORT_STARTING": "エクスポートを開始中…", + "stop": "中止", + "sync_continuously": "継続的に同期", + "export_starting": "エクスポートを開始中…", + "preparing": "準備しています…", + "renaming_album_folders": "アルバムフォルダの名前を変更しています…", + "trashing_deleted_files": "削除ファイルをごみ箱に移動中…", + "trashing_deleted_albums": "削除アルバムをごみ箱に移動中…", + "export_progress": "{{progress.success, number}} / {{progress.total, number}} アイテムが同期されました", + "pending_items": "保留中アイテム", "delete_account_reason_label": "アカウントを削除する主な理由は何ですか?", "delete_account_reason_placeholder": "理由を選択", "delete_reason": { @@ -551,13 +550,13 @@ "delete_account_confirm_checkbox_label": "はい、アカウントとすべてのアプリのデータを削除します", "delete_account_confirm": "アカウントの削除に同意", "delete_account_confirm_message": "

このアカウントは他のEnteアプリも使用している場合はそれらにも紐づけされています。

\n

すべてのEnteアプリでアップロードされたデータは削除され、アカウントは完全に削除されます。

", - "feedback_required": "", - "feedback_required_found_another_service": "", - "recover_two_factor": "", - "at": "", - "auth_next": "", - "auth_download_mobile_app": "", - "no_codes_added_yet": "", + "feedback_required": "よければ、情報をお寄せください", + "feedback_required_found_another_service": "他のサービスの方が良いのは何ですか?", + "recover_two_factor": "2要素認証を回復", + "at": "at", + "auth_next": "次へ", + "auth_download_mobile_app": "シークレットを管理するには、モバイルアプリをダウンロードしてください", + "no_codes_added_yet": "コードはまだ追加されていません", "hide": "非表示", "unhide": "再表示", "sort_by": "Sort by", @@ -565,21 +564,21 @@ "oldest_first": "古い順", "pin_album": "アルバムをピン留め", "unpin_album": "アルバムのピン留めを解除", - "unpreviewable_file_notification": "", + "unpreviewable_file_notification": "このファイルはプレビューできませんでした。オリジナルをダウンロードするにはここをクリックしてください。", "download_complete": "ダウンロード完了", "downloading_album": "{{name}} をダウンロード中", "download_failed": "ダウンロードに失敗しました", "download_progress": "{{count, number}} / {{total, number}}ファイル", - "CHRISTMAS": "クリスマス", - "CHRISTMAS_EVE": "クリスマスイブ", - "NEW_YEAR": "新年", - "NEW_YEAR_EVE": "大晦日", + "christmas": "クリスマス", + "christmas_eve": "クリスマスイブ", + "new_year": "新年", + "new_year_eve": "大晦日", "image": "Image", "video": "Video", - "live_photo": "", + "live_photo": "ライブフォト", "photo_editor": "フォトエディター", - "confirm_editor_close": "", - "confirm_editor_close_message": "", + "confirm_editor_close": "エディタを閉じてもよろしいですか?", + "confirm_editor_close_message": "編集した画像をダウンロードするか、コピーをEnteに保存して変更を保持します。", "brightness": "明るさ", "contrast": "コントラスト", "saturation": "彩度", @@ -603,59 +602,59 @@ "colors": "色", "invert_colors": "色を反転", "reset": "リセット", - "faster_upload": "", - "faster_upload_description": "", - "open_ente_on_startup": "", - "cast_album_to_tv": "", - "enter_cast_pin_code": "", + "faster_upload": "アップロードを高速化", + "faster_upload_description": "近くのサーバー経由でのアップロード", + "open_ente_on_startup": "開始時に Ente を開く", + "cast_album_to_tv": "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": "", + "tv_not_found": "TVが見つかりませんでした。入力したPINは正しいですか?", + "cast_auto_pair": "オートペアリング", + "cast_auto_pair_description": "自動ペアリングは Chromecast をサポートするデバイスでのみ動作します。", + "choose_device_from_browser": "ブラウザポップアップからキャスト対応のデバイスを選択します。", + "cast_auto_pair_failed": "Chromecast の自動ペアリングに失敗しました。もう一度やり直してください。", + "pair_with_pin": "PINを使ってペアリングする", + "pair_with_pin_description": "PINを使ってペアリングすると、どんなスクリーンで動作します。", + "visit_cast_url": "ペアリングしたいデバイスで {{url}} にアクセスしてください。", "passkeys": "パスキー", - "passkey_fetch_failed": "", + "passkey_fetch_failed": "パスキーを取得できません。", "manage_passkey": "パスキーの管理", "delete_passkey": "パスキーを削除", "delete_passkey_confirmation": "このパスキーを削除してもよろしいですか?この操作は元に戻せません。", "rename_passkey": "パスキーの名前を変更", "add_passkey": "パスキーを追加", "enter_passkey_name": "パスキーの名前を入力", - "passkeys_description": "", + "passkeys_description": "パスキーはEnteアカウントの最新かつ安全な第二要素です。利便性とセキュリティのためにデバイス上の生体認証を使用します。", "created_at": "作成日", "passkey_add_failed": "パスキーを追加できませんでした", "passkey_login_failed": "パスキーログインに失敗しました", "passkey_login_invalid_url": "ログインURLが不正です。", - "passkey_login_already_claimed_session": "", + "passkey_login_already_claimed_session": "このセッションはすでに検証されています。", "passkey_login_generic_error": "パスキーでログイン中にエラーが発生しました。", - "passkey_login_credential_hint": "", - "passkeys_not_supported": "", + "passkey_login_credential_hint": "パスキーが別のデバイスにある場合は、そのデバイスでこのページを開いて検証できます。", + "passkeys_not_supported": "このブラウザではパスキーはサポートされていません", "try_again": "再試行", "check_status": "ステータスの確認", - "passkey_login_instructions": "", - "passkey_login": "", - "totp_login": "", + "passkey_login_instructions": "ログインを続けるには、ブラウザの手順に従ってください。", + "passkey_login": "パスキーでログイン", + "totp_login": "TOTPでログイン", "passkey": "パスキー", - "passkey_verify_description": "", - "waiting_for_verification": "", - "verification_still_pending": "", - "passkey_verified": "", - "redirecting_back_to_app": "", - "redirect_close_instructions": "", - "redirect_again": "", + "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": "" + "theme": "テーマ", + "system": "システム", + "light": "ライト", + "dark": "ダーク" } diff --git a/web/packages/base/locales/km-KH/translation.json b/web/packages/base/locales/km-KH/translation.json index e6f4c65fd6..a429ed19f7 100644 --- a/web/packages/base/locales/km-KH/translation.json +++ b/web/packages/base/locales/km-KH/translation.json @@ -45,7 +45,6 @@ "create_albums": "", "enter_album_name": "", "close_key": "", - "enter_file_name": "", "close": "", "yes": "", "no": "", @@ -490,8 +489,8 @@ "update_available_message": "", "download_and_install": "", "ignore_this_version": "", - "TODAY": "", - "YESTERDAY": "", + "today": "", + "yesterday": "", "enter_name": "", "uploader_name_hint": "", "name_placeholder": "", @@ -529,15 +528,15 @@ "gb": "", "tb": "" }, - "STOP_EXPORT": "", - "EXPORT_PROGRESS": "", - "MIGRATING_EXPORT": "", - "RENAMING_COLLECTION_FOLDERS": "", - "TRASHING_DELETED_FILES": "", - "TRASHING_DELETED_COLLECTIONS": "", - "CONTINUOUS_EXPORT": "", - "PENDING_ITEMS": "", - "EXPORT_STARTING": "", + "stop": "", + "sync_continuously": "", + "export_starting": "", + "preparing": "", + "renaming_album_folders": "", + "trashing_deleted_files": "", + "trashing_deleted_albums": "", + "export_progress": "", + "pending_items": "", "delete_account_reason_label": "", "delete_account_reason_placeholder": "", "delete_reason": { @@ -570,10 +569,10 @@ "downloading_album": "", "download_failed": "", "download_progress": "", - "CHRISTMAS": "", - "CHRISTMAS_EVE": "", - "NEW_YEAR": "", - "NEW_YEAR_EVE": "", + "christmas": "", + "christmas_eve": "", + "new_year": "", + "new_year_eve": "", "image": "", "video": "", "live_photo": "", diff --git a/web/packages/base/locales/ko-KR/translation.json b/web/packages/base/locales/ko-KR/translation.json index 62a75eba4a..3840c135a1 100644 --- a/web/packages/base/locales/ko-KR/translation.json +++ b/web/packages/base/locales/ko-KR/translation.json @@ -45,7 +45,6 @@ "create_albums": "", "enter_album_name": "앨범 이름", "close_key": "닫기 (Esc)", - "enter_file_name": "파일 이름", "close": "닫기", "yes": "", "no": "아니오", @@ -250,7 +249,7 @@ "people_suggestions_empty": "", "info": "", "info_key": "", - "file_name": "", + "file_name": "파일 이름", "caption_placeholder": "", "location": "", "view_on_map": "", @@ -490,8 +489,8 @@ "update_available_message": "", "download_and_install": "", "ignore_this_version": "", - "TODAY": "", - "YESTERDAY": "", + "today": "", + "yesterday": "", "enter_name": "이름 입력", "uploader_name_hint": "친구들이 이 멋진 사진에 대해 고마워할 수 있도록 이름을 추가하세요!", "name_placeholder": "", @@ -529,15 +528,15 @@ "gb": "", "tb": "" }, - "STOP_EXPORT": "", - "EXPORT_PROGRESS": "", - "MIGRATING_EXPORT": "", - "RENAMING_COLLECTION_FOLDERS": "", - "TRASHING_DELETED_FILES": "", - "TRASHING_DELETED_COLLECTIONS": "", - "CONTINUOUS_EXPORT": "", - "PENDING_ITEMS": "", - "EXPORT_STARTING": "", + "stop": "", + "sync_continuously": "", + "export_starting": "", + "preparing": "", + "renaming_album_folders": "", + "trashing_deleted_files": "", + "trashing_deleted_albums": "", + "export_progress": "", + "pending_items": "", "delete_account_reason_label": "", "delete_account_reason_placeholder": "", "delete_reason": { @@ -570,10 +569,10 @@ "downloading_album": "", "download_failed": "", "download_progress": "", - "CHRISTMAS": "", - "CHRISTMAS_EVE": "", - "NEW_YEAR": "", - "NEW_YEAR_EVE": "", + "christmas": "", + "christmas_eve": "", + "new_year": "", + "new_year_eve": "", "image": "", "video": "", "live_photo": "", diff --git a/web/packages/base/locales/lt-LT/translation.json b/web/packages/base/locales/lt-LT/translation.json index 06a2e30217..4ebdfe19b7 100644 --- a/web/packages/base/locales/lt-LT/translation.json +++ b/web/packages/base/locales/lt-LT/translation.json @@ -45,7 +45,6 @@ "create_albums": "Kurti albumus", "enter_album_name": "Albumo pavadinimas", "close_key": "Užverti (Gr)", - "enter_file_name": "Failo pavadinimas", "close": "Užverti", "yes": "Taip", "no": "Ne", @@ -112,7 +111,7 @@ "password_changed_elsewhere": "Slaptažodis pakeistas kitur", "password_changed_elsewhere_message": "Prisijunkite iš naujo šiame įrenginyje, kad naudotumėte naują slaptažodį tapatybės nustatymui.", "go_back": "Eiti atgal", - "account": "", + "account": "Paskyra", "recovery_key": "Atkūrimo raktas", "do_this_later": "Daryti tai vėliau", "save_key": "Išsaugoti raktą", @@ -128,9 +127,9 @@ "no_recovery_key_message": "Dėl mūsų visapusio šifravimo protokolo pobūdžio jūsų duomenų negalima iššifruoti be slaptažodžio arba atkūrimo rakto", "no_two_factor_recovery_key_message": "Iš savo registruoto el. pašto adreso parašykite el. laišką adresu {{{emailID}}", "contact_support": "Susisiekti su palaikymo komanda", - "help": "", - "ente_help": "", - "blog": "", + "help": "Pagalba", + "ente_help": "„Ente“ pagalba", + "blog": "Tinklaraštis", "request_feature": "Prašyti funkcijos", "support": "Palaikymas", "cancel": "Atšaukti", @@ -475,8 +474,8 @@ "stop_watching_folder_message": "Jūsų esami failai nebus ištrinti, bet „Ente“ nebeatnaujins automatiškai susieto „Ente“ albumo, kai šiame aplanke bus atliekami pakeitimai.", "yes_stop": "Taip, stabdyti", "change_folder": "Keisti aplanką", - "view_logs": "", - "view_logs_message": "", + "view_logs": "Peržiūrėti žurnalus", + "view_logs_message": "

Tai parodys derinimo žurnalus, kuriuos galėsite išsiųsti mums el. laišku, kad padėtume išspręsti problemą.

Atkreipkite dėmesį, kad failų pavadinimai bus įtraukti, kad būtų lengviau atsekti problemas su konkrečiais failais.

", "weak_device_hint": "Interneto naršyklė, kurią naudojate, nėra pakankamai galinga, kad užšifruotų nuotraukas. Bandykite prisijungti prie „Ente“ kompiuteryje arba atsisiųskite „Ente“ mobiliąją / darbalaukio programą.", "drag_and_drop_hint": "Arba nutempkite į „Ente“ langą", "authenticate": "Nustatyti tapatybę", @@ -490,8 +489,8 @@ "update_available_message": "Išleista nauja „Ente“ versija, bet jos negalima automatiškai atsisiųsti ir įdiegti.", "download_and_install": "Atsisiųsti ir įdiegti", "ignore_this_version": "Ignoruoti šią versiją", - "TODAY": "Šiandien", - "YESTERDAY": "Vakar", + "today": "Šiandien", + "yesterday": "Vakar", "enter_name": "Įveskite vardą", "uploader_name_hint": "Pridėkite vardą, kad draugai žinotų, kam padėkoti už šias puikias nuotraukas.", "name_placeholder": "Pavadinimas...", @@ -529,15 +528,15 @@ "gb": "GB", "tb": "TB" }, - "STOP_EXPORT": "Stabdyti", - "EXPORT_PROGRESS": "{{progress.success, number}} / {{progress.total, number}} sinchronizuoti elementai", - "MIGRATING_EXPORT": "Ruošiama...", - "RENAMING_COLLECTION_FOLDERS": "Pervadinami albumų aplankai...", - "TRASHING_DELETED_FILES": "Ištuštiniami ištrinti failai...", - "TRASHING_DELETED_COLLECTIONS": "Ištuštiniami ištrinti albumai...", - "CONTINUOUS_EXPORT": "Sinchronizuoti nuolat", - "PENDING_ITEMS": "Laukiami elementai", - "EXPORT_STARTING": "Pradedamas eksportavimas...", + "stop": "Stabdyti", + "sync_continuously": "Sinchronizuoti nuolat", + "export_starting": "Pradedamas eksportavimas...", + "preparing": "Ruošiama...", + "renaming_album_folders": "Pervadinami albumų aplankai...", + "trashing_deleted_files": "Ištuštiniami ištrinti failai...", + "trashing_deleted_albums": "Ištuštiniami ištrinti albumai...", + "export_progress": "{{progress.success, number}} / {{progress.total, number}} sinchronizuoti elementai", + "pending_items": "Laukiami elementai", "delete_account_reason_label": "Kokia yra pagrindinė priežastis, dėl kurios ištrinate savo paskyrą?", "delete_account_reason_placeholder": "Pasirinkite priežastį", "delete_reason": { @@ -553,10 +552,10 @@ "delete_account_confirm_message": "

Ši paskyra susieta su kitomis „Ente“ programomis, jei jas naudojate.

Jūsų įkelti duomenys per visas „Ente“ programas bus planuojama ištrinti, o jūsų paskyra bus ištrinta negrįžtamai.

", "feedback_required": "Maloniai padėkite mums su šia informacija", "feedback_required_found_another_service": "Ką kita paslauga daro geriau?", - "recover_two_factor": "Atkurkite dvigubą tapatybės nustatymą", + "recover_two_factor": "Atkurti dvigubą tapatybės nustatymą", "at": " ", "auth_next": "sekantis", - "auth_download_mobile_app": "Atsisiųskite mūsų mobiliąją programą ir tvarkykite savo paslaptis", + "auth_download_mobile_app": "Atsisiųskite mūsų mobiliąją programą, kad tvarkytumėte savo paslaptis.", "no_codes_added_yet": "Kol kas nėra pridėtų kodų.", "hide": "Slėpti", "unhide": "Neslėpti", @@ -570,10 +569,10 @@ "downloading_album": "Atsisiunčiama {{name}}", "download_failed": "Atsisiuntimas nepavyko.", "download_progress": "{{count, number}} / {{total, number}} failai", - "CHRISTMAS": "Kalėdos", - "CHRISTMAS_EVE": "Kūčios", - "NEW_YEAR": "Naujieji metai", - "NEW_YEAR_EVE": "Naujųjų metų išvakarės", + "christmas": "Kalėdos", + "christmas_eve": "Kūčios", + "new_year": "Naujieji metai", + "new_year_eve": "Naujųjų metų išvakarės", "image": "Vaizdas", "video": "Vaizdo įrašas", "live_photo": "Gyva nuotrauka", @@ -604,8 +603,8 @@ "invert_colors": "Invertuoti spalvas", "reset": "Atkurti", "faster_upload": "Spartesni įkėlimai", - "faster_upload_description": "Nukreipkite įkėlimus per netoliese esančius serverius", - "open_ente_on_startup": "", + "faster_upload_description": "Nukreipkite įkėlimus per netoliese esančius serverius.", + "open_ente_on_startup": "Atverti „Ente“ paleidžiant", "cast_album_to_tv": "Paleisti albumą televizoriuje", "enter_cast_pin_code": "Įveskite žemiau esančiame televizoriuje matomą kodą, kad susietumėte šį įrenginį.", "code": "Kodas", @@ -632,7 +631,7 @@ "passkey_login_failed": "Nepavyko slaptarakčio prisijungimas", "passkey_login_invalid_url": "Prisijungimo URL netinkamas.", "passkey_login_already_claimed_session": "Šis seansas jau patvirtintas.", - "passkey_login_generic_error": "Įvyko klaida prisijungiant su slaptarakčiu.", + "passkey_login_generic_error": "Prisijungiant su slaptarakčiu įvyko klaida.", "passkey_login_credential_hint": "Jei slaptarakčiai yra kitame įrenginyje, galite atverti šį puslapį tame įrenginyje ir patvirtinti.", "passkeys_not_supported": "Šioje naršyklėje nepalaikomi slaptarakčiai.", "try_again": "Bandyti dar kartą", diff --git a/web/packages/base/locales/ml-IN/translation.json b/web/packages/base/locales/ml-IN/translation.json index e6f4c65fd6..a429ed19f7 100644 --- a/web/packages/base/locales/ml-IN/translation.json +++ b/web/packages/base/locales/ml-IN/translation.json @@ -45,7 +45,6 @@ "create_albums": "", "enter_album_name": "", "close_key": "", - "enter_file_name": "", "close": "", "yes": "", "no": "", @@ -490,8 +489,8 @@ "update_available_message": "", "download_and_install": "", "ignore_this_version": "", - "TODAY": "", - "YESTERDAY": "", + "today": "", + "yesterday": "", "enter_name": "", "uploader_name_hint": "", "name_placeholder": "", @@ -529,15 +528,15 @@ "gb": "", "tb": "" }, - "STOP_EXPORT": "", - "EXPORT_PROGRESS": "", - "MIGRATING_EXPORT": "", - "RENAMING_COLLECTION_FOLDERS": "", - "TRASHING_DELETED_FILES": "", - "TRASHING_DELETED_COLLECTIONS": "", - "CONTINUOUS_EXPORT": "", - "PENDING_ITEMS": "", - "EXPORT_STARTING": "", + "stop": "", + "sync_continuously": "", + "export_starting": "", + "preparing": "", + "renaming_album_folders": "", + "trashing_deleted_files": "", + "trashing_deleted_albums": "", + "export_progress": "", + "pending_items": "", "delete_account_reason_label": "", "delete_account_reason_placeholder": "", "delete_reason": { @@ -570,10 +569,10 @@ "downloading_album": "", "download_failed": "", "download_progress": "", - "CHRISTMAS": "", - "CHRISTMAS_EVE": "", - "NEW_YEAR": "", - "NEW_YEAR_EVE": "", + "christmas": "", + "christmas_eve": "", + "new_year": "", + "new_year_eve": "", "image": "", "video": "", "live_photo": "", diff --git a/web/packages/base/locales/nl-NL/translation.json b/web/packages/base/locales/nl-NL/translation.json index 4bbe05b76d..981d447c86 100644 --- a/web/packages/base/locales/nl-NL/translation.json +++ b/web/packages/base/locales/nl-NL/translation.json @@ -45,7 +45,6 @@ "create_albums": "Albums aanmaken", "enter_album_name": "Albumnaam", "close_key": "Sluiten (Esc)", - "enter_file_name": "Bestandsnaam", "close": "Sluiten", "yes": "Ja", "no": "Nee", @@ -112,7 +111,7 @@ "password_changed_elsewhere": "Wachtwoord elders gewijzigd", "password_changed_elsewhere_message": "Log opnieuw in op dit apparaat om uw nieuwe wachtwoord te gebruiken om te verifiëren.", "go_back": "Ga terug", - "account": "", + "account": "Account", "recovery_key": "Herstelsleutel", "do_this_later": "Doe dit later", "save_key": "Sleutel opslaan", @@ -128,9 +127,9 @@ "no_recovery_key_message": "Door de aard van ons end-to-end encryptieprotocol kunnen je gegevens niet worden ontsleuteld zonder je wachtwoord of herstelsleutel", "no_two_factor_recovery_key_message": "Stuur een e-mail naar {{emailID}} vanaf het door jou geregistreerde e-mailadres", "contact_support": "Klantenservice", - "help": "", - "ente_help": "", - "blog": "", + "help": "Hulp", + "ente_help": "Ente Hulp", + "blog": "Blog", "request_feature": "Vraag nieuwe functie aan", "support": "Ondersteuning", "cancel": "Annuleren", @@ -205,11 +204,11 @@ "delete_photos": "Foto's verwijderen", "keep_photos": "Foto's behouden", "share_album": "Album delen", - "sharing_with_self": "", - "sharing_already_shared": "", + "sharing_with_self": "Je kunt niet met jezelf delen", + "sharing_already_shared": "Je deelt dit al met {{email}}", "sharing_album_not_allowed": "Album delen niet toegestaan", "sharing_disabled_for_free_accounts": "Delen is uitgeschakeld voor gratis accounts", - "sharing_user_does_not_exist": "", + "sharing_user_does_not_exist": "Kan geen gebruiker met dat e-mailadres vinden", "search": "Zoeken", "search_results": "Zoekresultaten", "no_results": "Geen resultaten gevonden", @@ -475,8 +474,8 @@ "stop_watching_folder_message": "Uw bestaande bestanden zullen niet worden verwijderd, maar Ente stopt met het automatisch bijwerken van het gekoppelde Ente album bij wijzigingen in deze map.", "yes_stop": "Ja, stop", "change_folder": "Map wijzigen", - "view_logs": "", - "view_logs_message": "", + "view_logs": "Bekijk logs", + "view_logs_message": "

Dit zal logboeken tonen, die je ons kunt e-mailen om te helpen bij jouw probleem.

Hou er rekening mee dat bestandsnamen worden inbegrepen om problemen met specifieke bestanden bij te houden.

", "weak_device_hint": "De webbrowser die u gebruikt is niet krachtig genoeg om uw foto's te versleutelen. Probeer in te loggen op uw computer, of download de Ente mobiel/desktop app.", "drag_and_drop_hint": "Of sleep en plaats in het Ente venster", "authenticate": "Verifiëren", @@ -490,8 +489,8 @@ "update_available_message": "Er is een nieuwe versie van Ente vrijgegeven, maar deze kan niet automatisch worden gedownload en geïnstalleerd.", "download_and_install": "Downloaden en installeren", "ignore_this_version": "Negeer deze versie", - "TODAY": "Vandaag", - "YESTERDAY": "Gisteren", + "today": "Vandaag", + "yesterday": "Gisteren", "enter_name": "Naam invoeren", "uploader_name_hint": "Voeg een naam toe zodat je vrienden weten wie ze moeten bedanken voor deze geweldige foto's!", "name_placeholder": "Naam...", @@ -529,15 +528,15 @@ "gb": "GB", "tb": "TB" }, - "STOP_EXPORT": "Stoppen", - "EXPORT_PROGRESS": "{{progress.success}} / {{progress.total}} bestanden geëxporteerd", - "MIGRATING_EXPORT": "Voorbereiden...", - "RENAMING_COLLECTION_FOLDERS": "Albumnamen hernoemen...", - "TRASHING_DELETED_FILES": "Verwijderde bestanden naar prullenbak...", - "TRASHING_DELETED_COLLECTIONS": "Verwijderde albums naar prullenbak...", - "CONTINUOUS_EXPORT": "Continue synchroniseren", - "PENDING_ITEMS": "Bestanden in behandeling", - "EXPORT_STARTING": "Exporteren begonnen...", + "stop": "Stoppen", + "sync_continuously": "Continue synchroniseren", + "export_starting": "Exporteren begonnen...", + "preparing": "Voorbereiden...", + "renaming_album_folders": "Albumnamen hernoemen...", + "trashing_deleted_files": "Verwijderde bestanden naar prullenbak...", + "trashing_deleted_albums": "Verwijderde albums naar prullenbak...", + "export_progress": "{{progress.success}} / {{progress.total}} bestanden geëxporteerd", + "pending_items": "Bestanden in behandeling", "delete_account_reason_label": "Wat is de belangrijkste reden waarom je jouw account verwijdert?", "delete_account_reason_placeholder": "Kies een reden", "delete_reason": { @@ -570,10 +569,10 @@ "downloading_album": "{{name}} downloaden", "download_failed": "Download mislukt", "download_progress": "{{count, number}} / {{total, number}} bestanden", - "CHRISTMAS": "Kerst", - "CHRISTMAS_EVE": "Kerstavond", - "NEW_YEAR": "Nieuwjaar", - "NEW_YEAR_EVE": "Oudjaarsavond", + "christmas": "Kerst", + "christmas_eve": "Kerstavond", + "new_year": "Nieuwjaar", + "new_year_eve": "Oudjaarsavond", "image": "Afbeelding", "video": "Video", "live_photo": "Live foto", @@ -605,7 +604,7 @@ "reset": "Herstellen", "faster_upload": "Snellere uploads", "faster_upload_description": "Uploaden door nabije servers", - "open_ente_on_startup": "", + "open_ente_on_startup": "Open Ente tijdens opstarten", "cast_album_to_tv": "Album afspelen op TV", "enter_cast_pin_code": "Voer de code in die u op de TV ziet om dit apparaat te koppelen.", "code": "Code", @@ -654,8 +653,8 @@ "server_endpoint": "Server eindpunt", "more_information": "Meer informatie", "save": "Opslaan", - "theme": "", - "system": "", - "light": "", - "dark": "" + "theme": "Thema", + "system": "Systeem", + "light": "Licht", + "dark": "Donker" } diff --git a/web/packages/base/locales/pl-PL/translation.json b/web/packages/base/locales/pl-PL/translation.json index 7c4e388db7..3980083caa 100644 --- a/web/packages/base/locales/pl-PL/translation.json +++ b/web/packages/base/locales/pl-PL/translation.json @@ -45,7 +45,6 @@ "create_albums": "Utwórz albumy", "enter_album_name": "Nazwa albumu", "close_key": "Zamknij (Esc)", - "enter_file_name": "Nazwa pliku", "close": "Zamknij", "yes": "Tak", "no": "Nie", @@ -490,8 +489,8 @@ "update_available_message": "Nowa wersja Ente została wydana, ale nie może być automatycznie pobrana i zainstalowana.", "download_and_install": "Pobierz i zainstaluj", "ignore_this_version": "Ignoruj tę wersję", - "TODAY": "Dzisiaj", - "YESTERDAY": "Wczoraj", + "today": "Dzisiaj", + "yesterday": "Wczoraj", "enter_name": "Wprowadź nazwę", "uploader_name_hint": "Dodaj imię, aby Twoi znajomi wiedzieli, kto będzie mógł podziękować za te wspaniałe zdjęcia!", "name_placeholder": "Nazwa...", @@ -529,15 +528,15 @@ "gb": "GB", "tb": "TB" }, - "STOP_EXPORT": "Zatrzymaj", - "EXPORT_PROGRESS": "{{progress.success, number}} / {{progress.total, number}} elementy zsynchronizowane", - "MIGRATING_EXPORT": "Przygotowywanie...", - "RENAMING_COLLECTION_FOLDERS": "Zmienianie nazwy folderów albumów...", - "TRASHING_DELETED_FILES": "Przenoszenie usuniętych plików do kosza...", - "TRASHING_DELETED_COLLECTIONS": "Przenoszenie usuniętych albumów do kosza...", - "CONTINUOUS_EXPORT": "Synchronizuj ciągle", - "PENDING_ITEMS": "Oczekujące elementy", - "EXPORT_STARTING": "Rozpoczynanie eksportu...", + "stop": "Zatrzymaj", + "sync_continuously": "Synchronizuj ciągle", + "export_starting": "Rozpoczynanie eksportu...", + "preparing": "Przygotowywanie...", + "renaming_album_folders": "Zmienianie nazwy folderów albumów...", + "trashing_deleted_files": "Przenoszenie usuniętych plików do kosza...", + "trashing_deleted_albums": "Przenoszenie usuniętych albumów do kosza...", + "export_progress": "{{progress.success, number}} / {{progress.total, number}} elementy zsynchronizowane", + "pending_items": "Oczekujące elementy", "delete_account_reason_label": "Jaka jest główna przyczyna usunięcia Twojego konta?", "delete_account_reason_placeholder": "Wybierz powód", "delete_reason": { @@ -570,10 +569,10 @@ "downloading_album": "Pobieranie {{name}}", "download_failed": "Pobieranie nie powiodło się", "download_progress": "{{count, number}} / {{total, number}} plików", - "CHRISTMAS": "Boże Narodzenie", - "CHRISTMAS_EVE": "Wigilia", - "NEW_YEAR": "Nowy Rok", - "NEW_YEAR_EVE": "Sylwester", + "christmas": "Boże Narodzenie", + "christmas_eve": "Wigilia", + "new_year": "Nowy Rok", + "new_year_eve": "Sylwester", "image": "Zdjęcie", "video": "Wideo", "live_photo": "Live Photo", diff --git a/web/packages/base/locales/pt-BR/translation.json b/web/packages/base/locales/pt-BR/translation.json index 62bd0e65e5..03c478813d 100644 --- a/web/packages/base/locales/pt-BR/translation.json +++ b/web/packages/base/locales/pt-BR/translation.json @@ -45,7 +45,6 @@ "create_albums": "Criar álbuns", "enter_album_name": "Nome do álbum", "close_key": "Fechar (Esc)", - "enter_file_name": "Nome do arquivo", "close": "Fechar", "yes": "Sim", "no": "Não", @@ -129,7 +128,7 @@ "no_two_factor_recovery_key_message": "Envie um e-mail a {{emailID}} do endereço de e-mail registrado", "contact_support": "Entrar em contato conosco", "help": "Ajuda", - "ente_help": "Ajuda Ente", + "ente_help": "Ente Ajuda", "blog": "Blog", "request_feature": "Solicitar recurso", "support": "Suporte", @@ -364,7 +363,7 @@ "exif_metadata_date": "Exif:DataMetadados", "custom_time": "Horário personalizado", "caption_character_limit": "Máximo de 5.000 caracteres", - "sharing_details": "Detalhes de compartilhamento", + "sharing_details": "Detalhes do compartilhamento", "modify_sharing": "Modificar compartilhamento", "add_collaborators": "Adicionar colaboradores", "add_new_email": "Adicionar um novo e-mail", @@ -392,7 +391,7 @@ "viewers": "Visualizadores", "add_more": "Adicionar mais", "or_add_existing": "Ou escolher existente", - "NOT_FOUND": "404 Página não encontrada", + "NOT_FOUND": "404 - Não encontrado", "link_expired": "Link expirado", "link_expired_message": "Este link expirou-se ou foi desativado", "manage_link": "Gerenciar link", @@ -430,7 +429,7 @@ "lock": "Bloquear", "file": "Arquivo", "folder": "Pasta", - "google_takeout": "Google Takeout", + "google_takeout": "Google takeout", "deduplicate_files": "Desduplicar arquivos", "remove_duplicates": "Excluir duplicados", "total_size": "Tamanho total", @@ -455,7 +454,7 @@ "enter_two_factor_otp": "Insira o código de 6 dígitos do aplicativo de autenticação.", "create_account": "Criar conta", "copied": "Copiado", - "upgrade_now": "Aprimorar agora", + "upgrade_now": "Melhorar agora", "renew_now": "Renovar agora", "storage": "Armazenamento", "used": "usado", @@ -490,8 +489,8 @@ "update_available_message": "Uma nova versão do ente foi lançada, mas não pode ser baixada e instalada automaticamente.", "download_and_install": "Baixar e instalar", "ignore_this_version": "Ignorar esta versão", - "TODAY": "Hoje", - "YESTERDAY": "Ontem", + "today": "Hoje", + "yesterday": "Ontem", "enter_name": "Inserir um nome", "uploader_name_hint": "Adicione um nome para que seus amigos saibam a quem agradecer por estas ótimas fotos!", "name_placeholder": "Nome...", @@ -529,15 +528,15 @@ "gb": "GB", "tb": "TB" }, - "STOP_EXPORT": "Parar", - "EXPORT_PROGRESS": "{{progress.success, number}} / {{progress.total, number}} itens sincronizados", - "MIGRATING_EXPORT": "Preparando...", - "RENAMING_COLLECTION_FOLDERS": "Renomeando pastas do álbum...", - "TRASHING_DELETED_FILES": "Descartando arquivos excluídos...", - "TRASHING_DELETED_COLLECTIONS": "Descartando álbuns excluídos...", - "CONTINUOUS_EXPORT": "Sincronizar continuamente", - "PENDING_ITEMS": "Itens pendentes", - "EXPORT_STARTING": "Iniciando a exportação...", + "stop": "Parar", + "sync_continuously": "Sincronizar continuamente", + "export_starting": "Exportação iniciada...", + "preparing": "Preparando...", + "renaming_album_folders": "Renomeando pastas do álbum...", + "trashing_deleted_files": "Descartando arquivos excluídos...", + "trashing_deleted_albums": "Descartando álbuns excluídos...", + "export_progress": "{{progress.success, number}} / {{progress.total, number}} itens sincronizados", + "pending_items": "Itens pendentes", "delete_account_reason_label": "Qual é o principal motivo para você excluir sua conta?", "delete_account_reason_placeholder": "Selecione um motivo", "delete_reason": { @@ -570,10 +569,10 @@ "downloading_album": "Fazendo download de {{name}}", "download_failed": "Falhou ao baixar", "download_progress": "{{count, number}} / {{total, number}} arquivos", - "CHRISTMAS": "Natal", - "CHRISTMAS_EVE": "Véspera de Natal", - "NEW_YEAR": "Ano Novo", - "NEW_YEAR_EVE": "Véspera de Ano Novo", + "christmas": "Natal", + "christmas_eve": "Véspera de Natal", + "new_year": "Ano Novo", + "new_year_eve": "Véspera de Ano Novo", "image": "Imagem", "video": "Vídeo", "live_photo": "Foto ao vivo", diff --git a/web/packages/base/locales/pt-PT/translation.json b/web/packages/base/locales/pt-PT/translation.json index 9de71e0551..55cb7a681a 100644 --- a/web/packages/base/locales/pt-PT/translation.json +++ b/web/packages/base/locales/pt-PT/translation.json @@ -45,7 +45,6 @@ "create_albums": "Criar álbuns", "enter_album_name": "Nome do álbum", "close_key": "Fechar (Esc)", - "enter_file_name": "Nome do ficheiro", "close": "Fechar", "yes": "Sim", "no": "Não", @@ -490,8 +489,8 @@ "update_available_message": "Foi lançada uma nova versão do Ente, mas não pode ser descarregada e instalada automaticamente.", "download_and_install": "Descarregar e instalar", "ignore_this_version": "Ignorar esta versão", - "TODAY": "Hoje", - "YESTERDAY": "Ontem", + "today": "Hoje", + "yesterday": "Ontem", "enter_name": "Introduza o nome", "uploader_name_hint": "Adicione um nome para que os seus amigos saibam a quem agradecer por estas ótimas fotos!", "name_placeholder": "Nome...", @@ -529,15 +528,15 @@ "gb": "GB", "tb": "TB" }, - "STOP_EXPORT": "Parar", - "EXPORT_PROGRESS": "{{progress.success, number}} / {{progress.total, number}} itens sincronizados", - "MIGRATING_EXPORT": "Preparar...", - "RENAMING_COLLECTION_FOLDERS": "Renomear pastas de álbuns...", - "TRASHING_DELETED_FILES": "Eliminar arquivos apagados...", - "TRASHING_DELETED_COLLECTIONS": "Eliminar álbuns apagados...", - "CONTINUOUS_EXPORT": "Sincronização contínua", - "PENDING_ITEMS": "Itens pendentes", - "EXPORT_STARTING": "Iniciar a exportação...", + "stop": "Parar", + "sync_continuously": "Sincronização contínua", + "export_starting": "Iniciar a exportação...", + "preparing": "Preparar...", + "renaming_album_folders": "Renomear pastas de álbuns...", + "trashing_deleted_files": "Eliminar arquivos apagados...", + "trashing_deleted_albums": "Eliminar álbuns apagados...", + "export_progress": "{{progress.success, number}} / {{progress.total, number}} itens sincronizados", + "pending_items": "Itens pendentes", "delete_account_reason_label": "Qual o principal motivo pelo qual está a eliminar a conta?", "delete_account_reason_placeholder": "Selecione um motivo", "delete_reason": { @@ -570,10 +569,10 @@ "downloading_album": "Fazer download de {{name}}", "download_failed": "Falha no download", "download_progress": "{{count, number}} / {{total, number}} arquivos", - "CHRISTMAS": "Natal", - "CHRISTMAS_EVE": "Véspera de Natal", - "NEW_YEAR": "Ano Novo", - "NEW_YEAR_EVE": "Véspera de Ano Novo", + "christmas": "Natal", + "christmas_eve": "Véspera de Natal", + "new_year": "Ano Novo", + "new_year_eve": "Véspera de Ano Novo", "image": "Imagem", "video": "Vídeo", "live_photo": "Fotos em movimento", diff --git a/web/packages/base/locales/ro-RO/translation.json b/web/packages/base/locales/ro-RO/translation.json index e6f4c65fd6..a429ed19f7 100644 --- a/web/packages/base/locales/ro-RO/translation.json +++ b/web/packages/base/locales/ro-RO/translation.json @@ -45,7 +45,6 @@ "create_albums": "", "enter_album_name": "", "close_key": "", - "enter_file_name": "", "close": "", "yes": "", "no": "", @@ -490,8 +489,8 @@ "update_available_message": "", "download_and_install": "", "ignore_this_version": "", - "TODAY": "", - "YESTERDAY": "", + "today": "", + "yesterday": "", "enter_name": "", "uploader_name_hint": "", "name_placeholder": "", @@ -529,15 +528,15 @@ "gb": "", "tb": "" }, - "STOP_EXPORT": "", - "EXPORT_PROGRESS": "", - "MIGRATING_EXPORT": "", - "RENAMING_COLLECTION_FOLDERS": "", - "TRASHING_DELETED_FILES": "", - "TRASHING_DELETED_COLLECTIONS": "", - "CONTINUOUS_EXPORT": "", - "PENDING_ITEMS": "", - "EXPORT_STARTING": "", + "stop": "", + "sync_continuously": "", + "export_starting": "", + "preparing": "", + "renaming_album_folders": "", + "trashing_deleted_files": "", + "trashing_deleted_albums": "", + "export_progress": "", + "pending_items": "", "delete_account_reason_label": "", "delete_account_reason_placeholder": "", "delete_reason": { @@ -570,10 +569,10 @@ "downloading_album": "", "download_failed": "", "download_progress": "", - "CHRISTMAS": "", - "CHRISTMAS_EVE": "", - "NEW_YEAR": "", - "NEW_YEAR_EVE": "", + "christmas": "", + "christmas_eve": "", + "new_year": "", + "new_year_eve": "", "image": "", "video": "", "live_photo": "", diff --git a/web/packages/base/locales/ru-RU/translation.json b/web/packages/base/locales/ru-RU/translation.json index 32ea0ccd82..95eb4d7fbb 100644 --- a/web/packages/base/locales/ru-RU/translation.json +++ b/web/packages/base/locales/ru-RU/translation.json @@ -45,7 +45,6 @@ "create_albums": "Создать альбомы", "enter_album_name": "Название альбома", "close_key": "Закрыть (Esc)", - "enter_file_name": "Имя файла", "close": "Закрыть", "yes": "Да", "no": "Нет", @@ -430,7 +429,7 @@ "lock": "Замок", "file": "Файл", "folder": "Папка", - "google_takeout": "Еда на вынос из Google", + "google_takeout": "Загрузить из Google", "deduplicate_files": "Проверить файлы на дубликаты", "remove_duplicates": "Удалить дубликаты", "total_size": "Общий размер", @@ -490,8 +489,8 @@ "update_available_message": "Была выпущена новая версия Ente, но она не может быть автоматически загружена и установлена.", "download_and_install": "Скачайте и установите", "ignore_this_version": "Игнорируйте эту версию", - "TODAY": "Сегодня", - "YESTERDAY": "Вчера", + "today": "Сегодня", + "yesterday": "Вчера", "enter_name": "Введите имя", "uploader_name_hint": "Добавьте имя, чтобы ваши друзья знали, кого благодарить за эти замечательные фотографии!", "name_placeholder": "Имя...", @@ -529,15 +528,15 @@ "gb": "Гб", "tb": "Терабайт" }, - "STOP_EXPORT": "Остановка", - "EXPORT_PROGRESS": "{{progress.success, number}} / {{progress.total, number}} синхронизированные элементы", - "MIGRATING_EXPORT": "Подготовка...", - "RENAMING_COLLECTION_FOLDERS": "Переименование папок альбомов...", - "TRASHING_DELETED_FILES": "Очистка удаленных файлов...", - "TRASHING_DELETED_COLLECTIONS": "Очистка удаленных альбомов...", - "CONTINUOUS_EXPORT": "Непрерывная синхронизация", - "PENDING_ITEMS": "Отложенные пункты", - "EXPORT_STARTING": "Запуск экспорта...", + "stop": "Остановка", + "sync_continuously": "Непрерывная синхронизация", + "export_starting": "Запуск экспорта...", + "preparing": "Подготовка...", + "renaming_album_folders": "Переименование папок альбомов...", + "trashing_deleted_files": "Очистка удаленных файлов...", + "trashing_deleted_albums": "Очистка удаленных альбомов...", + "export_progress": "{{progress.success, number}} / {{progress.total, number}} синхронизированные элементы", + "pending_items": "Отложенные пункты", "delete_account_reason_label": "Какова основная причина, по которой вы удаляете свою учетную запись?", "delete_account_reason_placeholder": "Выберите причину", "delete_reason": { @@ -570,10 +569,10 @@ "downloading_album": "Загрузка {{name}}", "download_failed": "Загрузка не удалась", "download_progress": "{{count, number}} / {{total, number}} файлов", - "CHRISTMAS": "Рождество", - "CHRISTMAS_EVE": "Канун Рождества", - "NEW_YEAR": "Новый год", - "NEW_YEAR_EVE": "Канун Нового года", + "christmas": "Рождество", + "christmas_eve": "Канун Рождества", + "new_year": "Новый год", + "new_year_eve": "Канун Нового года", "image": "Изображение", "video": "Видео", "live_photo": "Живое фото", diff --git a/web/packages/base/locales/sl-SI/translation.json b/web/packages/base/locales/sl-SI/translation.json index e6f4c65fd6..a429ed19f7 100644 --- a/web/packages/base/locales/sl-SI/translation.json +++ b/web/packages/base/locales/sl-SI/translation.json @@ -45,7 +45,6 @@ "create_albums": "", "enter_album_name": "", "close_key": "", - "enter_file_name": "", "close": "", "yes": "", "no": "", @@ -490,8 +489,8 @@ "update_available_message": "", "download_and_install": "", "ignore_this_version": "", - "TODAY": "", - "YESTERDAY": "", + "today": "", + "yesterday": "", "enter_name": "", "uploader_name_hint": "", "name_placeholder": "", @@ -529,15 +528,15 @@ "gb": "", "tb": "" }, - "STOP_EXPORT": "", - "EXPORT_PROGRESS": "", - "MIGRATING_EXPORT": "", - "RENAMING_COLLECTION_FOLDERS": "", - "TRASHING_DELETED_FILES": "", - "TRASHING_DELETED_COLLECTIONS": "", - "CONTINUOUS_EXPORT": "", - "PENDING_ITEMS": "", - "EXPORT_STARTING": "", + "stop": "", + "sync_continuously": "", + "export_starting": "", + "preparing": "", + "renaming_album_folders": "", + "trashing_deleted_files": "", + "trashing_deleted_albums": "", + "export_progress": "", + "pending_items": "", "delete_account_reason_label": "", "delete_account_reason_placeholder": "", "delete_reason": { @@ -570,10 +569,10 @@ "downloading_album": "", "download_failed": "", "download_progress": "", - "CHRISTMAS": "", - "CHRISTMAS_EVE": "", - "NEW_YEAR": "", - "NEW_YEAR_EVE": "", + "christmas": "", + "christmas_eve": "", + "new_year": "", + "new_year_eve": "", "image": "", "video": "", "live_photo": "", diff --git a/web/packages/base/locales/sv-SE/translation.json b/web/packages/base/locales/sv-SE/translation.json index cd4e2a4ef6..eca462ca5c 100644 --- a/web/packages/base/locales/sv-SE/translation.json +++ b/web/packages/base/locales/sv-SE/translation.json @@ -45,7 +45,6 @@ "create_albums": "Skapa album", "enter_album_name": "Albumnamn", "close_key": "Stäng (Esc)", - "enter_file_name": "Filnamn", "close": "Stäng", "yes": "Ja", "no": "Nej", @@ -490,8 +489,8 @@ "update_available_message": "", "download_and_install": "", "ignore_this_version": "", - "TODAY": "", - "YESTERDAY": "", + "today": "", + "yesterday": "", "enter_name": "Ange namn", "uploader_name_hint": "Lägg till ett namn så att dina vänner vet vem de ska tacka för dessa fantastiska bilder!", "name_placeholder": "Namn...", @@ -529,15 +528,15 @@ "gb": "GB", "tb": "TB" }, - "STOP_EXPORT": "", - "EXPORT_PROGRESS": "", - "MIGRATING_EXPORT": "", - "RENAMING_COLLECTION_FOLDERS": "", - "TRASHING_DELETED_FILES": "", - "TRASHING_DELETED_COLLECTIONS": "", - "CONTINUOUS_EXPORT": "", - "PENDING_ITEMS": "", - "EXPORT_STARTING": "", + "stop": "", + "sync_continuously": "", + "export_starting": "", + "preparing": "", + "renaming_album_folders": "", + "trashing_deleted_files": "", + "trashing_deleted_albums": "", + "export_progress": "", + "pending_items": "", "delete_account_reason_label": "", "delete_account_reason_placeholder": "", "delete_reason": { @@ -570,10 +569,10 @@ "downloading_album": "", "download_failed": "", "download_progress": "", - "CHRISTMAS": "", - "CHRISTMAS_EVE": "", - "NEW_YEAR": "", - "NEW_YEAR_EVE": "", + "christmas": "", + "christmas_eve": "", + "new_year": "", + "new_year_eve": "", "image": "Bild", "video": "", "live_photo": "", diff --git a/web/packages/base/locales/ta-IN/translation.json b/web/packages/base/locales/ta-IN/translation.json index e6f4c65fd6..a429ed19f7 100644 --- a/web/packages/base/locales/ta-IN/translation.json +++ b/web/packages/base/locales/ta-IN/translation.json @@ -45,7 +45,6 @@ "create_albums": "", "enter_album_name": "", "close_key": "", - "enter_file_name": "", "close": "", "yes": "", "no": "", @@ -490,8 +489,8 @@ "update_available_message": "", "download_and_install": "", "ignore_this_version": "", - "TODAY": "", - "YESTERDAY": "", + "today": "", + "yesterday": "", "enter_name": "", "uploader_name_hint": "", "name_placeholder": "", @@ -529,15 +528,15 @@ "gb": "", "tb": "" }, - "STOP_EXPORT": "", - "EXPORT_PROGRESS": "", - "MIGRATING_EXPORT": "", - "RENAMING_COLLECTION_FOLDERS": "", - "TRASHING_DELETED_FILES": "", - "TRASHING_DELETED_COLLECTIONS": "", - "CONTINUOUS_EXPORT": "", - "PENDING_ITEMS": "", - "EXPORT_STARTING": "", + "stop": "", + "sync_continuously": "", + "export_starting": "", + "preparing": "", + "renaming_album_folders": "", + "trashing_deleted_files": "", + "trashing_deleted_albums": "", + "export_progress": "", + "pending_items": "", "delete_account_reason_label": "", "delete_account_reason_placeholder": "", "delete_reason": { @@ -570,10 +569,10 @@ "downloading_album": "", "download_failed": "", "download_progress": "", - "CHRISTMAS": "", - "CHRISTMAS_EVE": "", - "NEW_YEAR": "", - "NEW_YEAR_EVE": "", + "christmas": "", + "christmas_eve": "", + "new_year": "", + "new_year_eve": "", "image": "", "video": "", "live_photo": "", diff --git a/web/packages/base/locales/te-IN/translation.json b/web/packages/base/locales/te-IN/translation.json index e6f4c65fd6..a429ed19f7 100644 --- a/web/packages/base/locales/te-IN/translation.json +++ b/web/packages/base/locales/te-IN/translation.json @@ -45,7 +45,6 @@ "create_albums": "", "enter_album_name": "", "close_key": "", - "enter_file_name": "", "close": "", "yes": "", "no": "", @@ -490,8 +489,8 @@ "update_available_message": "", "download_and_install": "", "ignore_this_version": "", - "TODAY": "", - "YESTERDAY": "", + "today": "", + "yesterday": "", "enter_name": "", "uploader_name_hint": "", "name_placeholder": "", @@ -529,15 +528,15 @@ "gb": "", "tb": "" }, - "STOP_EXPORT": "", - "EXPORT_PROGRESS": "", - "MIGRATING_EXPORT": "", - "RENAMING_COLLECTION_FOLDERS": "", - "TRASHING_DELETED_FILES": "", - "TRASHING_DELETED_COLLECTIONS": "", - "CONTINUOUS_EXPORT": "", - "PENDING_ITEMS": "", - "EXPORT_STARTING": "", + "stop": "", + "sync_continuously": "", + "export_starting": "", + "preparing": "", + "renaming_album_folders": "", + "trashing_deleted_files": "", + "trashing_deleted_albums": "", + "export_progress": "", + "pending_items": "", "delete_account_reason_label": "", "delete_account_reason_placeholder": "", "delete_reason": { @@ -570,10 +569,10 @@ "downloading_album": "", "download_failed": "", "download_progress": "", - "CHRISTMAS": "", - "CHRISTMAS_EVE": "", - "NEW_YEAR": "", - "NEW_YEAR_EVE": "", + "christmas": "", + "christmas_eve": "", + "new_year": "", + "new_year_eve": "", "image": "", "video": "", "live_photo": "", diff --git a/web/packages/base/locales/th-TH/translation.json b/web/packages/base/locales/th-TH/translation.json index e6f4c65fd6..a429ed19f7 100644 --- a/web/packages/base/locales/th-TH/translation.json +++ b/web/packages/base/locales/th-TH/translation.json @@ -45,7 +45,6 @@ "create_albums": "", "enter_album_name": "", "close_key": "", - "enter_file_name": "", "close": "", "yes": "", "no": "", @@ -490,8 +489,8 @@ "update_available_message": "", "download_and_install": "", "ignore_this_version": "", - "TODAY": "", - "YESTERDAY": "", + "today": "", + "yesterday": "", "enter_name": "", "uploader_name_hint": "", "name_placeholder": "", @@ -529,15 +528,15 @@ "gb": "", "tb": "" }, - "STOP_EXPORT": "", - "EXPORT_PROGRESS": "", - "MIGRATING_EXPORT": "", - "RENAMING_COLLECTION_FOLDERS": "", - "TRASHING_DELETED_FILES": "", - "TRASHING_DELETED_COLLECTIONS": "", - "CONTINUOUS_EXPORT": "", - "PENDING_ITEMS": "", - "EXPORT_STARTING": "", + "stop": "", + "sync_continuously": "", + "export_starting": "", + "preparing": "", + "renaming_album_folders": "", + "trashing_deleted_files": "", + "trashing_deleted_albums": "", + "export_progress": "", + "pending_items": "", "delete_account_reason_label": "", "delete_account_reason_placeholder": "", "delete_reason": { @@ -570,10 +569,10 @@ "downloading_album": "", "download_failed": "", "download_progress": "", - "CHRISTMAS": "", - "CHRISTMAS_EVE": "", - "NEW_YEAR": "", - "NEW_YEAR_EVE": "", + "christmas": "", + "christmas_eve": "", + "new_year": "", + "new_year_eve": "", "image": "", "video": "", "live_photo": "", diff --git a/web/packages/base/locales/ti-ER/translation.json b/web/packages/base/locales/ti-ER/translation.json index e6f4c65fd6..a429ed19f7 100644 --- a/web/packages/base/locales/ti-ER/translation.json +++ b/web/packages/base/locales/ti-ER/translation.json @@ -45,7 +45,6 @@ "create_albums": "", "enter_album_name": "", "close_key": "", - "enter_file_name": "", "close": "", "yes": "", "no": "", @@ -490,8 +489,8 @@ "update_available_message": "", "download_and_install": "", "ignore_this_version": "", - "TODAY": "", - "YESTERDAY": "", + "today": "", + "yesterday": "", "enter_name": "", "uploader_name_hint": "", "name_placeholder": "", @@ -529,15 +528,15 @@ "gb": "", "tb": "" }, - "STOP_EXPORT": "", - "EXPORT_PROGRESS": "", - "MIGRATING_EXPORT": "", - "RENAMING_COLLECTION_FOLDERS": "", - "TRASHING_DELETED_FILES": "", - "TRASHING_DELETED_COLLECTIONS": "", - "CONTINUOUS_EXPORT": "", - "PENDING_ITEMS": "", - "EXPORT_STARTING": "", + "stop": "", + "sync_continuously": "", + "export_starting": "", + "preparing": "", + "renaming_album_folders": "", + "trashing_deleted_files": "", + "trashing_deleted_albums": "", + "export_progress": "", + "pending_items": "", "delete_account_reason_label": "", "delete_account_reason_placeholder": "", "delete_reason": { @@ -570,10 +569,10 @@ "downloading_album": "", "download_failed": "", "download_progress": "", - "CHRISTMAS": "", - "CHRISTMAS_EVE": "", - "NEW_YEAR": "", - "NEW_YEAR_EVE": "", + "christmas": "", + "christmas_eve": "", + "new_year": "", + "new_year_eve": "", "image": "", "video": "", "live_photo": "", diff --git a/web/packages/base/locales/tr-TR/translation.json b/web/packages/base/locales/tr-TR/translation.json index 30926580d7..723ab520c7 100644 --- a/web/packages/base/locales/tr-TR/translation.json +++ b/web/packages/base/locales/tr-TR/translation.json @@ -45,7 +45,6 @@ "create_albums": "Albüm oluştur", "enter_album_name": "Albüm adı", "close_key": "Kapat (Esc)", - "enter_file_name": "Dosya adı", "close": "Kapat", "yes": "", "no": "Hayır", @@ -250,7 +249,7 @@ "people_suggestions_empty": "", "info": "", "info_key": "", - "file_name": "", + "file_name": "Dosya adı", "caption_placeholder": "", "location": "", "view_on_map": "", @@ -490,8 +489,8 @@ "update_available_message": "", "download_and_install": "", "ignore_this_version": "", - "TODAY": "", - "YESTERDAY": "", + "today": "", + "yesterday": "", "enter_name": "İsim gir", "uploader_name_hint": "Arkadaşlarının bu harika fotoğraflar için kime teşekkür etmeleri gerektiğini bilmeleri için bir isim ekle!", "name_placeholder": "", @@ -529,15 +528,15 @@ "gb": "", "tb": "" }, - "STOP_EXPORT": "", - "EXPORT_PROGRESS": "", - "MIGRATING_EXPORT": "", - "RENAMING_COLLECTION_FOLDERS": "", - "TRASHING_DELETED_FILES": "", - "TRASHING_DELETED_COLLECTIONS": "", - "CONTINUOUS_EXPORT": "", - "PENDING_ITEMS": "", - "EXPORT_STARTING": "", + "stop": "", + "sync_continuously": "", + "export_starting": "", + "preparing": "", + "renaming_album_folders": "", + "trashing_deleted_files": "", + "trashing_deleted_albums": "", + "export_progress": "", + "pending_items": "", "delete_account_reason_label": "", "delete_account_reason_placeholder": "", "delete_reason": { @@ -570,10 +569,10 @@ "downloading_album": "", "download_failed": "", "download_progress": "", - "CHRISTMAS": "", - "CHRISTMAS_EVE": "", - "NEW_YEAR": "", - "NEW_YEAR_EVE": "", + "christmas": "", + "christmas_eve": "", + "new_year": "", + "new_year_eve": "", "image": "", "video": "", "live_photo": "", diff --git a/web/packages/base/locales/uk-UA/translation.json b/web/packages/base/locales/uk-UA/translation.json index 4c40eeab63..4b5061e376 100644 --- a/web/packages/base/locales/uk-UA/translation.json +++ b/web/packages/base/locales/uk-UA/translation.json @@ -45,7 +45,6 @@ "create_albums": "Створити альбоми", "enter_album_name": "Назва альбому", "close_key": "Закрити (Esc)", - "enter_file_name": "Назва файлу", "close": "Закрити", "yes": "Так", "no": "Ні", @@ -490,8 +489,8 @@ "update_available_message": "Вийшла нова версія Ente, але вона не може бути автоматично завантажена і встановлена.", "download_and_install": "Завантажити й установити", "ignore_this_version": "Ігнорувати цю версію", - "TODAY": "Сьогодні", - "YESTERDAY": "Вчора", + "today": "Сьогодні", + "yesterday": "Вчора", "enter_name": "Введіть ім'я", "uploader_name_hint": "Додайте ім'я, щоб ваші друзі знали кому подякувати за ці чудові фото!", "name_placeholder": "Назва...", @@ -529,15 +528,15 @@ "gb": "ГБ", "tb": "ТБ" }, - "STOP_EXPORT": "Зупинити", - "EXPORT_PROGRESS": "{{progress.success, number}} / {{progress.total, number}} елементів синхронізовано", - "MIGRATING_EXPORT": "Підготування...", - "RENAMING_COLLECTION_FOLDERS": "Перейменування тек альбомів...", - "TRASHING_DELETED_FILES": "Очищення видалених файлів...", - "TRASHING_DELETED_COLLECTIONS": "Очищення видалених альбомів...", - "CONTINUOUS_EXPORT": "Постійна синхронізація", - "PENDING_ITEMS": "Елементи на розгляді", - "EXPORT_STARTING": "Початок експортування...", + "stop": "Зупинити", + "sync_continuously": "Постійна синхронізація", + "export_starting": "Початок експортування...", + "preparing": "Підготування...", + "renaming_album_folders": "Перейменування тек альбомів...", + "trashing_deleted_files": "Очищення видалених файлів...", + "trashing_deleted_albums": "Очищення видалених альбомів...", + "export_progress": "{{progress.success, number}} / {{progress.total, number}} елементів синхронізовано", + "pending_items": "Елементи на розгляді", "delete_account_reason_label": "Яка основна причина видалення вашого облікового запису?", "delete_account_reason_placeholder": "Оберіть причину", "delete_reason": { @@ -570,10 +569,10 @@ "downloading_album": "Завантаження {{name}}", "download_failed": "Не вдалося завантажити", "download_progress": "{{count, number}} / {{total, number}} файлів", - "CHRISTMAS": "Різдво", - "CHRISTMAS_EVE": "Святвечір", - "NEW_YEAR": "Новий рік", - "NEW_YEAR_EVE": "Вечір Нового року", + "christmas": "Різдво", + "christmas_eve": "Святвечір", + "new_year": "Новий рік", + "new_year_eve": "Вечір Нового року", "image": "Зображення", "video": "Відео", "live_photo": "Живі фото", diff --git a/web/packages/base/locales/vi-VN/translation.json b/web/packages/base/locales/vi-VN/translation.json index e69dd0fc9f..616bdff3b7 100644 --- a/web/packages/base/locales/vi-VN/translation.json +++ b/web/packages/base/locales/vi-VN/translation.json @@ -45,7 +45,6 @@ "create_albums": "Tạo album", "enter_album_name": "Tên album", "close_key": "Đóng (Esc)", - "enter_file_name": "Tên tệp", "close": "Đóng", "yes": "Có", "no": "Không", @@ -490,8 +489,8 @@ "update_available_message": "Một phiên bản mới của Ente đã được phát hành, nhưng không thể tự động tải xuống và cài đặt.", "download_and_install": "Tải xuống và cài đặt", "ignore_this_version": "Bỏ qua phiên bản này", - "TODAY": "Hôm nay", - "YESTERDAY": "Hôm qua", + "today": "Hôm nay", + "yesterday": "Hôm qua", "enter_name": "Nhập tên", "uploader_name_hint": "Thêm một tên để bạn bè biết ai là người đáng cảm ơn cho những bức ảnh tuyệt vời này!", "name_placeholder": "Tên...", @@ -529,15 +528,15 @@ "gb": "GB", "tb": "TB" }, - "STOP_EXPORT": "Dừng", - "EXPORT_PROGRESS": "{{progress.success, number}} / {{progress.total, number}} mục đã đồng bộ", - "MIGRATING_EXPORT": "Đang chuẩn bị...", - "RENAMING_COLLECTION_FOLDERS": "Đang đổi tên thư mục album...", - "TRASHING_DELETED_FILES": "Đang xóa tệp đã xóa...", - "TRASHING_DELETED_COLLECTIONS": "Đang xóa album đã xóa...", - "CONTINUOUS_EXPORT": "Đồng bộ liên tục", - "PENDING_ITEMS": "Mục đang chờ", - "EXPORT_STARTING": "Xuất bắt đầu...", + "stop": "Dừng", + "sync_continuously": "Đồng bộ liên tục", + "export_starting": "Xuất bắt đầu...", + "preparing": "Đang chuẩn bị...", + "renaming_album_folders": "Đang đổi tên thư mục album...", + "trashing_deleted_files": "Đang xóa tệp đã xóa...", + "trashing_deleted_albums": "Đang xóa album đã xóa...", + "export_progress": "{{progress.success, number}} / {{progress.total, number}} mục đã đồng bộ", + "pending_items": "Mục đang chờ", "delete_account_reason_label": "Lý do chính bạn xóa tài khoản là gì?", "delete_account_reason_placeholder": "Chọn một lý do", "delete_reason": { @@ -570,10 +569,10 @@ "downloading_album": "Đang tải xuống {{name}}", "download_failed": "Tải xuống thất bại", "download_progress": "{{count, number}} / {{total, number}} tệp", - "CHRISTMAS": "Giáng sinh", - "CHRISTMAS_EVE": "Đêm Giáng sinh", - "NEW_YEAR": "Năm mới", - "NEW_YEAR_EVE": "Đêm giao thừa", + "christmas": "Giáng sinh", + "christmas_eve": "Đêm Giáng sinh", + "new_year": "Năm mới", + "new_year_eve": "Đêm giao thừa", "image": "Hình ảnh", "video": "Video", "live_photo": "Ảnh trực tiếp", diff --git a/web/packages/base/locales/zh-CN/translation.json b/web/packages/base/locales/zh-CN/translation.json index 4d23a25fe3..7cab66cf88 100644 --- a/web/packages/base/locales/zh-CN/translation.json +++ b/web/packages/base/locales/zh-CN/translation.json @@ -45,7 +45,6 @@ "create_albums": "创建相册", "enter_album_name": "相册名称", "close_key": "关闭 (或按Esc键)", - "enter_file_name": "文件名", "close": "关闭", "yes": "是", "no": "否", @@ -112,7 +111,7 @@ "password_changed_elsewhere": "密码已在别处更改", "password_changed_elsewhere_message": "请在此设备上再次登录以使用您的新密码进行身份验证。", "go_back": "返回", - "account": "", + "account": "账户", "recovery_key": "恢复密钥", "do_this_later": "稍后再做", "save_key": "保存密钥", @@ -128,9 +127,9 @@ "no_recovery_key_message": "由于我们端到端加密协议的性质,如果没有您的密码或恢复密钥,您的数据将无法解密", "no_two_factor_recovery_key_message": "请用您注册Ente账户的电子邮箱发一封邮件给 {{emailID}}", "contact_support": "联系支持", - "help": "", - "ente_help": "", - "blog": "", + "help": "帮助", + "ente_help": "Ente 帮助", + "blog": "博客", "request_feature": "功能建议", "support": "支持", "cancel": "取消", @@ -205,11 +204,11 @@ "delete_photos": "删除照片", "keep_photos": "保留照片", "share_album": "分享相册", - "sharing_with_self": "", - "sharing_already_shared": "", + "sharing_with_self": "您不能与自己共享", + "sharing_already_shared": "您已经与 {{email}} 共享了", "sharing_album_not_allowed": "不允许分享相册", "sharing_disabled_for_free_accounts": "免费账户禁用共享", - "sharing_user_does_not_exist": "", + "sharing_user_does_not_exist": "找不到拥有该电子邮件的用户", "search": "搜索", "search_results": "搜索结果", "no_results": "未找到任何结果", @@ -475,8 +474,8 @@ "stop_watching_folder_message": "您现有的文件不会被删除,但 Ente 将停止自动更新链接的 Ente 相册在此文件夹中的更改。", "yes_stop": "是的,停止", "change_folder": "更改文件夹", - "view_logs": "", - "view_logs_message": "", + "view_logs": "查看日志", + "view_logs_message": "

这将显示调试日志,您可以通过电子邮件将其发送给我们以帮助调试您的问题。

请注意,将包含文件名以帮助跟踪特定文件的问题。

", "weak_device_hint": "您使用的网络浏览器功能不够强大,无法加密您的照片。 请尝试在电脑上登录Ente,或下载Ente移动/桌面应用程序。", "drag_and_drop_hint": "或者拖动并拖动到 Ente 窗口", "authenticate": "身份认证", @@ -490,8 +489,8 @@ "update_available_message": "新版本的 Ente 已发布,但无法自动下载和安装。", "download_and_install": "下载并安装", "ignore_this_version": "忽略该版本", - "TODAY": "今天", - "YESTERDAY": "昨天", + "today": "今天", + "yesterday": "昨天", "enter_name": "输入名字", "uploader_name_hint": "请添加一个名字,以便您的朋友知晓该感谢谁拍摄了这些精美的照片!", "name_placeholder": "名称...", @@ -529,15 +528,15 @@ "gb": "GB", "tb": "TB" }, - "STOP_EXPORT": "停止", - "EXPORT_PROGRESS": "{{progress.success}} / {{progress.total}} 个文件已导出", - "MIGRATING_EXPORT": "准备中...", - "RENAMING_COLLECTION_FOLDERS": "正在重命名相册文件夹...", - "TRASHING_DELETED_FILES": "正在回收删除的文件...", - "TRASHING_DELETED_COLLECTIONS": "正在回收已删除的相册...", - "CONTINUOUS_EXPORT": "持续同步", - "PENDING_ITEMS": "待处理的项目", - "EXPORT_STARTING": "导出开始...", + "stop": "停止", + "sync_continuously": "持续同步", + "export_starting": "导出开始...", + "preparing": "准备中...", + "renaming_album_folders": "正在重命名相册文件夹...", + "trashing_deleted_files": "正在回收删除的文件...", + "trashing_deleted_albums": "正在回收已删除的相册...", + "export_progress": "{{progress.success}} / {{progress.total}} 个文件已导出", + "pending_items": "待处理的项目", "delete_account_reason_label": "您删除账户的主要原因是什么?", "delete_account_reason_placeholder": "选择一个原因", "delete_reason": { @@ -570,10 +569,10 @@ "downloading_album": "正在下载 {{name}}", "download_failed": "下载失败", "download_progress": "{{count, number}} / {{total, number}} 个文件", - "CHRISTMAS": "圣诞", - "CHRISTMAS_EVE": "平安夜", - "NEW_YEAR": "新年", - "NEW_YEAR_EVE": "除夕", + "christmas": "圣诞", + "christmas_eve": "平安夜", + "new_year": "新年", + "new_year_eve": "除夕", "image": "图像", "video": "视频", "live_photo": "实况照片", @@ -605,7 +604,7 @@ "reset": "重设", "faster_upload": "更快上传", "faster_upload_description": "通过附近的服务器路由上传", - "open_ente_on_startup": "", + "open_ente_on_startup": "启动时打开 Ente", "cast_album_to_tv": "在电视上播放相册", "enter_cast_pin_code": "输入您在下面的电视上看到的代码来配对此设备。", "code": "代码", @@ -654,8 +653,8 @@ "server_endpoint": "服务器端点", "more_information": "更多信息", "save": "保存", - "theme": "", - "system": "", - "light": "", - "dark": "" + "theme": "主题", + "system": "系统", + "light": "浅色", + "dark": "深色" } diff --git a/web/packages/base/locales/zh-HK/translation.json b/web/packages/base/locales/zh-HK/translation.json index e6f4c65fd6..a429ed19f7 100644 --- a/web/packages/base/locales/zh-HK/translation.json +++ b/web/packages/base/locales/zh-HK/translation.json @@ -45,7 +45,6 @@ "create_albums": "", "enter_album_name": "", "close_key": "", - "enter_file_name": "", "close": "", "yes": "", "no": "", @@ -490,8 +489,8 @@ "update_available_message": "", "download_and_install": "", "ignore_this_version": "", - "TODAY": "", - "YESTERDAY": "", + "today": "", + "yesterday": "", "enter_name": "", "uploader_name_hint": "", "name_placeholder": "", @@ -529,15 +528,15 @@ "gb": "", "tb": "" }, - "STOP_EXPORT": "", - "EXPORT_PROGRESS": "", - "MIGRATING_EXPORT": "", - "RENAMING_COLLECTION_FOLDERS": "", - "TRASHING_DELETED_FILES": "", - "TRASHING_DELETED_COLLECTIONS": "", - "CONTINUOUS_EXPORT": "", - "PENDING_ITEMS": "", - "EXPORT_STARTING": "", + "stop": "", + "sync_continuously": "", + "export_starting": "", + "preparing": "", + "renaming_album_folders": "", + "trashing_deleted_files": "", + "trashing_deleted_albums": "", + "export_progress": "", + "pending_items": "", "delete_account_reason_label": "", "delete_account_reason_placeholder": "", "delete_reason": { @@ -570,10 +569,10 @@ "downloading_album": "", "download_failed": "", "download_progress": "", - "CHRISTMAS": "", - "CHRISTMAS_EVE": "", - "NEW_YEAR": "", - "NEW_YEAR_EVE": "", + "christmas": "", + "christmas_eve": "", + "new_year": "", + "new_year_eve": "", "image": "", "video": "", "live_photo": "", diff --git a/web/packages/base/log-web.ts b/web/packages/base/log-web.ts index 6ff6ed0a04..6cfcba31cb 100644 --- a/web/packages/base/log-web.ts +++ b/web/packages/base/log-web.ts @@ -50,6 +50,31 @@ export const logUnhandledErrorsAndRejections = (attach: boolean) => { } }; +/** + * Attach handlers to log any unhandled exceptions and promise rejections in web + * workers. + * + * This is a variant of {@link logUnhandledErrorsAndRejections} that works in + * web workers. It should be called at the top level of the main worker script. + * + * Note: When I tested this, attaching the onerror handler to the worker outside + * the worker (e.g. when creating it in comlink-worker.ts) worked, but attaching + * the "unhandledrejection" event there did not work. Attaching them to `self` + * (the {@link WorkerGlobal}) worked. + */ +export const logUnhandledErrorsAndRejectionsInWorker = () => { + const handleError = (event: ErrorEvent) => { + log.error("Unhandled error", event.error); + }; + + const handleUnhandledRejection = (event: PromiseRejectionEvent) => { + log.error("Unhandled promise rejection", event.reason); + }; + + self.addEventListener("error", handleError); + self.addEventListener("unhandledrejection", handleUnhandledRejection); +}; + interface LogEntry { timestamp: number; logLine: string; diff --git a/web/packages/base/next.config.base.js b/web/packages/base/next.config.base.js index 2c8155bf7a..594a8b9a3c 100644 --- a/web/packages/base/next.config.base.js +++ b/web/packages/base/next.config.base.js @@ -104,6 +104,10 @@ if (process.env.NEXT_PUBLIC_ENTE_FAMILY_URL) { const nextConfig = { // Generate a static export when we run `next build`. output: "export", + // Instead of the nice and useful HMR indicator that used to exist prior to + // 15.2, the Next.js folks have made this a persistent "branding" indicator + // that gets in the way and needs to be disabled. + devIndicators: false, compiler: { emotion: true, }, diff --git a/web/packages/base/package.json b/web/packages/base/package.json index 6fea597abd..566fccca9d 100644 --- a/web/packages/base/package.json +++ b/web/packages/base/package.json @@ -7,25 +7,27 @@ "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.0", "@fontsource-variable/inter": "^5.1.1", - "@mui/icons-material": "^6.4.3", - "@mui/material": "^6.4.3", + "@mui/icons-material": "^6.4.6", + "@mui/material": "^6.4.6", "comlink": "^4.4.2", + "formik": "^2.4.6", "get-user-locale": "^2.3.2", "i18next": "^24.2.2", "i18next-resources-to-backend": "^1.2.1", "idb": "^8.0.2", "libsodium-wrappers-sumo": "^0.7.15", - "nanoid": "^5.0.9", - "next": "^15.1.6", + "nanoid": "^5.1.2", + "next": "^15.2.0", "react": "^19.0.0", "react-dom": "^19.0.0", - "react-i18next": "^15.4.0", - "zod": "^3.24.1" + "react-i18next": "^15.4.1", + "yup": "^1.6.1", + "zod": "^3.24.2" }, "devDependencies": { "@/build-config": "*", "@types/libsodium-wrappers-sumo": "^0.7.8", - "@types/react": "^19.0.8", - "@types/react-dom": "^19.0.3" + "@types/react": "^19.0.10", + "@types/react-dom": "^19.0.4" } } diff --git a/web/packages/base/session-store.ts b/web/packages/base/session.ts similarity index 96% rename from web/packages/base/session-store.ts rename to web/packages/base/session.ts index 6b9b38387f..b3c2e7fd82 100644 --- a/web/packages/base/session-store.ts +++ b/web/packages/base/session.ts @@ -3,7 +3,7 @@ import { decryptBox } from "./crypto"; import { toB64 } from "./crypto/libsodium"; /** - * Return the decrypted user's master key from session storage. + * Return the user's decrypted master key from session storage. * * Precondition: The user should be logged in. */ diff --git a/web/packages/base/tsconfig.json b/web/packages/base/tsconfig.json index bff3b51d3a..649d10a907 100644 --- a/web/packages/base/tsconfig.json +++ b/web/packages/base/tsconfig.json @@ -1,9 +1,5 @@ { "extends": "@/build-config/tsconfig-next.json", - "compilerOptions": { - /* MUI doesn't work with exactOptionalPropertyTypes yet. */ - "exactOptionalPropertyTypes": false - }, "include": [ ".", "../base/global-electron.d.ts", diff --git a/web/packages/base/types/ipc.ts b/web/packages/base/types/ipc.ts index 5a1e39bddb..07f6b79db7 100644 --- a/web/packages/base/types/ipc.ts +++ b/web/packages/base/types/ipc.ts @@ -259,6 +259,18 @@ export interface Electron { */ writeFile: (path: string, contents: string) => Promise; + /** + * A variant of {@link writeFile} that first writes the file to a backup + * file, and then renames the backup file to the actual path. This both + * makes the write atomic (as far as the node's fs.rename guarantees + * atomicity), and also keeps the backup file around for recovery if + * something goes wrong during the rename. + * + * This behaviour of this function is tailored around for writes to the + * "export_status.json" during exports. + */ + writeFileViaBackup: (path: string, contents: string) => Promise; + /** * Return true if there is an item at {@link dirPath}, and it is as * directory. diff --git a/web/packages/build-config/tsconfig-next.json b/web/packages/build-config/tsconfig-next.json index f06e08e7f4..a8257b0bba 100644 --- a/web/packages/build-config/tsconfig-next.json +++ b/web/packages/build-config/tsconfig-next.json @@ -4,7 +4,9 @@ "compilerOptions": { /* Next.js insists on adding these. Sigh. */ "allowJs": true, - "incremental": true + "incremental": true, + /* MUI doesn't work with exactOptionalPropertyTypes yet. */ + "exactOptionalPropertyTypes": false }, /* Next.js insists on adding this, even though we don't need it. */ "exclude": ["node_modules"] diff --git a/web/packages/gallery/components/FileInfo.tsx b/web/packages/gallery/components/FileInfo.tsx new file mode 100644 index 0000000000..c021aeb07d --- /dev/null +++ b/web/packages/gallery/components/FileInfo.tsx @@ -0,0 +1,1079 @@ +/* eslint-disable @typescript-eslint/ban-ts-comment */ +/* TODO: Audit this file +Plan of action: +- Move common components into FileInfoComponents.tsx + +- Move the rest out to files in the apps themeselves: albums/SharedFileInfo + and photos/FileInfo to deal with the @/new/photos imports here. +*/ + +import { assertionFailed } from "@/base/assert"; +import { LinkButtonUndecorated } from "@/base/components/LinkButton"; +import { type ButtonishProps } from "@/base/components/mui"; +import { ActivityIndicator } from "@/base/components/mui/ActivityIndicator"; +import { SidebarDrawer } from "@/base/components/mui/SidebarDrawer"; +import { SingleInputForm } from "@/base/components/SingleInputForm"; +import { Titlebar } from "@/base/components/Titlebar"; +import { EllipsizedTypography } from "@/base/components/Typography"; +import { + useModalVisibility, + type ModalVisibilityProps, +} from "@/base/components/utils/modal"; +import { useBaseContext } from "@/base/context"; +import { haveWindow } from "@/base/env"; +import { nameAndExtension } from "@/base/file-name"; +import { formattedDate, formattedTime } from "@/base/i18n-date"; +import log from "@/base/log"; +import type { Location } from "@/base/types"; +import { CopyButton } from "@/gallery/components/FileInfoComponents"; +import { tagNumericValue, type RawExifTags } from "@/gallery/services/exif"; +import { + changeCaption, + changeFileName, + updateExistingFilePubMetadata, +} from "@/gallery/services/file"; +import { formattedByteSize } from "@/gallery/utils/units"; +import { type EnteFile } from "@/media/file"; +import { + fileCaption, + fileCreationPhotoDate, + fileLocation, + filePublicMagicMetadata, + updateRemotePublicMagicMetadata, + type ParsedMetadata, + type ParsedMetadataDate, +} from "@/media/file-metadata"; +import { FileType } from "@/media/file-type"; +import { FileDateTimePicker } from "@/new/photos/components/FileDateTimePicker"; +import { FilePeopleList } from "@/new/photos/components/PeopleList"; +import { + confirmDisableMapsDialogAttributes, + confirmEnableMapsDialogAttributes, +} from "@/new/photos/components/utils/dialog"; +import { useSettingsSnapshot } from "@/new/photos/components/utils/use-snapshot"; +import { + aboveFileViewerContentZ, + fileInfoDrawerZ, +} from "@/new/photos/components/utils/z-index"; +import { + getAnnotatedFacesForFile, + isMLEnabled, + type AnnotatedFaceID, +} from "@/new/photos/services/ml"; +import { updateMapEnabled } from "@/new/photos/services/settings"; +import CalendarTodayIcon from "@mui/icons-material/CalendarToday"; +import CameraOutlinedIcon from "@mui/icons-material/CameraOutlined"; +import CloseIcon from "@mui/icons-material/Close"; +import DoneIcon from "@mui/icons-material/Done"; +import EditIcon from "@mui/icons-material/Edit"; +import FaceRetouchingNaturalIcon from "@mui/icons-material/FaceRetouchingNatural"; +import FolderOutlinedIcon from "@mui/icons-material/FolderOutlined"; +import LocationOnOutlinedIcon from "@mui/icons-material/LocationOnOutlined"; +import PhotoOutlinedIcon from "@mui/icons-material/PhotoOutlined"; +import TextSnippetOutlinedIcon from "@mui/icons-material/TextSnippetOutlined"; +import VideocamOutlinedIcon from "@mui/icons-material/VideocamOutlined"; +import { + Box, + Button, + CircularProgress, + Dialog, + DialogContent, + DialogTitle, + IconButton, + InputAdornment, + Link, + Stack, + styled, + TextField, + Typography, + type ButtonProps, + type DialogProps, +} from "@mui/material"; +import { useFormik } from "formik"; +import { t } from "i18next"; +import React, { useEffect, useMemo, useRef, useState } from "react"; + +// Re-uses images from ~leaflet package. +import "leaflet-defaulticon-compatibility/dist/leaflet-defaulticon-compatibility.webpack.css"; +import "leaflet/dist/leaflet.css"; +// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-unused-expressions +haveWindow() && require("leaflet-defaulticon-compatibility"); +const leaflet = haveWindow() + ? // eslint-disable-next-line @typescript-eslint/no-require-imports + (require("leaflet") as typeof import("leaflet")) + : null; + +/** + * Exif data for a file, in a form suitable for use by {@link FileInfo}. + * + * TODO: Indicate missing exif (e.g. videos) better, both in the data type, and + * in the UI (e.g. by omitting the entire row). + */ +export interface FileInfoExif { + tags: RawExifTags | undefined; + parsed: ParsedMetadata | undefined; +} + +export type FileInfoProps = ModalVisibilityProps & { + /** + * The file whose information we are showing. + */ + file: EnteFile | undefined; + /** + * Exif information for {@link file}. + */ + exif: FileInfoExif | undefined; + /** + * If set, then controls to edit the file's metadata (name, date, caption) + * will be shown. + */ + allowEdits?: boolean; + /** + * If set, then an inline map will be shown (if the user has enabled it) + * using the file's location. + */ + allowMap?: boolean; + /** + * If set, then a clickable chip will be shown for each collection that this + * file is a part of. + * + * Uses {@link fileCollectionIDs}, {@link allCollectionsNameByID} and + * {@link onSelectCollection}, so all of those props should also be set for + * this to have an effect. + */ + showCollections?: boolean; + /** + * A map from file IDs to the IDs of the collections that they're a part of. + * + * Used when {@link showCollections} is set. + */ + fileCollectionIDs?: Map; + /** + * A map from collection IDs to their name. + * + * Used when {@link showCollections} is set. + */ + allCollectionsNameByID?: Map; + /** + * Called when the action on the file info drawer has changed some the + * metadata for some file, and we need to sync with remote to get our + * locally persisted file objects up to date. + * + * The sync is not performed immediately by the file info drawer to give + * faster feedback to the user, and to allow changes to multiple files to be + * batched together into a single sync when the file viewer is closed. + */ + onNeedsRemoteSync: () => void; + /** + * Called when an action on the file info drawer change the caption of the + * given {@link EnteFile}. + * + * This hook allows the file viewer to update the caption it is displaying + * for the given file. + * + * @param updatedFile The updated file object, containing the updated + * caption. + */ + onUpdateCaption: (updatedFile: EnteFile) => void; + /** + * Called when the user selects a collection from among the collections that + * the file belongs to. + */ + onSelectCollection?: (collectionID: number) => void; + /** + * Called when the user selects a person in the file info panel. + */ + onSelectPerson?: (personID: string) => void; +}; + +export const FileInfo: React.FC = ({ + open, + onClose, + file, + exif, + allowEdits, + allowMap, + showCollections, + fileCollectionIDs, + allCollectionsNameByID, + onNeedsRemoteSync, + onUpdateCaption, + onSelectCollection, + onSelectPerson, +}) => { + const { showMiniDialog } = useBaseContext(); + + const { mapEnabled } = useSettingsSnapshot(); + + const [annotatedFaces, setAnnotatedFaces] = useState([]); + + const { show: showRawExif, props: rawExifVisibilityProps } = + useModalVisibility(); + + const location = useMemo( + // Prefer the location in the EnteFile, then fall back to Exif. + () => (file ? fileLocation(file) : undefined) ?? exif?.parsed?.location, + [file, exif], + ); + + const annotatedExif = useMemo(() => annotateExif(exif), [exif]); + + useEffect(() => { + if (!file) return; + if (!isMLEnabled()) return; + + let didCancel = false; + + void getAnnotatedFacesForFile(file).then( + (faces) => !didCancel && setAnnotatedFaces(faces), + ); + + return () => { + didCancel = true; + }; + }, [file]); + + const openEnableMapConfirmationDialog = () => + showMiniDialog( + confirmEnableMapsDialogAttributes(() => updateMapEnabled(true)), + ); + + const openDisableMapConfirmationDialog = () => + showMiniDialog( + confirmDisableMapsDialogAttributes(() => updateMapEnabled(false)), + ); + + const handleSelectFace = ({ personID }: AnnotatedFaceID) => + onSelectPerson?.(personID); + + if (!file) { + if (open) assertionFailed(); + return <>; + } + + return ( + + + + + + + + {annotatedExif?.takenOnDevice && ( + } + title={annotatedExif.takenOnDevice} + caption={createMultipartCaption( + annotatedExif.fNumber, + annotatedExif.exposureTime, + annotatedExif.iso, + )} + /> + )} + + {location && ( + <> + } + title={t("location")} + caption={ + !mapEnabled || !allowMap ? ( + + {t("view_on_map")} + + ) : ( + + {t("disable_map")} + + ) + } + trailingButton={ + + } + /> + {allowMap && ( + + )} + + )} + } + title={t("details")} + caption={ + !exif ? ( + + ) : !exif.tags ? ( + t("no_exif") + ) : ( + + {t("view_exif")} + + ) + } + /> + {annotatedFaces.length > 0 && ( + }> + + + )} + {showCollections && + fileCollectionIDs && + allCollectionsNameByID && + onSelectCollection && ( + + )} + + + + ); +}; + +/** + * Some immediate fields of interest, in the form that we want to display on the + * info panel for a file. + */ +type AnnotatedExif = Required & { + resolution?: string; + megaPixels?: string; + takenOnDevice?: string; + fNumber?: string; + exposureTime?: string; + iso?: string; +}; + +const annotateExif = ( + fileInfoExif: FileInfoExif | undefined, +): AnnotatedExif | undefined => { + if (!fileInfoExif || !fileInfoExif.tags || !fileInfoExif.parsed) + return undefined; + + const info: AnnotatedExif = { ...fileInfoExif }; + + const { width, height } = fileInfoExif.parsed; + if (width && height) { + info.resolution = `${width} x ${height}`; + const mp = Math.round((width * height) / 1000000); + if (mp) info.megaPixels = `${mp}MP`; + } + + const { tags } = fileInfoExif; + const { exif } = tags; + + if (exif) { + if (exif.Make && exif.Model) + info.takenOnDevice = `${exif.Make.description} ${exif.Model.description}`; + + if (exif.FNumber) + info.fNumber = exif.FNumber.description; /* e.g. "f/16" */ + + if (exif.ExposureTime) + info.exposureTime = exif.ExposureTime.description; /* "1/10" */ + + if (exif.ISOSpeedRatings) + info.iso = `ISO${tagNumericValue(exif.ISOSpeedRatings)}`; + } + + return info; +}; + +const FileInfoSidebar = styled( + (props: Pick) => ( + + ), +)(({ theme }) => ({ + zIndex: fileInfoDrawerZ, + // [Note: Lighter backdrop for overlays on photo viewer] + // + // The default backdrop color we use for the drawer in light mode is too + // "white" when used in the image gallery because unlike the rest of the app + // the gallery retains a black background irrespective of the mode. So use a + // lighter scrim when overlaying content directly atop the image gallery. + // + // We don't need to add this special casing for nested overlays (e.g. + // dialogs initiated from the file info drawer itself) since now there is + // enough "white" on the screen to warrant the stronger (default) backdrop. + ...theme.applyStyles("light", { + ".MuiBackdrop-root": { + backgroundColor: theme.vars.palette.backdrop.faint, + }, + }), +})); + +interface InfoItemProps { + /** + * The icon associated with the info entry. + */ + icon: React.ReactNode; + /** + * The primary content / title of the info entry. + * + * Only used if {@link children} are not specified. + */ + title?: string; + /** + * The secondary information / subtext associated with the info entry. + * + * Only used if {@link children} are not specified. + */ + caption?: React.ReactNode; + /** + * A component, usually a button (e.g. an "edit button"), shown at the + * trailing edge of the info entry. + */ + trailingButton?: React.ReactNode; +} + +/** + * An entry in the file info panel listing. + */ +const InfoItem: React.FC> = ({ + icon, + title, + caption, + trailingButton, + children, +}) => ( + + {icon} + {children ? ( + {children} + ) : ( + + {title} + + {caption} + + + )} + {trailingButton} + +); + +const InfoItemIconContainer = styled("div")( + ({ theme }) => ` + width: 48px; + aspect-ratio: 1; + display: flex; + justify-content: center; + align-items: center; + color: ${theme.vars.palette.stroke.muted} +`, +); + +type EditButtonProps = ButtonishProps & { + /** + * If true, then an activity indicator is shown in place of the edit icon. + */ + loading?: boolean; +}; + +const EditButton: React.FC = ({ onClick, loading }) => ( + + {!loading ? ( + + ) : ( + + )} + +); + +type CaptionProps = Pick< + FileInfoProps, + "allowEdits" | "onNeedsRemoteSync" | "onUpdateCaption" +> & { + /* TODO(PS): This is DisplayFile, but that's meant to be removed */ + file: EnteFile & { + title?: string; + }; +}; + +const Caption: React.FC = ({ + file, + allowEdits, + onNeedsRemoteSync, + onUpdateCaption, +}) => { + const [isSaving, setIsSaving] = useState(false); + + const caption = fileCaption(file) ?? ""; + + const formik = useFormik<{ caption: string }>({ + initialValues: { caption }, + validate: ({ caption }) => + caption.length > 5000 + ? { caption: t("caption_character_limit") } + : {}, + onSubmit: async ({ caption: newCaption }, { setFieldError }) => { + if (newCaption == caption) return; + setIsSaving(true); + try { + const updatedFile = await changeCaption(file, newCaption); + updateExistingFilePubMetadata(file, updatedFile); + // @ts-ignore + file.title = file.pubMagicMetadata.data.caption; + onUpdateCaption(file); + } catch (e) { + log.error("Failed to update caption", e); + setFieldError("caption", t("generic_error")); + } + onNeedsRemoteSync(); + setIsSaving(false); + }, + }); + + const { values, errors, handleChange, handleSubmit, resetForm } = formik; + + if (!caption.length && !allowEdits) { + return <>; + } + + return ( + + + {values.caption != caption && ( + + + {isSaving ? ( + + ) : ( + + )} + + resetForm()} disabled={isSaving}> + + + + )} + + ); +}; + +const CaptionForm = styled("form")(({ theme }) => ({ + padding: theme.spacing(1), +})); + +type CreationTimeProps = Pick< + FileInfoProps, + "allowEdits" | "onNeedsRemoteSync" +> & { + file: EnteFile; +}; + +const CreationTime: React.FC = ({ + file, + allowEdits, + onNeedsRemoteSync, +}) => { + const { onGenericError } = useBaseContext(); + + const [isEditing, setIsEditing] = useState(false); + const [isSaving, setIsSaving] = useState(false); + + const originalDate = fileCreationPhotoDate( + file, + filePublicMagicMetadata(file), + ); + + const saveEdits = async (pickedTime: ParsedMetadataDate) => { + setIsEditing(false); + + const { dateTime, timestamp: editedTime } = pickedTime; + if (editedTime == originalDate.getTime()) { + // Same as before. + return; + } + + setIsSaving(true); + try { + // [Note: Don't modify offsetTime when editing date via picker] + // + // Use the updated date time (both in its canonical dateTime form, + // and also as in the epoch timestamp), but don't use the offset. + // + // The offset here will be the offset of the computer where this + // user is making this edit, not the offset of the place where the + // photo was taken. In a future iteration of the date time editor, + // we can provide functionality for the user to edit the associated + // offset, but right now it is not even surfaced, so don't also + // potentially overwrite it. + await updateRemotePublicMagicMetadata(file, { + dateTime, + editedTime, + }); + } catch (e) { + onGenericError(e); + } + onNeedsRemoteSync(); + setIsSaving(false); + }; + + return ( + <> + } + title={formattedDate(originalDate)} + caption={formattedTime(originalDate)} + trailingButton={ + allowEdits && ( + setIsEditing(true)} + loading={isSaving} + /> + ) + } + /> + {isEditing && ( + setIsEditing(false)} + /> + )} + + ); +}; + +type FileNameProps = Pick & { + file: EnteFile; + annotatedExif: AnnotatedExif | undefined; +}; + +const FileName: React.FC = ({ + file, + annotatedExif, + allowEdits, + onNeedsRemoteSync, +}) => { + const { show: showRename, props: renameVisibilityProps } = + useModalVisibility(); + + const fileName = file.metadata.title; + + const handleRename = async (newFileName: string) => { + const updatedFile = await changeFileName(file, newFileName); + updateExistingFilePubMetadata(file, updatedFile); + onNeedsRemoteSync(); + }; + + const icon = + file.metadata.fileType === FileType.video ? ( + + ) : ( + + ); + + const fileSize = file.info?.fileSize; + const caption = createMultipartCaption( + annotatedExif?.megaPixels, + annotatedExif?.resolution, + fileSize ? formattedByteSize(fileSize) : undefined, + ); + + return ( + <> + + } + /> + + + ); +}; + +const createMultipartCaption = ( + p1: string | undefined, + p2: string | undefined, + p3: string | undefined, +) => ( + + {p1 &&
{p1}
} + {p2 &&
{p2}
} + {p3 &&
{p3}
} +
+); + +type RenameFileDialogProps = ModalVisibilityProps & { + /** + * The current name of the file. + */ + fileName: string; + /** + * Called when the user makes a change to the existing name and activates the + * rename button on the dialog. + * + * @param newFileName The changed name. The extension currently cannot be + * modified, but it is guaranteed the name component of {@link newFileName} + * will be different from that of the {@link fileName} prop of the dialog. + * + * Until the promise settles, the dialog will show an activity indicator. If + * the promise rejects, it will also show an error. If the promise is + * fulfilled, then the dialog will also be closed. + * + * The dialog will also be closed if the user activates the rename button + * without changing the name. + */ + onRename: (newFileName: string) => Promise; +}; + +const RenameFileDialog: React.FC = ({ + open, + onClose, + fileName, + onRename, +}) => { + const [name, extension] = nameAndExtension(fileName); + + const handleSubmit = async (newName: string) => { + const newFileName = [newName, extension].filter((x) => !!x).join("."); + if (newFileName != fileName) { + await onRename(newFileName); + } + onClose(); + }; + + return ( + + + {t("rename_file")} + + + + {`.${extension}`} + + ), + }, + }} + /> + + + ); +}; + +const openStreetMapLink = ({ latitude, longitude }: Location) => + `https://www.openstreetmap.org/?mlat=${latitude}&mlon=${longitude}#map=15/${latitude}/${longitude}`; + +interface MapBoxProps { + location: Location; + mapEnabled: boolean; + openUpdateMapConfirmationDialog: () => void; +} + +const MapBox: React.FC = ({ + location, + mapEnabled, + openUpdateMapConfirmationDialog, +}) => { + const urlTemplate = "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"; + const attribution = + '© OpenStreetMap contributors'; + const zoom = 16; + + const mapBoxContainerRef = useRef(null); + + useEffect(() => { + const mapContainer = mapBoxContainerRef.current; + if (mapEnabled) { + const position: L.LatLngTuple = [ + location.latitude, + location.longitude, + ]; + if (mapContainer && !mapContainer.hasChildNodes()) { + // @ts-ignore + const map = leaflet.map(mapContainer).setView(position, zoom); + // @ts-ignore + leaflet + .tileLayer(urlTemplate, { + attribution, + }) + .addTo(map); + // @ts-ignore + leaflet.marker(position).addTo(map).openPopup(); + } + } else { + if (mapContainer?.hasChildNodes()) { + if (mapContainer.firstChild) { + mapContainer.removeChild(mapContainer.firstChild); + } + } + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [mapEnabled]); + + return mapEnabled ? ( + + ) : ( + + + {t("enable_map")} + + + ); +}; + +const MapBoxContainer = styled("div")` + height: 200px; + width: 100%; +`; + +const MapBoxEnableContainer = styled(MapBoxContainer)( + ({ theme }) => ` + position: relative; + display: flex; + justify-content: center; + align-items: center; + background-color: ${theme.vars.palette.fill.fainter}; +`, +); + +interface RawExifProps { + open: boolean; + onClose: () => void; + onInfoClose: () => void; + tags: RawExifTags | undefined; + fileName: string; +} + +const RawExif: React.FC = ({ + open, + onClose, + onInfoClose, + tags, + fileName, +}) => { + if (!tags) { + return <>; + } + + const handleRootClose = () => { + onClose(); + onInfoClose(); + }; + + const items: (readonly [string, string, string, string])[] = Object.entries( + tags, + ) + .map(([namespace, namespaceTags]) => { + return Object.entries(namespaceTags).map(([tagName, tag]) => { + const key = `${namespace}:${tagName}`; + let description = "<...>"; + if (typeof tag == "string") { + description = tag; + } else if (typeof tag == "number") { + description = `${tag}`; + } else if ( + tag && + typeof tag == "object" && + "description" in tag && + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + typeof tag.description == "string" + ) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access + description = tag.description; + } + return [key, namespace, tagName, description] as const; + }); + }) + .flat() + .filter(([, , , description]) => description); + + return ( + + + } + /> + + {items.map(([key, namespace, tagName, description]) => ( + + + + {tagName} + + + {namespace} + + + + {description} + + + ))} + + + ); +}; + +const ExifItem = styled("div")` + padding-left: 8px; + padding-right: 8px; + display: flex; + flex-direction: column; + gap: 4px; +`; + +type AlbumsProps = Required< + Pick< + FileInfoProps, + "fileCollectionIDs" | "allCollectionsNameByID" | "onSelectCollection" + > +> & { + file: EnteFile; +}; + +const Albums: React.FC = ({ + file, + fileCollectionIDs, + allCollectionsNameByID, + onSelectCollection, +}) => ( + }> + + {fileCollectionIDs + .get(file.id) + ?.filter((collectionID) => + allCollectionsNameByID.has(collectionID), + ) + .map((collectionID) => ( + onSelectCollection(collectionID)} + > + {allCollectionsNameByID.get(collectionID)} + + ))} + + +); + +const ChipButton = styled((props: ButtonProps) => ( + + + + {activeAnnotatedFile?.annotation.showDownload == "menu" && ( + + + {/*TODO */ t("download")} + + + + )} + {activeAnnotatedFile?.annotation.showDelete && ( + + + {/*TODO */ t("delete")} + + + + )} + {activeAnnotatedFile?.annotation.showCopyImage && + activeImageURL && ( + + + {/*TODO */ pt("Copy as PNG")} + + {/* Tweak icon size to visually fit better with neighbours */} + + + )} + {activeAnnotatedFile?.annotation.showEditImage && ( + + + {/*TODO */ pt("Edit image")} + + + + )} + + + { + /*TODO */ isFullscreen + ? pt("Exit fullscreen") + : pt("Go fullscreen") + } + + {isFullscreen ? ( + + ) : ( + + )} + + + + {pt("Shortcuts")} + + + + {/* TODO(PS): Fix imports */} + + + +
+ ); +}; + +export default FileViewer; + +const Container = styled("div")` + border: 1px solid red; + + #test-gallery { + border: 1px solid red; + min-height: 10px; + } +`; + +const MoreMenu = styled(Menu)( + ({ theme }) => ` + & .MuiPaper-root { + background-color: ${theme.vars.palette.fixed.dark.background.paper}; + } + & .MuiList-root { + padding-block: 2px; + } +`, +); + +const MoreMenuItem = styled(MenuItem)( + ({ theme }) => ` + min-width: 210px; + + /* MUI MenuItem default implementation has a minHeight of "48px" below the + "sm" breakpoint, and auto after it. We always want the same height, so + set minHeight auto and use an explicit padding always to come out to 44px + (20px (icon or Typography height + 12 + 12) */ + padding-block: 12px; + min-height: auto; + + gap: 1; + justify-content: space-between; + align-items: center; + + /* Same as other controls on the PhotoSwipe UI */ + color: rgba(255 255 255 / 0.85); + &:hover { + color: rgba(255 255 255 / 1); + background-color: ${theme.vars.palette.fixed.dark.background.paper2} + } + + .MuiSvgIcon-root { + font-size: 20px; + } +`, +); + +const MoreMenuItemTitle: React.FC = ({ children }) => ( + {children} +); + +const Shortcuts: React.FC = ({ open, onClose }) => ( + + + {pt("Shortcuts")} + + + + + + + + + + + + + + + + +); + +const ShortcutsContent = styled(DialogContent)` + display: flex; + flex-direction: column; + gap: 16px; +`; + +interface ShortcutProps { + action: string; + shortcut: string; +} + +const Shortcut: React.FC = ({ action, shortcut }) => ( + + + {action} + + {shortcut} + +); + +const fileIsEditableImage = (file: EnteFile) => { + // Only images are editable. + if (file.metadata.fileType !== FileType.image) return false; + + const extension = lowercaseExtension(file.metadata.title); + // Assume it is editable; + let isRenderable = true; + if (extension && needsJPEGConversion(extension)) { + // See if the file is on the whitelist of extensions that we know + // will not be directly renderable. + if (!isDesktop) { + // On the web, we only support HEIC conversion. + isRenderable = isHEICExtension(extension); + } + } + return isRenderable; +}; + +/** + * Return a promise that resolves with a "image/png" blob derived from the given + * {@link imageURL} that can be written to the navigator's clipboard. + */ +const createImagePNGBlob = async (imageURL: string) => + new Promise((resolve, reject) => { + const image = new Image(); + image.onload = () => { + const canvas = document.createElement("canvas"); + canvas.width = image.width; + canvas.height = image.height; + canvas.getContext("2d").drawImage(image, 0, 0); + canvas.toBlob(resolve, "image/png"); + }; + image.onerror = reject; + image.src = imageURL; + }); diff --git a/web/packages/gallery/components/viewer/data-source.ts b/web/packages/gallery/components/viewer/data-source.ts new file mode 100644 index 0000000000..1aa115535d --- /dev/null +++ b/web/packages/gallery/components/viewer/data-source.ts @@ -0,0 +1,562 @@ +import log from "@/base/log"; +import type { FileInfoExif } from "@/gallery/components/FileInfo"; +import { + downloadManager, + type LivePhotoSourceURL, +} from "@/gallery/services/download"; +import { extractRawExif, parseExif } from "@/gallery/services/exif"; +import type { EnteFile } from "@/media/file"; +import { fileCaption } from "@/media/file-metadata"; +import { FileType } from "@/media/file-type"; +import { ensureString } from "@/utils/ensure"; + +/** + * This is a subset of the fields expected by PhotoSwipe itself (see the + * {@link SlideData} type exported by PhotoSwipe). + */ +interface PhotoSwipeSlideData { + /** + * The image URL expected by PhotoSwipe. + * + * This is set to the URL of the image that should be shown in the image + * viewer component provided by PhotoSwipe. + * + * It will be a renderable (i.e., possibly converted) object URL obtained + * from the current "best" image we have. + * + * For example, if all we have is the thumbnail, then this'll be an + * renderable object URL obtained from the thumbnail data. Then later when + * we fetch the original image, this'll be the renderable object URL derived + * from the original. But if it is a video, this will just be cleared. + */ + src?: string | undefined; + /** + * The width (in pixels) of the {@link src} image. + */ + width?: number | undefined; + /** + * The height (in pixels) of the {@link src} image. + */ + height?: number | undefined; + /** + * The alt text associated with the file. + * + * This will be set to the file's caption. PhotoSwipe will use it as the alt + * text when constructing img elements (if any) for this item. We will also + * use this for displaying the visible "caption" element atop the file (both + * images and video). + */ + alt?: string; +} + +/** + * The data returned by the flagship {@link itemDataForFile} function provided + * by the file viewer data source module. + * + * This is the minimal data expected by PhotoSwipe, plus some fields we use + * ourselves in the custom scaffolding we have around PhotoSwipe. + */ +export type ItemData = PhotoSwipeSlideData & { + /** + * The ID of the {@link EnteFile} whose data we are. + */ + fileID: number; + /** + * The {@link EnteFile} type of the file whose data we are. + */ + fileType: FileType; + /** + * The renderable object URL of the image associated with the file. + * + * - For images, this will be the object URL of a renderable image. + * - For videos, this will not be defined. + * - For live photos, this will be a renderable object URL of the image + * portion of the live photo. + */ + imageURL?: string; + /** + * The original image associated with the file, as a Blob. + * + * - For images, this will be the original image itself. + * - For live photos, this will be the image component of the live photo. + * - For videos, this will be not be present. + */ + originalImageBlob?: Blob; + /** + * The renderable object URL of the video associated with the file. + * + * - For images, this will not be defined. + * - For videos, this will be the object URL of a renderable video. + * - For live photos, this will be a renderable object URL of the video + * portion of the live photo. + */ + videoURL?: string; + /** + * `true` if we should indicate to the user that we're still fetching data + * for this file. + * + * Note that this doesn't imply that the data is final. e.g. for a live + * photo, this will be not be set after we get the original image component, + * but the fetch for the video component might still be ongoing. + */ + isContentLoading?: boolean; + /** + * This will be explicitly set to `false` when we want to disable + * PhotoSwipe's built in image zoom. + * + * It is set while the thumbnail is loaded. + */ + isContentZoomable?: boolean; + /** + * This will be `true` if the fetch for the file's data has failed. + * + * It is possible for this to be set in tandem with the content URLs also + * being set (e.g. if we were able to use the cached thumbnail, but the + * original file could not be fetched because of an network error). + */ + fetchFailed?: boolean; +}; + +/** + * This module stores and serves data required by our custom PhotoSwipe + * instance, effectively acting as an in-memory cache. + * + * By keeping this independent of the lifetime of the PhotoSwipe instance, we + * can reuse the same cache for multiple displays of our file viewer. + * + * This will be cleared on logout. + */ +class FileViewerDataSourceState { + /** + * Non-zero if a file viewer is currently open. + * + * This is a counter, but the file viewer data source has other many + * assumptions about only a single instance of PhotoSwipe being active at a + * time, so this could've been a boolean as well. + */ + viewerCount = 0; + /** + * True if our state needs to be cleared the next time the file viewer is + * closed. + */ + needsReset = false; + /** + * The best data we have for a particular file (ID). + */ + itemDataByFileID = new Map(); + /** + * The latest callback registered for notifications of better data being + * available for a particular file (ID). + */ + needsRefreshByFileID = new Map void>(); + /** + * The exif data we have for a particular file (ID). + */ + fileInfoExifByFileID = new Map(); + /** + * The latest callback registered for notifications of exif data being + * available for a particular file (ID). + */ + exifObserverByFileID = new Map void>(); +} + +/** + * State shared by functions in this module. + * + * See {@link FileViewerDataSourceState}. + */ +let _state = new FileViewerDataSourceState(); + +const resetState = () => { + _state = new FileViewerDataSourceState(); +}; + +/** + * Clear any internal state maintained by the file viewer data source. + */ +// TODO(PS): Call me during logout sequence once this is integrated. +export const logoutFileViewerDataSource = resetState; + +/** + * Clear any internal state if possible. This is invoked when files have been + * updated on remote, and those changes synced locally. + * + * Because we also retain callbacks, clearing existing item data when the file + * viewer is open can lead to problematic edge cases. Thus, this function + * behaves in two different ways: + * + * - If the file viewer is already open, then we enqueue a reset for when it is + * closed the next time. + * + * - Otherwise we immediately reset our state. + * + * See: [Note: Changes to underlying files when file viewer is open] + */ +export const resetFileViewerDataSourceOnClose = () => { + if (_state.viewerCount) { + _state.needsReset = true; + } else { + resetState(); + } +}; + +/** + * Called by the file viewer whenever it is opened. + */ +export const fileViewerWillOpen = () => { + _state.viewerCount++; +}; + +/** + * Called by the file viewer whenever it has been closed. + */ +export const fileViewerDidClose = () => { + _state.viewerCount--; + if (_state.needsReset && _state.viewerCount == 0) { + // Reset everything. + resetState(); + } else { + // Selectively clear. + forgetFailedItems(); + forgetExif(); + } +}; + +/** + * Return the best available {@link ItemData} for rendering the given + * {@link file}. + * + * If an entry does not exist for a particular file, then it is lazily added on + * demand, and updated as we keep getting better data (thumbnail, original) for + * the file. + * + * At each step, we call the provided callback so that file viewer can call us + * again to get the updated data. + * + * --- + * + * Detailed flow: + * + * If we already have the final data about the file, then this function will + * return it and do nothing subsequently. + * + * Otherwise, it will: + * + * 1. Return empty slide data; PhotoSwipe will not show anything in the image + * area but will otherwise render UI controls properly (in most cases a + * cached renderable thumbnail URL will be available shortly). + * + * 2. Insert this empty data in its cache so that we don't enqueue multiple + * updates. + * + * Then it we start fetching data for the file. + * + * First it'll fetch the thumbnail. Once that is done, it'll update the data it + * has cached, and notify the caller (using the provided callback) so it can + * refresh the slide. + * + * Then it'll continue fetching the original. + * + * - For images and videos, this will be the single original. + * + * - For live photos, this will also be a two step process, first fetching the + * original image, then again the video component. + * + * At this point, the data for this file will be considered final, and + * subsequent calls for the same file will return this same value unless it is + * invalidated. + * + * If at any point an error occurs, we reset our cache for this file so that the + * next time the data is requested we repeat the process instead of continuing + * to serve the incomplete result. + */ +export const itemDataForFile = (file: EnteFile, needsRefresh: () => void) => { + const fileID = file.id; + const fileType = file.metadata.fileType; + + let itemData = _state.itemDataByFileID.get(fileID); + + // We assume that there is only one file viewer that is using us at a given + // point of time. This assumption is currently valid. + _state.needsRefreshByFileID.set(file.id, needsRefresh); + + if (!itemData) { + itemData = { fileID, fileType, isContentLoading: true }; + _state.itemDataByFileID.set(file.id, itemData); + void enqueueUpdates(file); + } + + return itemData; +}; + +/** + * Forget item data for the given {@link file} if its fetch had failed. + * + * This is called when the user moves away from a slide so that we attempt a + * full retry when they come back the next time. + */ +export const forgetFailedItemDataForFileID = (fileID: number) => { + if (_state.itemDataByFileID.get(fileID)?.fetchFailed) { + _state.itemDataByFileID.delete(fileID); + } +}; + +/** + * Update the alt attribute of the {@link ItemData}, if any, associated with the + * given {@link EnteFile}. + * + * @param updatedFile The file whose caption was updated. + */ +export const updateItemDataAlt = (updatedFile: EnteFile) => { + const itemData = _state.itemDataByFileID.get(updatedFile.id); + if (itemData) { + itemData.alt = fileCaption(updatedFile); + } +}; + +/** + * Forget item data for the all files whose fetch had failed. + * + * This is called when the user closes the file viewer so that we attempt a full + * retry when they reopen the viewer the next time. + */ +const forgetFailedItems = () => + [..._state.itemDataByFileID.keys()].forEach(forgetFailedItemDataForFileID); + +const enqueueUpdates = async (file: EnteFile) => { + const fileID = file.id; + const fileType = file.metadata.fileType; + + const update = (itemData: Partial) => { + // Use the file's caption as its alt text (in addition to using it as + // the visible caption). + const alt = fileCaption(file); + + _state.itemDataByFileID.set(file.id, { + ...itemData, + fileType, + fileID, + alt, + }); + _state.needsRefreshByFileID.get(file.id)?.(); + }; + + // Use the last best available data, but stop showing the loading indicator + // and instead show the error indicator. + const markFailed = () => { + const lastData: Partial = + _state.itemDataByFileID.get(file.id) ?? {}; + delete lastData.isContentLoading; + update({ ...lastData, fetchFailed: true }); + }; + + try { + const thumbnailURL = await downloadManager.renderableThumbnailURL(file); + // While the types don't reflect it, it is safe to use the ! (null + // assertion) here since renderableThumbnailURL can throw but will not + // return undefined by default. + const thumbnailData = await withDimensions(ensureString(thumbnailURL)); + update({ + ...thumbnailData, + isContentLoading: true, + isContentZoomable: false, + }); + } catch (e) { + // If we can't even get the thumbnail, then a network error is likely + // (download manager already has retries); in particular, it cannot be a + // format error since thumbnails are already standard JPEGs. + // + // Notify the user of the error. The entire process will be retried when + // they reopen the slide later. + // + // See: [Note: File viewer error handling] + log.error("Failed to fetch thumbnail", e); + markFailed(); + return; + } + + try { + switch (fileType) { + case FileType.image: { + const sourceURLs = + await downloadManager.renderableSourceURLs(file); + const imageURL = ensureString(sourceURLs.url); + const originalImageBlob = sourceURLs.originalImageBlob!; + const itemData = await withDimensions(imageURL); + update({ ...itemData, imageURL, originalImageBlob }); + break; + } + + case FileType.video: { + const sourceURLs = + await downloadManager.renderableSourceURLs(file); + // TODO(PS): + update({ videoURL: sourceURLs.url as string }); + break; + } + + case FileType.livePhoto: { + const sourceURLs = + await downloadManager.renderableSourceURLs(file); + const livePhotoSourceURLs = + sourceURLs.url as LivePhotoSourceURL; + const imageURL = ensureString( + await livePhotoSourceURLs.image(), + ); + const originalImageBlob = + livePhotoSourceURLs.originalImageBlob()!; + const imageData = { + ...(await withDimensions(imageURL)), + imageURL, + originalImageBlob, + }; + update(imageData); + const videoURL = await livePhotoSourceURLs.video(); + update({ ...imageData, videoURL }); + break; + } + } + } catch (e) { + // [Note: File viewer error handling] + // + // Generally, file downloads will fail because of two reasons: Network + // errors, or format errors. + // + // In the first case (network error), the `renderableSourceURLs` method + // above will throw. We will show an error indicator icon in the UI, but + // will keep showing the thumbnail. + // + // In the second case (format error), we'll get back a URL, but if a + // file conversion was needed but not possible (say, it is an + // unsupported format), we might have at our hands the original file's + // untouched URL, that the browser might not know how to render. + // + // In this case we won't get into an error state here, but PhotoSwipe + // will encounter an error when trying to render it, and it will show + // our customized error message ("This file could not be previewed"), + // but the user will still be able to download it. + log.error("Failed to fetch file", e); + markFailed(); + } +}; + +/** + * Take a image URL, determine its dimensions using browser APIs, and return the URL + * and its dimensions in a form that can directly be passed to PhotoSwipe as + * {@link ItemData}. + */ +const withDimensions = (imageURL: string): Promise> => + new Promise((resolve, reject) => { + const image = new Image(); + image.onload = () => + resolve({ + src: imageURL, + width: image.naturalWidth, + height: image.naturalHeight, + }); + image.onerror = reject; + image.src = imageURL; + }); + +/** + * Return the cached Exif data for the given {@link file}. + * + * The shape of the returned data is such that it can directly be used by the + * {@link FileInfo} sidebar. + * + * Exif extraction is not too expensive, and takes around 10-200 ms usually, so + * this can be done preemptively. As soon as we get data for a particular item + * as the user swipes through the file viewer, we extract its exif data using + * {@link updateFileInfoExifIfNeeded}. + * + * Then if the user were to open the file info sidebar for that particular file, + * the associated exif data will be returned by this function. Since the happy + * path is for synchronous use in a React component, this function synchronously + * returns the cached value (and the callback is never invoked). + * + * The user can open the file info sidebar before the original has been fetched, + * so it is possible that this function gets called before + * {@link updateFileInfoExifIfNeeded} has completed. In such cases, this + * function will synchronously return `undefined`, and then later call the + * provided {@link observer} once the extraction results are available. + */ +export const fileInfoExifForFile = ( + file: EnteFile, + observer: (exifData: FileInfoExif) => void, +) => { + const fileID = file.id; + const exifData = _state.fileInfoExifByFileID.get(fileID); + if (exifData) return exifData; + + _state.exifObserverByFileID.set(fileID, observer); + return undefined; +}; + +/** + * Update, if needed, the cached Exif data for with the given {@link itemData}. + * + * This function is expected to be called when an item is loaded as PhotoSwipe + * content. It can be safely called multiple times - it will ignore calls until + * the item has an associated {@link originalImageBlob}, and it will also ignore calls + * that are made after exif data has already been extracted. + * + * If required, it will extract the exif data from the file, massage it to a + * form suitable for use by {@link FileInfo}, and stash it in its caches, and + * notify the most recent observer for that file attached via + * {@link fileInfoExifForFile}. + * + * See also {@link forgetExifForItemData}. + */ +export const updateFileInfoExifIfNeeded = async (itemData: ItemData) => { + const { fileID, fileType, originalImageBlob } = itemData; + + // We already have it available. + if (_state.fileInfoExifByFileID.has(fileID)) return; + + const updateNotifyAndReturn = (exifData: FileInfoExif) => { + _state.fileInfoExifByFileID.set(fileID, exifData); + _state.exifObserverByFileID.get(fileID)?.(exifData); + return exifData; + }; + + // For videos, insert a placeholder. + if (fileType === FileType.video) { + return updateNotifyAndReturn(createPlaceholderFileInfoExif()); + } + + // This is not a video, but the original image is not available yet. + if (!originalImageBlob) return; + + try { + const file = new File([originalImageBlob], ""); + const tags = await extractRawExif(file); + const parsed = parseExif(tags); + return updateNotifyAndReturn({ tags, parsed }); + } catch (e) { + log.error("Failed to extract exif", e); + // Save the empty placeholder exif corresponding to the file, no point + // in unnecessarily retrying this, it will deterministically fail again. + return updateNotifyAndReturn(createPlaceholderFileInfoExif()); + } +}; + +const createPlaceholderFileInfoExif = (): FileInfoExif => ({ + tags: undefined, + parsed: undefined, +}); + +/** + * Clear any cached {@link FileInfoExif} for the given {@link ItemData}. + */ +export const forgetExifForItemData = ({ fileID }: ItemData) => { + _state.fileInfoExifByFileID.delete(fileID); + _state.exifObserverByFileID.delete(fileID); +}; + +/** + * Clear all cached {@link FileInfoExif}. + */ +export const forgetExif = () => { + _state.fileInfoExifByFileID.clear(); + _state.exifObserverByFileID.clear(); +}; diff --git a/web/packages/gallery/components/viewer/icons.tsx b/web/packages/gallery/components/viewer/icons.tsx new file mode 100644 index 0000000000..bd5bc062f4 --- /dev/null +++ b/web/packages/gallery/components/viewer/icons.tsx @@ -0,0 +1,58 @@ +/** + * @file [Note: SVG paths of MUI icons] + * + * When creating buttons for use with PhotoSwipe, we need to provide just the + * contents of the SVG element (e.g. paths) as an HTML string. + * + * Since we only need a handful, these strings were created by temporarily + * adding the following code in some existing React component to render the + * corresponding MUI icon React component to a string, and retain the path. + * + * + * import { renderToString } from "react-dom/server"; + * import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined"; + * + * console.log(renderToString()); + */ + +// The transforms are not part of the originals, they have been applied +// separately to get these icons to align with the ones built into PhotoSwipe. +const paths = { + // "@mui/icons-material/ErrorOutline" + error: '', + * outlineID: "pswp__icn-info", + * } + * + */ +export const createPSRegisterElementIconHTML = (name: "info") => ({ + isCustomSVG: true, + inner: `${paths[name]} id="pswp__icn-${name}" />`, + outlineID: `pswp__icn-${name}`, +}); diff --git a/web/packages/gallery/components/viewer/photoswipe.ts b/web/packages/gallery/components/viewer/photoswipe.ts new file mode 100644 index 0000000000..db7239bf5e --- /dev/null +++ b/web/packages/gallery/components/viewer/photoswipe.ts @@ -0,0 +1,850 @@ +/* eslint-disable */ +// @ts-nocheck + +import { pt } from "@/base/i18n"; +import log from "@/base/log"; +import type { EnteFile } from "@/media/file"; +import { FileType } from "@/media/file-type"; +import { t } from "i18next"; +import { + fileViewerDidClose, + fileViewerWillOpen, + forgetExifForItemData, + forgetFailedItemDataForFileID, + itemDataForFile, + updateFileInfoExifIfNeeded, +} from "./data-source"; +import { + type FileViewerAnnotatedFile, + type FileViewerFileAnnotation, +} from "./FileViewer"; +import { createPSRegisterElementIconHTML } from "./icons"; + +// TODO(PS): WIP gallery using upstream photoswipe +// +// Needs (not committed yet): +// yarn workspace gallery add photoswipe@^5.4.4 +// mv node_modules/photoswipe packages/new/photos/components/ps5 + +if (process.env.NEXT_PUBLIC_ENTE_WIP_PS5) { + console.warn("Using WIP upstream photoswipe"); +} else { + throw new Error("Whoa"); +} + +let PhotoSwipe; +if (process.env.NEXT_PUBLIC_ENTE_WIP_PS5) { + // TODO(PS): Comment me before merging into main. + // PhotoSwipe = require("./ps5/dist/photoswipe.esm.js").default; +} + +export interface FileViewerPhotoSwipeDelegate { + /** + * Called to obtain the latest list of files. + * + * [Note: Changes to underlying files when file viewer is open] + * + * The list of files shown by the viewer might change while the viewer is + * open. We do not actively refresh the viewer when this happens since that + * would result in the user's zoom / pan state being lost. + * + * However, we always read the latest list via the delegate, so any + * subsequent user initiated slide navigation (e.g. moving to the next + * slide) will use the new list. + */ + getFiles: () => EnteFile[]; + /** + * Return `true` if the provided file has been marked as a favorite by the + * user. + * + * The toggle favorite button will not be shown for the file if + * this callback returns `undefined`. Otherwise the return value determines + * the toggle state of the toggle favorite button for the file. + */ + isFavorite: (annotatedFile: FileViewerAnnotatedFile) => boolean | undefined; + /** + * Called when the user activates the toggle favorite action on a file. + * + * The toggle favorite button will be disabled for the file until the + * promise returned by this function returns fulfills. + * + * > Note: The caller is expected to handle any errors that occur, and + * > should not reject for foreseeable failures, otherwise the button will + * > remain in the disabled state (until the file viewer is closed). + */ + toggleFavorite: (annotatedFile: FileViewerAnnotatedFile) => Promise; + /** + * Called when the user triggers a potential action using a keyboard + * shortcut. + * + * The caller does not check if the action is valid in the current context, + * so the delegate must validate and only then perform the action if it is + * appropriate. + */ + performKeyAction: (action: "delete" | "copy" | "toggle-fullscreen") => void; +} + +type FileViewerPhotoSwipeOptions = Pick< + FileViewerProps, + "initialIndex" | "disableDownload" +> & { + /** + * `true` if we're running in the context of a logged in user, and so + * various actions that modify the file should be shown. + * + * This is the static variant of various per file annotations that control + * various modifications. If this is not `true`, then various actions like + * favorite, delete etc are never shown. If this is `true`, then their + * visibility depends on the corresponding annotation. + * + * For example, the favorite action is shown only if both this and the + * {@link showFavorite} file annotation are true. + */ + haveUser: boolean; + /** + * Dynamic callbacks. + * + * The extra level of indirection allows these to be updated without + * recreating us. + */ + delegate: FileViewerPhotoSwipeDelegate; + /** + * Called when the file viewer is closed. + */ + onClose: () => void; + /** + * Called whenever the slide is initially displayed or changes, to obtain + * various derived data for the file that is about to be displayed. + */ + onAnnotate: (file: EnteFile) => FileViewerFileAnnotation; + /** + * Called when the user activates the info action on a file. + */ + onViewInfo: (annotatedFile: FileViewerAnnotatedFile) => void; + /** + * Called when the user activates the download action on a file. + */ + onDownload: (annotatedFile: FileViewerAnnotatedFile) => void; + /** + * Called when the user activates the more action on a file. + * + * @param annotatedFile The current (annotated) file. + * + * @param imageURL If the current file has an associated non-thumbnail image + * that is being shown in the viewer, then this is set to the (object) URL + * of the image being shown. Specifically, this is the same as the + * {@link imageURL} attribute of the {@link ItemData} associated with the + * current file. + * + * @param buttonElement The more button DOM element. + */ + onMore: ( + annotatedFile: FileViewerAnnotatedFile, + imageURL: string | undefined, + buttonElement: HTMLElement, + ) => void; +}; + +/** + * The ID that is used by the "more" action button (if one is being displayed). + * + * @see also {@link moreMenuID}. + */ +export const moreButtonID = "ente-pswp-more-button"; + +/** + * The ID this is expected to be used by the more menu that is shown in response + * to the more action button being activated. + * + * @see also {@link moreButtonID}. + */ +export const moreMenuID = "ente-pswp-more-menu"; + +/** + * A wrapper over {@link PhotoSwipe} to tailor its interface for use by our file + * viewer. + * + * This is somewhat akin to the {@link PhotoSwipeLightbox}, except this doesn't + * have any UI of its own, it only modifies PhotoSwipe Core's behaviour. + * + * [Note: PhotoSwipe] + * + * PhotoSwipe is a library that behaves similarly to the OG "lightbox" image + * gallery JavaScript component from the middle ages. + * + * We don't need the lightbox functionality since we already have our own + * thumbnail list (the "gallery"), so we only use the "Core" PhotoSwipe module + * as our image viewer component. + * + * When the user clicks on one of the thumbnails in our gallery, we make the + * root PhotoSwipe component visible. Within the DOM this is a dialog-like div + * that takes up the entire viewport, shows the image, various controls etc. + * + * Documentation: https://photoswipe.com/. + */ +export class FileViewerPhotoSwipe { + /** + * The PhotoSwipe instance which we wrap. + */ + private pswp: PhotoSwipe; + /** + * The options with which we were initialized. + */ + private opts: Pick; + /** + * An interval that invokes a periodic check of whether we should the hide + * controls if the user does not perform any pointer events for a while. + */ + private autoHideCheckIntervalId: ReturnType | undefined; + /** + * The time the last activity occurred. Used in tandem with + * {@link autoHideCheckIntervalId} to implement the auto hiding of controls + * when the user stops moving the pointer for a while. + * + * Apart from a date, this can also be: + * + * - "already-hidden" if controls have already been hidden, say by a + * bgClickAction. + * + * - "auto-hidden" if controls were hidden by us because of inactivity. + */ + private lastActivityDate: Date | "auto-hidden" | "already-hidden"; + /** + * Derived data about the currently displayed file. + * + * This is recomputed on-demand (by using the {@link onAnnotate} callback) + * each time the slide changes, and cached until the next slide change. + * + * Instead of accessing this property directly, code should funnel through + * the `activeFileAnnotation` helper function defined in the constructor + * scope. + */ + private activeFileAnnotation: FileViewerFileAnnotation | undefined; + /** + * IDs of files for which a there is a favorite update in progress. + */ + private pendingFavoriteUpdates = new Set(); + + constructor({ + initialIndex, + disableDownload, + haveUser, + delegate, + onClose, + onAnnotate, + onViewInfo, + onDownload, + onMore, + }: FileViewerPhotoSwipeOptions) { + this.opts = { disableDownload }; + this.lastActivityDate = new Date(); + + const pswp = new PhotoSwipe({ + // Opaque background. + bgOpacity: 1, + // The default, "zoom", cannot be used since we're not animating + // from a thumbnail, so effectively "fade" is in effect anyway. Set + // it still, just for and explicitness and documentation. + showHideAnimationType: "fade", + // The default imageClickAction is "zoom-or-close". When the image + // is small and cannot be zoomed into further (which is common when + // just the thumbnail has been loaded), this causes PhotoSwipe to + // close. Disable this behaviour. + clickToCloseNonZoomable: false, + // The default `bgClickAction` is "close", but it is not always + // apparent where the background is and where the controls are, + // since everything is black, and so accidentally closing PhotoSwipe + // is easy. + // + // Disable this behaviour, instead repurposing this action to behave + // the same as the `tapAction` ("tap on PhotoSwipe viewport + // content") and toggle the visibility of UI controls (We also have + // auto hide based on mouse activity, but that would not have any + // effect on touch devices) + bgClickAction: "toggle-controls", + // At least on macOS, manual zooming with the trackpad is very + // cumbersome (possibly because of the small multiplier in the + // PhotoSwipe source, but I'm not sure). The other option to do a + // manual zoom is to scroll (e.g. with the trackpad) but with the + // CTRL key pressed, however on macOS this invokes the system zoom + // if enabled in accessibility settings. + // + // Taking a step back though, the PhotoSwipe viewport is fixed, so + // we can just directly map wheel / trackpad scrolls to zooming. + wheelToZoom: true, + // Chrome yells about incorrectly mixing focus and aria-hidden if we + // leave this at the default (true) and then swipe between slides + // fast, or show MUI drawers etc. + // + // See: [Note: Overzealous Chrome? Complicated ARIA?], but time with + // a different library. + trapFocus: false, + // Set the index within files that we should open to. Subsequent + // updates to the index will be tracked by PhotoSwipe internally. + index: initialIndex, + // TODO(PS): padding option? for handling custom title bar. + // TODO(PS): will we need this? + mainClass: "pswp-ente", + // Translated variants + closeTitle: t("close_key"), + zoomTitle: t("zoom_in_out_key") /* TODO(PS): Add "(scroll)" */, + arrowPrevTitle: t("previous_key"), + arrowNextTitle: t("next_key"), + // TODO(PS): Move to translations (unpreviewable_file_notification). + errorMsg: "This file could not be previewed", + }); + + this.pswp = pswp; + + // Various helper routines to obtain the file at `currIndex`. + + const currentFile = () => delegate.getFiles()[pswp.currIndex]!; + + const currentAnnotatedFile = () => { + const file = currentFile(); + let annotation = this.activeFileAnnotation; + if (annotation?.fileID != file.id) { + annotation = onAnnotate(file); + this.activeFileAnnotation = annotation; + } + return { + file, + // The above condition implies that annotation can never be + // undefined, but it doesn't seem to be enough to convince + // TypeScript. Writing the condition in a more unnatural way + // `(!(annotation && annotation?.fileID == file.id))` works, but + // instead we use a non-null assertion here. + annotation: annotation!, + }; + }; + + const currentFileAnnotation = () => currentAnnotatedFile().annotation; + + // Provide data about slides to PhotoSwipe via callbacks + // https://photoswipe.com/data-sources/#dynamically-generated-data + + pswp.addFilter("numItems", () => delegate.getFiles().length); + + pswp.addFilter("itemData", (_, index) => { + const files = delegate.getFiles(); + const file = files[index]!; + + let itemData = itemDataForFile(file, () => + pswp.refreshSlideContent(index), + ); + + const { videoURL, ...rest } = itemData; + if (itemData.fileType === FileType.video && videoURL) { + const disableDownload = !!this.opts.disableDownload; + itemData = { + ...rest, + html: videoHTML(videoURL, disableDownload), + }; + } + + log.debug(() => ["[viewer]", { index, itemData, file }]); + + if (this.lastActivityDate != "already-hidden") + this.lastActivityDate = new Date(); + + return itemData; + }); + + pswp.addFilter("isContentLoading", (isLoading, content) => { + return content.data.isContentLoading ?? isLoading; + }); + + pswp.addFilter("isContentZoomable", (isZoomable, content) => { + return content.data.isContentZoomable ?? isZoomable; + }); + + pswp.addFilter("preventPointerEvent", (preventPointerEvent) => { + // There was a pointer event. We don't care which one, we just use + // this as a hook to show the UI again (if needed), and update our + // last activity date. + this.onPointerActivity(); + return preventPointerEvent; + }); + + pswp.on("contentAppend", (e) => { + const { fileType, videoURL } = e.content.data; + if (fileType !== FileType.livePhoto) return; + if (!videoURL) return; + + // This slide is displaying a live photo. Append a video element to + // show its video part. + + const img = e.content.element; + const video = createElementFromHTMLString( + livePhotoVideoHTML(videoURL), + ); + const container = e.content.slide.container; + container.style = "position: relative"; + container.appendChild(video); + // Set z-index to 1 to keep it on top, and set pointer-events to + // none to pass the clicks through. + video.style = + "position: absolute; top: 0; left: 0; z-index: 1; pointer-events: none;"; + + // Size it to the underlying image. + video.style.width = img.style.width; + video.style.height = img.style.height; + }); + + pswp.on("imageSizeChange", ({ content, width, height }) => { + if (content.data.fileType !== FileType.livePhoto) return; + + // This slide is displaying a live photo. Resize the size of the + // video element to match that of the image. + + const video = + content.slide.container.getElementsByTagName("video")[0]; + if (!video) { + // We might have been called before "contentAppend". + return; + } + + video.style.width = `${width}px`; + video.style.height = `${height}px`; + }); + + pswp.on("contentDeactivate", (e) => { + // Reset failures, if any, for this file so that the fetch is tried + // again when we come back to it^. + // + // ^ Note that because of how the preloading works, this will have + // an effect (i.e. the retry will happen) only if the user moves + // more than 2 slides and then back, or if they reopen the viewer. + // + // See: [Note: File viewer error handling] + const fileID = e.content?.data?.fileID; + if (fileID) forgetFailedItemDataForFileID(fileID); + + // Pause the video element, if any, when we move away from the + // slide. + const video = + e.content?.slide?.container?.getElementsByTagName("video")[0]; + video?.pause(); + }); + + pswp.on("contentActivate", (e) => { + // Undo the effect of a previous "contentDeactivate" if it was + // displaying a live photo. + if (e.content?.slide.data?.fileType === FileType.livePhoto) { + e.content?.slide?.container + ?.getElementsByTagName("video")[0] + ?.play(); + } + }); + + pswp.on("loadComplete", (e) => + updateFileInfoExifIfNeeded(e.content.data), + ); + + pswp.on("change", (e) => { + const itemData = this.pswp.currSlide.content.data; + updateFileInfoExifIfNeeded(itemData); + }); + + pswp.on("contentDestroy", (e) => forgetExifForItemData(e.content.data)); + + // State needed to hide the caption when a video is playing. + let videoElement: HTMLVideoElement | undefined; + let onVideoPlayback: EventHandler | undefined; + let captionElementRef: HTMLElement | undefined; + + pswp.on("change", (e) => { + const itemData = this.pswp.currSlide.content.data; + + // Clear existing listeners, if any. + if (videoElement && onVideoPlayback) { + videoElement.removeEventListener("play", onVideoPlayback); + videoElement.removeEventListener("pause", onVideoPlayback); + videoElement.removeEventListener("ended", onVideoPlayback); + videoElement = undefined; + onVideoPlayback = undefined; + } + + // Reset. + showIf(captionElementRef, true); + + // Attach new listeners, if needed. + if (itemData.fileType == FileType.video) { + const contentElement = pswp.currSlide.content.element; + videoElement = contentElement.getElementsByTagName("video")[0]; + if (videoElement) { + onVideoPlayback = (e) => { + showIf(captionElementRef, !!videoElement?.paused); + }; + videoElement.addEventListener("play", onVideoPlayback); + videoElement.addEventListener("pause", onVideoPlayback); + videoElement.addEventListener("ended", onVideoPlayback); + } + } + }); + + // The PhotoSwipe dialog has being closed and the animations have + // completed. + pswp.on("destroy", () => { + this.clearAutoHideIntervalIfNeeded(); + fileViewerDidClose(); + // Let our parent know that we have been closed. + onClose(); + }); + + const handleViewInfo = () => onViewInfo(currentAnnotatedFile()); + + let favoriteButtonElement: HTMLButtonElement | undefined; + let unfavoriteButtonElement: HTMLButtonElement | undefined; + + const toggleFavorite = async () => { + const af = currentAnnotatedFile(); + this.pendingFavoriteUpdates.add(af.file.id); + favoriteButtonElement.disabled = true; + unfavoriteButtonElement.disabled = true; + await delegate.toggleFavorite(af); + this.pendingFavoriteUpdates.delete(af.file.id); + // TODO: We reload the entire slide instead of just updating + // the button state. This is because there are two buttons, + // instead of a single button toggling between two states + // e.g. like the zoom button. + // + // To fix this, a single button can be achieved by moving + // the fill of the heart as a layer. + this.refreshCurrentSlideContent(); + }; + + const handleToggleFavorite = () => void toggleFavorite(); + + const handleToggleFavoriteIfEnabled = () => { + if (haveUser) handleToggleFavorite(); + }; + + const handleDownload = () => onDownload(currentAnnotatedFile()); + + const handleDownloadIfEnabled = () => { + if (!!currentFileAnnotation().showDownload) handleDownload(); + }; + + const showIf = (element: HTMLElement, condition: boolean) => + condition + ? element.classList.remove("pswp__hidden") + : element.classList.add("pswp__hidden"); + + // Add our custom UI elements to inside the PhotoSwipe dialog. + // + // API docs for registerElement: + // https://photoswipe.com/adding-ui-elements/#uiregisterelement-api + // + // The "order" prop is used to position items. Some landmarks: + // - counter: 5 + // - preloader: 7 + // - zoom: 10 + // - close: 20 + pswp.on("uiRegister", () => { + // Move the zoom button to the left so that it is in the same place + // as the other items like preloader or the error indicator that + // come and go as files get loaded. + // + // We cannot use the PhotoSwipe "uiElement" filter to modify the + // order since that only allows us to edit the DOM element, not the + // underlying UI element data. + pswp.ui.uiElementsData.find((e) => e.name == "zoom").order = 6; + + // Register our custom elements... + + pswp.ui.registerElement({ + name: "error", + order: 6, + html: createPSRegisterElementIconHTML("error"), + onInit: (errorElement, pswp) => { + pswp.on("change", () => { + const { fetchFailed, isContentLoading } = + pswp.currSlide.content.data; + errorElement.classList.toggle( + "pswp__error--active", + !!fetchFailed && !isContentLoading, + ); + }); + }, + }); + + if (haveUser) { + const showFavoriteIf = ( + buttonElement: HTMLButtonElement, + value: boolean, + ) => { + const af = currentAnnotatedFile(); + const isFavorite = delegate.isFavorite(af); + showIf( + buttonElement, + af.annotation.showFavorite && isFavorite === value, + ); + buttonElement.disabled = this.pendingFavoriteUpdates.has( + af.file.id, + ); + }; + + // Only one of these two ("favorite" or "unfavorite") will end + // up being shown, so they can safely share the same order. + pswp.ui.registerElement({ + name: "favorite", + title: t("favorite"), + order: 8, + isButton: true, + html: createPSRegisterElementIconHTML("favorite"), + onClick: handleToggleFavorite, + onInit: (buttonElement) => + pswp.on("change", () => + showFavoriteIf(buttonElement, false), + ), + }); + pswp.ui.registerElement({ + name: "unfavorite", + title: t("unfavorite"), + order: 8, + isButton: true, + html: createPSRegisterElementIconHTML("unfavorite"), + onClick: handleToggleFavorite, + onInit: (buttonElement) => + pswp.on("change", () => + showFavoriteIf(buttonElement, true), + ), + }); + } else { + // When we don't have a user (i.e. in the context of public + // albums), the download button is shown (if enabled for that + // album) instead of the favorite button as the first action. + // + // It can thus also use the same order as fav/unfav. + pswp.ui.registerElement({ + name: "download", + title: t("download"), + order: 8, + isButton: true, + html: createPSRegisterElementIconHTML("download"), + onClick: handleDownload, + onInit: (buttonElement) => + pswp.on("change", () => + showIf( + buttonElement, + currentFileAnnotation().showDownload == "bar", + ), + ), + }); + } + + pswp.ui.registerElement({ + name: "info", + title: t("info"), + order: 9, + isButton: true, + html: createPSRegisterElementIconHTML("info"), + onClick: handleViewInfo, + }); + + pswp.ui.registerElement({ + name: "more", + // TODO(PS): + title: pt("More"), + order: 16, + isButton: true, + html: createPSRegisterElementIconHTML("more"), + onInit: (buttonElement) => { + buttonElement.setAttribute("id", moreButtonID); + buttonElement.setAttribute("aria-haspopup", "true"); + }, + onClick: (e) => { + const buttonElement = e.target; + // See also: `resetMoreMenuButtonOnMenuClose`. + buttonElement.setAttribute("aria-controls", moreMenuID); + buttonElement.setAttribute("aria-expanded", true); + onMore( + currentAnnotatedFile(), + pswp.currSlide.content.data.imageURL, + buttonElement, + ); + }, + }); + + pswp.ui.registerElement({ + name: "caption", + // Arbitrary order towards the end (it doesn't matter anyways + // since we're absolutely positioned). + order: 30, + appendTo: "root", + tagName: "p", + onInit: (captionElement, pswp) => { + captionElementRef = captionElement; + pswp.on("change", () => { + const { fileType, alt } = pswp.currSlide.content.data; + captionElement.innerText = alt ?? ""; + captionElement.style.visibility = alt + ? "visible" + : "hidden"; + // Add extra offset for video captions so that they do + // not overlap with the video controls. The constant is + // an ad-hoc value that looked okay-ish across browsers. + captionElement.style.bottom = + fileType === FileType.video ? "36px" : "0"; + }); + }, + }); + }); + + // Modify the default UI elements. + pswp.addFilter("uiElement", (element, data) => { + if (element.name == "preloader") { + // TODO(PS): Left as an example. For now, this is customized in + // the CSS. + } + return element; + }); + + // Some actions routed via the delegate + + const handleDelete = () => delegate.performKeyAction("delete"); + + const handleCopy = () => delegate.performKeyAction("copy"); + + const handleToggleFullscreen = () => + delegate.performKeyAction("toggle-fullscreen"); + + pswp.on("keydown", (e, z) => { + const key = e.originalEvent.key ?? ""; + const cb = (() => { + switch (key.toLowerCase()) { + case "l": + return handleToggleFavoriteIfEnabled; + case "d": + return handleDownloadIfEnabled; + case "i": + return handleViewInfo; + case "f": + return handleToggleFullscreen; + } + return undefined; + })(); + cb?.(); + }); + + // Let our data source know that we're about to open. + fileViewerWillOpen(); + + // Initializing PhotoSwipe adds it to the DOM as a dialog-like div with + // the class "pswp". + pswp.init(); + + this.autoHideCheckIntervalId = setInterval(() => { + this.autoHideIfInactive(); + }, 1000); + } + + /** + * Close this instance of {@link FileViewerPhotoSwipe} if it hasn't itself + * initiated the close. + * + * This instance **cannot** be used after this function has been called. + */ + closeIfNeeded() { + // Closing PhotoSwipe removes it from the DOM. + // + // This will only have an effect if we're being closed externally (e.g. + // if the user selects an album in the file info). + // + // If this cleanup function is running in the sequence where we were + // closed internally (e.g. the user activated the close button within + // the file viewer), then PhotoSwipe will ignore this extra close. + this.pswp.close(); + } + + /** + * Reload the current slide, asking the data source for its data afresh. + */ + refreshCurrentSlideContent() { + this.pswp.refreshSlideContent(this.pswp.currIndex); + } + + private clearAutoHideIntervalIfNeeded() { + if (this.autoHideCheckIntervalId) { + clearInterval(this.autoHideCheckIntervalId); + this.autoHideCheckIntervalId = undefined; + } + } + + private onPointerActivity() { + if (this.lastActivityDate == "already-hidden") return; + if (this.lastActivityDate == "auto-hidden") this.showUIControls(); + this.lastActivityDate = new Date(); + } + + private autoHideIfInactive() { + if (this.lastActivityDate == "already-hidden") return; + if (this.lastActivityDate == "auto-hidden") return; + if (Date.now() - this.lastActivityDate.getTime() > 5000 /* 5s */) { + if (this.areUIControlsVisible()) { + this.hideUIControlsIfNotFocused(); + this.lastActivityDate = "auto-hidden"; + } else { + this.lastActivityDate = "already-hidden"; + } + } + } + + private areUIControlsVisible() { + return this.pswp.element.classList.contains("pswp--ui-visible"); + } + + private showUIControls() { + this.pswp.element.classList.add("pswp--ui-visible"); + } + + private hideUIControlsIfNotFocused() { + // Check if the current keyboard focus is on any of the UI controls. + // + // By default, the pswp root element takes up the keyboard focus, so we + // check if the currently focused element is still the PhotoSwipe dialog + // (if so, this means we're not focused on a specific control). + const isDefaultFocus = document + .querySelector(":focus-visible") + ?.classList.contains("pswp"); + if (!isDefaultFocus) { + // The user focused (e.g. via keyboard tabs) to a specific UI + // element. Skip auto hiding. + return; + } + + // TODO(PS): Commented during testing + // this.pswp.element.classList.remove("pswp--ui-visible"); + } +} + +const videoHTML = (url: string, disableDownload: boolean) => ` + +`; + +const livePhotoVideoHTML = (videoURL: string) => ` + +`; + +const createElementFromHTMLString = (htmlString: string) => { + const template = document.createElement("template"); + // Excess whitespace causes excess DOM nodes, causing our firstChild to not + // be what we wanted them to be. + template.innerHTML = htmlString.trim(); + return template.content.firstChild; +}; + +/** + * Update the ARIA attributes for the button that controls the more menu when + * the menu is closed. + */ +export const resetMoreMenuButtonOnMenuClose = (buttonElement: HTMLElement) => { + buttonElement.removeAttribute("aria-controls"); + buttonElement.removeAttribute("aria-expanded"); +}; diff --git a/web/packages/gallery/package.json b/web/packages/gallery/package.json index 24e07a5a6f..3c4669150d 100644 --- a/web/packages/gallery/package.json +++ b/web/packages/gallery/package.json @@ -3,10 +3,17 @@ "version": "0.0.0", "private": true, "dependencies": { + "@/base": "*", + "@/utils": "*", "@ffmpeg/ffmpeg": "^0.12.10", - "bs58": "^6.0.0" + "bs58": "^6.0.0", + "exifreader": "^4.26.1", + "leaflet": "^1.9.4", + "leaflet-defaulticon-compatibility": "^0.1.2", + "react-dropzone": "14.2.10" }, "devDependencies": { - "@/build-config": "*" + "@/build-config": "*", + "@types/leaflet": "^1.9.16" } } diff --git a/web/packages/gallery/services/download.ts b/web/packages/gallery/services/download.ts index bce3d92bd0..c0dc2b1ce6 100644 --- a/web/packages/gallery/services/download.ts +++ b/web/packages/gallery/services/download.ts @@ -26,6 +26,7 @@ import { decodeLivePhoto } from "@/media/live-photo"; export interface LivePhotoSourceURL { image: () => Promise; + originalImageBlob: () => Blob | undefined; video: () => Promise; } @@ -52,6 +53,7 @@ export interface LoadedLivePhotoSourceURL { */ export interface RenderableSourceURLs { url: string | LivePhotoSourceURL | LoadedLivePhotoSourceURL; + originalImageBlob?: Blob | undefined; type: "normal" | "livePhoto"; /** * `true` if there is potential conversion that can still be applied. @@ -217,6 +219,9 @@ class DownloadManager { * * The returned URL is actually an object URL, but it should not be revoked * since the download manager caches it for future use. + * + * If {@link cachedOnly} is false (the default), then this method will + * indicate errors by throwing but will never return `undefined`. */ async renderableThumbnailURL( file: EnteFile, @@ -267,7 +272,9 @@ class DownloadManager { } private downloadThumbnail = async (file: EnteFile) => { - const encryptedData = await this._downloadThumbnail(file); + const encryptedData = await wrapErrors(() => + this._downloadThumbnail(file), + ); const decryptionHeader = file.thumbnail.decryptionHeader; return decryptThumbnail({ encryptedData, decryptionHeader }, file.key); }; @@ -381,13 +388,15 @@ class DownloadManager { ): Promise | null> { log.info(`download attempted for file id ${file.id}`); - const res = await this._downloadFile(file); + const res = await wrapErrors(() => this._downloadFile(file)); if ( file.metadata.fileType === FileType.image || file.metadata.fileType === FileType.livePhoto ) { - const encryptedData = new Uint8Array(await res.arrayBuffer()); + const encryptedData = new Uint8Array( + await wrapErrors(() => res.arrayBuffer()), + ); const decrypted = await decryptStreamBytes( { @@ -427,7 +436,9 @@ class DownloadManager { do { // done is a boolean and value is an Uint8Array. When done // is true value will be empty. - const { done, value } = await reader.read(); + const { done, value } = await wrapErrors(() => + reader.read(), + ); let data: Uint8Array; if (done) { @@ -518,6 +529,52 @@ class DownloadManager { */ export const downloadManager = new DownloadManager(); +/** + * A custom Error that is thrown if a download fails during network I/O. + * + * [Note: Identifying network related errors during download] + * + * We dealing with code that touches the network, we often don't specifically + * care about the specific error - there is a lot that can go wrong when a + * network is involved - but need to identify if an error was in the network + * related phase of an action, since these are usually transient and can be + * dealt with more softly than other errors. + * + * To that end, network related phases of download operations are wrapped in + * catches that intercept the error and wrap it in our custom + * {@link NetworkDownloadError} whose presence can be checked using the + * {@link isNetworkDownloadError} predicate. + */ +export class NetworkDownloadError extends Error { + error: unknown; + + constructor(e: unknown) { + super( + `NetworkDownloadError: ${e instanceof Error ? e.message : String(e)}`, + ); + + // Cargo culted from + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error#custom_error_types + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (Error.captureStackTrace) + Error.captureStackTrace(this, NetworkDownloadError); + + this.error = e; + } +} + +export const isNetworkDownloadError = (e: unknown) => + e instanceof NetworkDownloadError; + +/** + * A helper function to convert all rejections of the given promise {@link op} + * into {@link NetworkDownloadError}s. + */ +const wrapErrors = (op: () => Promise) => + op().catch((e: unknown) => { + throw new NetworkDownloadError(e); + }); + /** * Create and return a {@link RenderableSourceURLs} for the given {@link file}, * where {@link originalFileURLPromise} is a promise that resolves with an @@ -542,6 +599,9 @@ const createRenderableSourceURLs = async ( : undefined; let url: RenderableSourceURLs["url"] | undefined; + let originalImageBlob: + | RenderableSourceURLs["originalImageBlob"] + | undefined; let type: RenderableSourceURLs["type"] = "normal"; let mimeType: string | undefined; let canForceConvert = false; @@ -552,6 +612,7 @@ const createRenderableSourceURLs = async ( const convertedBlob = await renderableImageBlob(fileBlob, fileName); const convertedURL = existingOrNewObjectURL(convertedBlob); url = convertedURL; + originalImageBlob = fileBlob; mimeType = convertedBlob.type; break; } @@ -588,6 +649,7 @@ const createRenderableSourceURLs = async ( // @ts-ignore return { url: url!, + originalImageBlob, type, mimeType, canForceConvert, @@ -612,6 +674,15 @@ async function getRenderableLivePhotoURL( } }; + const getOriginalImageBlob = () => { + try { + return new Blob([livePhoto.imageData]); + } catch { + //ignore and return null + return undefined; + } + }; + const getRenderableLivePhotoVideoURL = async () => { try { const videoBlob = new Blob([livePhoto.videoData]); @@ -630,6 +701,7 @@ async function getRenderableLivePhotoURL( return { image: getRenderableLivePhotoImageURL, + originalImageBlob: getOriginalImageBlob, video: getRenderableLivePhotoVideoURL, }; } diff --git a/web/packages/new/photos/services/exif.ts b/web/packages/gallery/services/exif.ts similarity index 92% rename from web/packages/new/photos/services/exif.ts rename to web/packages/gallery/services/exif.ts index b880ebb7b0..6ed42a4159 100644 --- a/web/packages/new/photos/services/exif.ts +++ b/web/packages/gallery/services/exif.ts @@ -362,31 +362,21 @@ const parseIPTCDate = ( /** * Parse the width and height of the image from the metadata embedded in the - * file. + * file, taking into account any orientation tags that are also present. */ const parseDimensions = (tags: RawExifTags) => { // Go through all possiblities in order, returning the first pair with both // the width and height defined, and non-zero. + const pair = (w: number | undefined, h: number | undefined) => w && h ? { width: w, height: h } : undefined; - return ( + // 1. Use the width and height from the file itself (e.g. JPEG data). + + let wh = pair( - tags.exif?.ImageWidth?.value, - /* The Exif spec calls it ImageLength, not ImageHeight. */ - tags.exif?.ImageLength?.value, - ) ?? - pair( - tags.exif?.PixelXDimension?.value, - tags.exif?.PixelYDimension?.value, - ) ?? - pair( - parseXMPNum(tags.xmp?.ImageWidth), - parseXMPNum(tags.xmp?.ImageLength), - ) ?? - pair( - parseXMPNum(tags.xmp?.PixelXDimension), - parseXMPNum(tags.xmp?.PixelYDimension), + tags.file?.["Image Width"]?.value, + tags.file?.["Image Height"]?.value, ) ?? pair( tags.pngFile?.["Image Width"]?.value, @@ -396,12 +386,83 @@ const parseDimensions = (tags: RawExifTags) => { tags.gif?.["Image Width"]?.value, tags.gif?.["Image Height"]?.value, ) ?? - pair(tags.riff?.ImageWidth?.value, tags.riff?.ImageHeight?.value) ?? + pair(tags.riff?.ImageWidth?.value, tags.riff?.ImageHeight?.value); + if (wh) { + return wh; + } + + // 2. Exif dimensions, taking Orientation also into account if needed. + + wh = pair( - tags.file?.["Image Width"]?.value, - tags.file?.["Image Height"]?.value, - ) - ); + tags.exif?.ImageWidth?.value, + /* The Exif spec calls it ImageLength, not ImageHeight. */ + tags.exif?.ImageLength?.value, + ) ?? + pair( + tags.exif?.PixelXDimension?.value, + tags.exif?.PixelYDimension?.value, + ); + if (wh) { + // Exif Orientation tags can have the following values: + // + // 1 = Horizontal (normal) + // 2 = Mirror horizontal + // 3 = Rotate 180 + // 4 = Mirror vertical + // 5 = Mirror horizontal and rotate 270 CW + // 6 = Rotate 90 CW + // 7 = Mirror horizontal and rotate 90 CW + // 8 = Rotate 270 CW + // + // Ref: https://exiftool.org/TagNames/EXIF.html + + let swap = false; + + switch (tags.exif?.Orientation?.value) { + case 5: + case 6: + case 7: + case 8: + swap = true; + break; + } + + return swap ? { width: wh.height, height: wh.width } : wh; + } + + // 3. XMP dimensions, taking Orientation also into account if needed. + + wh = + pair( + parseXMPNum(tags.xmp?.ImageWidth), + parseXMPNum(tags.xmp?.ImageLength), + ) ?? + pair( + parseXMPNum(tags.xmp?.PixelXDimension), + parseXMPNum(tags.xmp?.PixelYDimension), + ); + + if (wh) { + // The Orientation present in XMP (as a TIFF tag) also uses the same + // constants as Exif above. + // + // Ref: https://exiftool.org/TagNames/XMP.html + + let swap = false; + + switch (tags.xmp?.Orientation?.value) { + case "5": + case "6": + case "7": + case "8": + swap = true; + } + + return swap ? { width: wh.height, height: wh.width } : wh; + } + + return undefined; }; /** diff --git a/web/packages/gallery/services/ffmpeg/index.ts b/web/packages/gallery/services/ffmpeg/index.ts index 2141dfec23..18303461c3 100644 --- a/web/packages/gallery/services/ffmpeg/index.ts +++ b/web/packages/gallery/services/ffmpeg/index.ts @@ -1,17 +1,17 @@ import { ensureElectron } from "@/base/electron"; import log from "@/base/log"; import type { Electron } from "@/base/types/ipc"; +import { + toDataOrPathOrZipEntry, + type DesktopUploadItem, + type UploadItem, +} from "@/gallery/services/upload"; import { readConvertToMP4Done, readConvertToMP4Stream, writeConvertToMP4Stream, } from "@/gallery/utils/native-stream"; import { parseMetadataDate, type ParsedMetadata } from "@/media/file-metadata"; -import { - toDataOrPathOrZipEntry, - type DesktopUploadItem, - type UploadItem, -} from "@/new/photos/services/upload/types"; import { ffmpegPathPlaceholder, inputPathPlaceholder, diff --git a/web/apps/photos/src/services/fileService.ts b/web/packages/gallery/services/file.ts similarity index 63% rename from web/apps/photos/src/services/fileService.ts rename to web/packages/gallery/services/file.ts index f948e45067..ac2a7a30b2 100644 --- a/web/apps/photos/src/services/fileService.ts +++ b/web/packages/gallery/services/file.ts @@ -1,11 +1,18 @@ +/* TODO: Audit this file */ +/* eslint-disable @typescript-eslint/ban-ts-comment */ + import { encryptMetadataJSON } from "@/base/crypto"; import { apiURL } from "@/base/origins"; -import { - type EncryptedMagicMetadata, +import { updateMagicMetadata } from "@/gallery/services/magic-metadata"; +import type { + EncryptedMagicMetadata, EnteFile, + FilePublicMagicMetadata, + FilePublicMagicMetadataProps, FileWithUpdatedMagicMetadata, FileWithUpdatedPublicMagicMetadata, } from "@/media/file"; +import { mergeMetadata } from "@/media/file"; import HTTPService from "@ente/shared/network/HTTPService"; import { getToken } from "@ente/shared/storage/localStorage/helpers"; @@ -14,7 +21,7 @@ export interface UpdateMagicMetadataRequest { magicMetadata: EncryptedMagicMetadata; } -export interface BulkUpdateMagicMetadataRequest { +interface BulkUpdateMagicMetadataRequest { metadataList: UpdateMagicMetadataRequest[]; } @@ -48,6 +55,7 @@ export const updateFileMagicMetadata = async ( await HTTPService.put( await apiURL("/files/magic-metadata"), reqBody, + // @ts-ignore null, { "X-Auth-Token": token, @@ -69,6 +77,7 @@ export const updateFilePublicMagicMetadata = async ( ): Promise => { const token = getToken(); if (!token) { + // @ts-ignore return; } const reqBody: BulkUpdateMagicMetadataRequest = { metadataList: [] }; @@ -94,6 +103,7 @@ export const updateFilePublicMagicMetadata = async ( await HTTPService.put( await apiURL("/files/public-magic-metadata"), reqBody, + // @ts-ignore null, { "X-Auth-Token": token, @@ -109,3 +119,55 @@ export const updateFilePublicMagicMetadata = async ( }), ); }; + +export async function changeFileName( + file: EnteFile, + editedName: string, +): Promise { + const updatedPublicMagicMetadataProps: FilePublicMagicMetadataProps = { + editedName, + }; + + const updatedPublicMagicMetadata: FilePublicMagicMetadata = + await updateMagicMetadata( + updatedPublicMagicMetadataProps, + file.pubMagicMetadata, + file.key, + ); + const updateResult = await updateFilePublicMagicMetadata([ + { file, updatedPublicMagicMetadata }, + ]); + // @ts-ignore + return updateResult[0]; +} + +export async function changeCaption( + file: EnteFile, + caption: string, +): Promise { + const updatedPublicMagicMetadataProps: FilePublicMagicMetadataProps = { + caption, + }; + + const updatedPublicMagicMetadata: FilePublicMagicMetadata = + await updateMagicMetadata( + updatedPublicMagicMetadataProps, + file.pubMagicMetadata, + file.key, + ); + const updateResult = await updateFilePublicMagicMetadata([ + { file, updatedPublicMagicMetadata }, + ]); + // @ts-ignore + return updateResult[0]; +} + +export function updateExistingFilePubMetadata( + existingFile: EnteFile, + updatedFile: EnteFile, +) { + // @ts-ignore + existingFile.pubMagicMetadata = updatedFile.pubMagicMetadata; + // @ts-ignore + existingFile.metadata = mergeMetadata([existingFile])[0].metadata; +} diff --git a/web/packages/new/photos/services/magic-metadata.ts b/web/packages/gallery/services/magic-metadata.ts similarity index 100% rename from web/packages/new/photos/services/magic-metadata.ts rename to web/packages/gallery/services/magic-metadata.ts diff --git a/web/packages/gallery/services/upload.ts b/web/packages/gallery/services/upload.ts index 868b9caf3d..e6b1040d73 100644 --- a/web/packages/gallery/services/upload.ts +++ b/web/packages/gallery/services/upload.ts @@ -1,5 +1,6 @@ import log from "@/base/log"; import { customAPIOrigin } from "@/base/origins"; +import type { ZipItem } from "@/base/types/ipc"; import { nullToUndefined } from "@/utils/transform"; import { z } from "zod"; @@ -28,6 +29,83 @@ export const resetUploadState = () => { _state = new UploadState(); }; +/** + * An item to upload is one of the following: + * + * 1. A file drag-and-dropped or selected by the user when we are running in the + * web browser. These is the {@link File} case. + * + * 2. A file drag-and-dropped or selected by the user when we are running in the + * context of our desktop app. In such cases, we also have the absolute path + * of the file in the user's local file system. This is the + * {@link FileAndPath} case. + * + * 3. A file path programmatically requested by the desktop app. For example, we + * might be resuming a previously interrupted upload after an app restart + * (thus we no longer have access to the {@link File} from case 2). Or we + * could be uploading a file this is in one of the folders the user has asked + * us to watch for changes. This is the `string` case. + * + * 4. A file within a zip file on the user's local file system. This too is only + * possible when we are running in the context of our desktop app. The user + * might have drag-and-dropped or selected a zip file, or it might be a zip + * file that they'd previously selected but we now are resuming an + * interrupted upload of. Either ways, what we have is a tuple containing the + * (path to zip file, and the name of an entry within that zip file). This is + * the {@link ZipItem} case. + * + * Also see: [Note: Reading a UploadItem]. + */ +export type UploadItem = File | FileAndPath | string | ZipItem; + +/** + * When we are running in the context of our desktop app, we have access to the + * absolute path of {@link File} objects. This convenience type clubs these two + * bits of information, saving us the need to query the path again and again + * using the {@link getPathForFile} method of {@link Electron}. + */ +export interface FileAndPath { + file: File; + path: string; +} + +/** + * The of cases of {@link UploadItem} that apply when we're running in the + * context of our desktop app. + */ +export type DesktopUploadItem = Exclude; + +/** + * For each of cases of {@link UploadItem} that apply when we're running in the + * context of our desktop app, return a value that can be passed to + * {@link Electron} functions over IPC. + */ +export const toDataOrPathOrZipEntry = (desktopUploadItem: DesktopUploadItem) => + typeof desktopUploadItem == "string" || Array.isArray(desktopUploadItem) + ? desktopUploadItem + : desktopUploadItem.path; + +export const RANDOM_PERCENTAGE_PROGRESS_FOR_PUT = () => 90 + 10 * Math.random(); + +export type UploadPhase = + | "preparing" + | "readingMetadata" + | "uploading" + | "cancelling" + | "done"; + +export enum UPLOAD_RESULT { + FAILED, + ALREADY_UPLOADED, + UNSUPPORTED, + BLOCKED, + TOO_LARGE, + LARGER_THAN_AVAILABLE_STORAGE, + UPLOADED, + UPLOADED_WITH_STATIC_THUMBNAIL, + ADDED_SYMLINK, +} + /** * Return true to disable the upload of files via Cloudflare Workers. * diff --git a/web/packages/new/photos/utils/units.ts b/web/packages/gallery/utils/units.ts similarity index 100% rename from web/packages/new/photos/utils/units.ts rename to web/packages/gallery/utils/units.ts diff --git a/web/packages/media/collection.ts b/web/packages/media/collection.ts index e658ec1767..a813afaed3 100644 --- a/web/packages/media/collection.ts +++ b/web/packages/media/collection.ts @@ -27,6 +27,13 @@ export interface CollectionUser { } export interface EncryptedCollection { + /** + * The collection's globally unique ID. + * + * The collection's ID is a integer assigned by remote as the identifier for + * an {@link Collection} when it is created. It is globally unique across + * all collections on an Ente instance (i.e., it is not scoped to a user). + */ id: number; owner: CollectionUser; // collection name was unencrypted in the past, so we need to keep it as optional diff --git a/web/packages/media/file-metadata.ts b/web/packages/media/file-metadata.ts index 2d1c963876..44936d9af5 100644 --- a/web/packages/media/file-metadata.ts +++ b/web/packages/media/file-metadata.ts @@ -2,7 +2,11 @@ import { decryptMetadataJSON, encryptMetadataJSON } from "@/base/crypto"; import { authenticatedRequestHeaders, ensureOk } from "@/base/http"; import { apiURL } from "@/base/origins"; import { type Location } from "@/base/types"; -import { type EnteFile, type FilePublicMagicMetadata } from "@/media/file"; +import { + fileLogID, + type EnteFile, + type FilePublicMagicMetadata, +} from "@/media/file"; import { nullToUndefined } from "@/utils/transform"; import { z } from "zod"; import { mergeMetadata1 } from "./file"; @@ -290,6 +294,27 @@ const PublicMagicMetadata = z }) .passthrough(); +/** + * Return the public magic metadata for an {@link EnteFile}. + * + * We are not expected to be in a scenario where the file gets to the UI without + * having its public magic metadata decrypted, so this function is a sanity + * check and should be a no-op in usually. It'll throw if it finds its + * assumptions broken. Once the types have been refactored this entire + * check/cast shouldn't be needed, and this should become a trivial accessor. + */ +export const filePublicMagicMetadata = (file: EnteFile) => { + if (!file.pubMagicMetadata) return undefined; + if (typeof file.pubMagicMetadata.data == "string") { + throw new Error( + `Public magic metadata for ${fileLogID(file)} had not been decrypted even when the file reached the UI layer`, + ); + } + // This cast is unavoidable in the current setup. We need to refactor the + // types so that this cast in not needed. + return file.pubMagicMetadata.data as PublicMagicMetadata; +}; + /** * Return the hash of the file by reading it from its metadata. * @@ -830,3 +855,10 @@ export const fileLocation = (file: EnteFile): Location | undefined => { return { latitude, longitude }; }; + +/** + * Return the caption, aka "description", (if any) attached to the given + * {@link EnteFile}. + */ +export const fileCaption = (file: EnteFile): string | undefined => + filePublicMagicMetadata(file)?.caption; diff --git a/web/packages/media/file.ts b/web/packages/media/file.ts index 767c86f5ef..9499aa4523 100644 --- a/web/packages/media/file.ts +++ b/web/packages/media/file.ts @@ -2,6 +2,7 @@ import { sharedCryptoWorker } from "@/base/crypto"; import { dateFromEpochMicroseconds } from "@/base/date"; import log from "@/base/log"; import { type Metadata, ItemVisibility } from "./file-metadata"; +import { FileType } from "./file-type"; // TODO: Audit this file. @@ -66,11 +67,11 @@ export type EncryptedMagicMetadata = MagicMetadataCore; export interface EncryptedEnteFile { /** - * The file's ID. + * The file's globally unique ID. * - * The file's ID is a integer assigned by remote that is unique across all - * files stored by an Ente instance. That is, the file ID is a global unique - * identifier for this {@link EnteFile}. + * The file's ID is a integer assigned by remote as the identifier for an + * {@link EnteFile} when it is created. It is globally unique across all + * files stored by an Ente instance, and is not scoped to the current user. */ id: number; /** @@ -405,12 +406,19 @@ export const mergeMetadata1 = (file: EnteFile): EnteFile => { } } - // In a very rare cases (have found only one so far, a very old file in + // In very rare cases (have found only one so far, a very old file in // Vishnu's account, uploaded by an initial dev version of Ente) the photo // has no modification time. Gracefully handle such cases. if (!file.metadata.modificationTime) file.metadata.modificationTime = file.metadata.creationTime; + // In very rare cases (again, some files shared with Vishnu's account, + // uploaded by dev builds) the photo might not have a file type. Gracefully + // handle these too. The file ID threshold is an arbitrary cutoff so that + // this graceful handling does not mask new issues. + if (!file.metadata.fileType && file.id < 100000000) + file.metadata.fileType = FileType.image; + return file; }; diff --git a/web/packages/media/heic-convert.worker.ts b/web/packages/media/heic-convert.worker.ts index 52c9d7abba..0209267702 100644 --- a/web/packages/media/heic-convert.worker.ts +++ b/web/packages/media/heic-convert.worker.ts @@ -1,3 +1,4 @@ +import { logUnhandledErrorsAndRejectionsInWorker } from "@/base/log-web"; import { wait } from "@/utils/promise"; import { expose } from "comlink"; import HeicConvert from "heic-convert"; @@ -19,6 +20,8 @@ export class HEICConvertWorker { expose(HEICConvertWorker); +logUnhandledErrorsAndRejectionsInWorker(); + const heicToJPEG = async (heicBlob: Blob): Promise => { const buffer = new Uint8Array(await heicBlob.arrayBuffer()); const result = await HeicConvert({ buffer, format: "JPEG" }); diff --git a/web/packages/new/package.json b/web/packages/new/package.json index 86c25694a8..e3d807db95 100644 --- a/web/packages/new/package.json +++ b/web/packages/new/package.json @@ -7,11 +7,10 @@ "@/gallery": "*", "@/utils": "*", "@ente/shared": "*", - "@mui/material": "^6.4.3", - "@mui/system": "^6.4.3", - "@mui/x-date-pickers": "^7.25.0", + "@mui/material": "^6.4.6", + "@mui/system": "^6.4.6", + "@mui/x-date-pickers": "^7.27.1", "dayjs": "^1.11.13", - "formik": "^2.4.6", "react": "^19.0.0", "react-dom": "^19.0.0" }, diff --git a/web/packages/new/photos/components/CollectionMappingChoice.tsx b/web/packages/new/photos/components/CollectionMappingChoice.tsx index fdcda398c1..6917b60a25 100644 --- a/web/packages/new/photos/components/CollectionMappingChoice.tsx +++ b/web/packages/new/photos/components/CollectionMappingChoice.tsx @@ -1,4 +1,5 @@ import { SpacedRow } from "@/base/components/containers"; +import { DialogCloseIconButton } from "@/base/components/mui/DialogCloseIconButton"; import { FocusVisibleButton } from "@/base/components/mui/FocusVisibleButton"; import type { ModalVisibilityProps } from "@/base/components/utils/modal"; import type { CollectionMapping } from "@/base/types/ipc"; @@ -13,7 +14,6 @@ import { } from "@mui/material"; import { t } from "i18next"; import React from "react"; -import { DialogCloseIconButton } from "./mui/Dialog"; type CollectionMappingChoiceProps = ModalVisibilityProps & { /** diff --git a/web/packages/new/photos/components/CollectionSelector.tsx b/web/packages/new/photos/components/CollectionSelector.tsx index af8b0b13fd..e0b10c0a5c 100644 --- a/web/packages/new/photos/components/CollectionSelector.tsx +++ b/web/packages/new/photos/components/CollectionSelector.tsx @@ -1,4 +1,5 @@ import { SpacedRow } from "@/base/components/containers"; +import { DialogCloseIconButton } from "@/base/components/mui/DialogCloseIconButton"; import type { ModalVisibilityProps } from "@/base/components/utils/modal"; import type { Collection } from "@/media/collection"; import { @@ -24,7 +25,6 @@ import { } from "@mui/material"; import { t } from "i18next"; import React, { useEffect, useState } from "react"; -import { DialogCloseIconButton } from "./mui/Dialog"; export type CollectionSelectorAction = | "upload" diff --git a/web/packages/new/photos/components/DeleteAccount.tsx b/web/packages/new/photos/components/DeleteAccount.tsx new file mode 100644 index 0000000000..e8a4d2c180 --- /dev/null +++ b/web/packages/new/photos/components/DeleteAccount.tsx @@ -0,0 +1,283 @@ +import { TitledMiniDialog } from "@/base/components/MiniDialog"; +import { FocusVisibleButton } from "@/base/components/mui/FocusVisibleButton"; +import { LoadingButton } from "@/base/components/mui/LoadingButton"; +import type { ModalVisibilityProps } from "@/base/components/utils/modal"; +import { useBaseContext } from "@/base/context"; +import { + DropdownInput, + type DropdownOption, +} from "@/new/photos/components/DropdownInput"; +import { + deleteAccount, + getAccountDeleteChallenge, +} from "@/new/photos/services/user"; +import { initiateEmail } from "@/new/photos/utils/web"; +import { decryptDeleteAccountChallenge } from "@ente/shared/crypto/helpers"; +import { + Checkbox, + FormControlLabel, + FormGroup, + Link, + Stack, + TextField, + Typography, +} from "@mui/material"; +import { useFormik } from "formik"; +import { t } from "i18next"; +import React, { useState } from "react"; +import { Trans } from "react-i18next"; + +type DeleteAccountProps = ModalVisibilityProps & { + /** + * Called when the user should be authenticated again. + * + * Account deletion only proceeds if the promise returned by this function + * is fulfilled. + */ + onAuthenticateUser: () => Promise; +}; + +export const DeleteAccount: React.FC = ({ + open, + onClose, + onAuthenticateUser, +}) => { + const { logout, showMiniDialog, onGenericError } = useBaseContext(); + + const [acceptDataDeletion, setAcceptDataDeletion] = useState(false); + const [loading, setLoading] = useState(false); + + const { values, touched, errors, handleChange, handleSubmit } = useFormik({ + initialValues: { reason: "", feedback: "" }, + validate: ({ reason, feedback }) => { + if (!reason) return { reason: t("required") }; + if (!feedback.trim().length) { + return { + feedback: + reason == "found_another_service" + ? t("feedback_required_found_another_service") + : t("feedback_required"), + }; + } + return {}; + }, + onSubmit: async ({ reason, feedback }) => { + feedback = feedback.trim(); + setLoading(true); + try { + const { allowDelete, encryptedChallenge } = + await getAccountDeleteChallenge(); + if (allowDelete && encryptedChallenge) { + await onAuthenticateUser() + .then(confirmAccountDeletion) + .then(() => + solveChallengeAndDeleteAccount( + encryptedChallenge, + reason, + feedback, + ), + ); + } else { + askToMailForDeletion(); + } + } catch (e) { + onGenericError(e); + } + setLoading(false); + }, + }); + + const confirmAccountDeletion = () => + new Promise((resolve) => + showMiniDialog({ + title: t("delete_account"), + message: , + continue: { + text: t("delete"), + color: "critical", + action: resolve, + }, + }), + ); + + const askToMailForDeletion = () => { + const emailID = "account-deletion@ente.io"; + + showMiniDialog({ + title: t("delete_account"), + message: ( + }} + values={{ emailID }} + /> + ), + continue: { + text: t("delete"), + color: "critical", + action: () => initiateEmail(emailID), + }, + }); + }; + + const solveChallengeAndDeleteAccount = async ( + encryptedChallenge: string, + reason: string, + feedback: string, + ) => { + const decryptedChallenge = + await decryptDeleteAccountChallenge(encryptedChallenge); + await deleteAccount(decryptedChallenge, reason, feedback); + logout(); + }; + + return ( + +
+ + + + {t("delete_account_reason_label")} + + + {touched.reason && errors.reason && ( + + {errors.reason} + + )} + + + + + + {t("delete_account_confirm")} + + + {t("cancel")} + + + +
+
+ ); +}; + +/** + * All of these must have a corresponding localized string nested under the + * "delete_reason" key. + */ +const deleteReasons = [ + "missing_feature", + "behaviour", + "found_another_service", + "not_listed", +] as const; + +type DeleteReason = (typeof deleteReasons)[number]; + +const deleteReasonOptions = (): DropdownOption[] => + deleteReasons.map((reason) => ({ + label: t(`delete_reason.${reason}`), + value: reason, + })); + +interface FeedbackInputProps { + value: string; + errorMessage?: string | undefined; + onChange: (value: string) => void; +} + +const FeedbackInput: React.FC = ({ + value, + onChange, + errorMessage, +}) => ( + + {t("delete_account_feedback_label")} + onChange(e.target.value)} + placeholder={t("delete_account_feedback_placeholder")} + sx={{ + border: "1px solid", + borderColor: "stroke.faint", + borderRadius: "8px", + padding: "12px", + ".MuiInputBase-formControl": { + "::before, ::after": { + borderBottom: "none !important", + }, + }, + }} + /> + {errorMessage && ( + + {errorMessage} + + )} + +); + +interface ConfirmationCheckboxInputProps { + checked: boolean; + onChange: (value: boolean) => void; +} + +const ConfirmationCheckboxInput: React.FC = ({ + checked, + onChange, +}) => ( + + onChange(e.target.checked)} + /> + } + label={ + + {t("delete_account_confirm_checkbox_label")} + + } + /> + +); diff --git a/web/packages/new/photos/components/DropdownInput.tsx b/web/packages/new/photos/components/DropdownInput.tsx index af095dfcfb..5522028ace 100644 --- a/web/packages/new/photos/components/DropdownInput.tsx +++ b/web/packages/new/photos/components/DropdownInput.tsx @@ -73,17 +73,17 @@ export const DropdownInput = ({ // maxWidth to 0 forces element widths to equal minWidth. sx: { maxWidth: 0 }, }, - }, - MenuListProps: { - sx: { - backgroundColor: "background.paper2", - ".MuiMenuItem-root": { - color: "text.faint", - whiteSpace: "normal", - }, - // Make the selected item pop out by using color. - "&&& > .Mui-selected": { - color: "text.base", + list: { + sx: { + backgroundColor: "background.paper2", + ".MuiMenuItem-root": { + color: "text.faint", + whiteSpace: "normal", + }, + // Make the selected item pop out by using color. + "&&& > .Mui-selected": { + color: "text.base", + }, }, }, }, diff --git a/web/packages/new/photos/components/PhotoDateTimePicker.tsx b/web/packages/new/photos/components/FileDateTimePicker.tsx similarity index 94% rename from web/packages/new/photos/components/PhotoDateTimePicker.tsx rename to web/packages/new/photos/components/FileDateTimePicker.tsx index e035ba11c4..329bfd6061 100644 --- a/web/packages/new/photos/components/PhotoDateTimePicker.tsx +++ b/web/packages/new/photos/components/FileDateTimePicker.tsx @@ -8,26 +8,24 @@ import dayjs, { Dayjs } from "dayjs"; import React, { useState } from "react"; import { aboveFileViewerContentZ } from "./utils/z-index"; -interface PhotoDateTimePickerProps { +interface FileDateTimePickerProps { /** * The initial date to preselect in the date/time picker. * * If not provided, the current date/time is used. */ initialValue?: Date; - /** - * If true, then the picker shows provided date/time but doesn't allow - * editing it. - */ - disabled?: boolean; /** * Callback invoked when the user makes and confirms a date/time. */ onAccept: (date: ParsedMetadataDate) => void; /** * Optional callback invoked when the picker has been closed. + * + * > Note: This is only informational, for the caller to update their state. + * > The picker has already been closed at this point. */ - onClose?: () => void; + onDidClose?: () => void; } /** @@ -45,11 +43,10 @@ interface PhotoDateTimePickerProps { * UTC offset. For more discussion of the caveats and nuances around this, see * [Note: Photos are always in local date/time]. */ -export const PhotoDateTimePicker: React.FC = ({ +export const FileDateTimePicker: React.FC = ({ initialValue, - disabled, onAccept, - onClose, + onDidClose, }) => { const [open, setOpen] = useState(true); const [value, setValue] = useState(dayjs(initialValue)); @@ -61,8 +58,9 @@ export const PhotoDateTimePicker: React.FC = ({ }; const handleClose = () => { + console.log("handleClose"); setOpen(false); - onClose?.(); + onDidClose?.(); }; return ( @@ -73,7 +71,6 @@ export const PhotoDateTimePicker: React.FC = ({ open={open} onClose={handleClose} onOpen={() => setOpen(true)} - disabled={disabled} disableFuture={true} /* The dialog grows too big on the default portrait mode with our theme customizations. So we instead use the landscape diff --git a/web/packages/new/photos/components/FileViewerComponents-temp.ts b/web/packages/new/photos/components/FileViewerComponents-temp.ts new file mode 100644 index 0000000000..834bc63d6c --- /dev/null +++ b/web/packages/new/photos/components/FileViewerComponents-temp.ts @@ -0,0 +1,7 @@ +// TODO(PS): Temporary trampoline +export const resetFileViewerDataSourceOnClose = async () => { + if (!process.env.NEXT_PUBLIC_ENTE_WIP_PS5) return; + ( + await import("@/gallery/components/viewer/data-source") + ).resetFileViewerDataSourceOnClose(); +}; diff --git a/web/packages/new/photos/components/FileViewer.tsx b/web/packages/new/photos/components/FileViewerComponents.tsx similarity index 87% rename from web/packages/new/photos/components/FileViewer.tsx rename to web/packages/new/photos/components/FileViewerComponents.tsx index 6cc139ea64..c65c74e52f 100644 --- a/web/packages/new/photos/components/FileViewer.tsx +++ b/web/packages/new/photos/components/FileViewerComponents.tsx @@ -9,6 +9,22 @@ import { t } from "i18next"; import { useState } from "react"; import { aboveFileViewerContentZ } from "./utils/z-index"; +// TODO(PS) +import dynamic from "next/dynamic"; +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore +const FV5 = dynamic(() => import("@/gallery/components/viewer/FileViewer"), { + ssr: false, +}); + +const FVD = () => <>; + +export const FileViewer: React.FC = (props) => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + return process.env.NEXT_PUBLIC_ENTE_WIP_PS5 ? : ; +}; + type ConfirmDeleteFileDialogProps = ModalVisibilityProps & { /** * Called when the user confirms the deletion. diff --git a/web/apps/photos/src/components/PhotoViewer/ImageEditorOverlay.tsx b/web/packages/new/photos/components/ImageEditorOverlay.tsx similarity index 89% rename from web/apps/photos/src/components/PhotoViewer/ImageEditorOverlay.tsx rename to web/packages/new/photos/components/ImageEditorOverlay.tsx index 4ee4ab91f2..7bd37bb5c6 100644 --- a/web/apps/photos/src/components/PhotoViewer/ImageEditorOverlay.tsx +++ b/web/packages/new/photos/components/ImageEditorOverlay.tsx @@ -1,3 +1,10 @@ +/* TODO: Audit this file. + All the bangs shouldn't be needed with better types / restructuring. */ +/* eslint-disable @typescript-eslint/no-floating-promises */ +/* eslint-disable @typescript-eslint/no-unnecessary-condition */ +/* eslint-disable react-hooks/exhaustive-deps */ +/* eslint-disable @typescript-eslint/prefer-promise-reject-errors */ + import type { MiniDialogAttributes } from "@/base/components/MiniDialog"; import { SidebarDrawer } from "@/base/components/mui/SidebarDrawer"; import { @@ -7,14 +14,16 @@ import { RowButtonGroupTitle, RowSwitch, } from "@/base/components/RowButton"; +import type { ModalVisibilityProps } from "@/base/components/utils/modal"; +import { useBaseContext } from "@/base/context"; import { nameAndExtension } from "@/base/file-name"; import log from "@/base/log"; import { downloadAndRevokeObjectURL } from "@/base/utils/web"; import { downloadManager } from "@/gallery/services/download"; -import { EnteFile } from "@/media/file"; +import type { Collection } from "@/media/collection"; +import type { EnteFile } from "@/media/file"; import { aboveFileViewerContentZ } from "@/new/photos/components/utils/z-index"; import { getLocalCollections } from "@/new/photos/services/collections"; -import { AppContext } from "@/new/photos/types/context"; import { CenteredFlex } from "@ente/shared/components/Container"; import ChevronRightIcon from "@mui/icons-material/ChevronRight"; import CloseIcon from "@mui/icons-material/Close"; @@ -46,21 +55,33 @@ import { t } from "i18next"; import React, { forwardRef, Fragment, - useContext, useEffect, useRef, useState, type Ref, type RefObject, } from "react"; -import uploadManager from "services/upload/uploadManager"; -interface ImageEditorOverlayProps { +export type ImageEditorOverlayProps = ModalVisibilityProps & { + /** + * The (Ente) file to edit. + */ file: EnteFile; - show: boolean; - onClose: () => void; - closePhotoViewer: () => void; -} + /** + * Called when the user activates the button to save a copy of the given + * {@link enteFile} to their Ente account with the edits they have made. + * + * @param editedFile A Web {@link File} containing the edited contents. + * @param collection The collection to which the edited file should be + * added. + * @param enteFile The original {@link EnteFile}. + */ + onSaveEditedCopy: ( + editedFile: File, + collection: Collection, + enteFile: EnteFile, + ) => void; +}; const filterDefaultValues = { brightness: 100, @@ -79,20 +100,23 @@ interface CropBoxProps { height: number; } -export const ImageEditorOverlay: React.FC = ( - props, -) => { - const { showMiniDialog } = useContext(AppContext); +export const ImageEditorOverlay: React.FC = ({ + open, + onClose, + file, + onSaveEditedCopy, +}) => { + const { showMiniDialog } = useBaseContext(); const canvasRef = useRef(null); const originalSizeCanvasRef = useRef(null); const parentRef = useRef(null); - const [fileURL, setFileURL] = useState(""); + const [fileURL, setFileURL] = useState(undefined); // The MIME type of the original file that we are editing. // // It should generally be present, but it is not guaranteed to be. - const [mimeType, setMIMEType] = useState(); + const [mimeType, setMIMEType] = useState(undefined); const [currentRotationAngle, setCurrentRotationAngle] = useState(0); @@ -138,10 +162,10 @@ export const ImageEditorOverlay: React.FC = ( const getCanvasBoundsOffsets = () => { const canvasBounds = { - height: canvasRef.current.height, - width: canvasRef.current.width, + height: canvasRef.current!.height, + width: canvasRef.current!.width, }; - const parentBounds = parentRef.current.getBoundingClientRect(); + const parentBounds = parentRef.current!.getBoundingClientRect(); // calculate the offset created by centering the canvas in its parent const offsetX = (parentBounds.width - canvasBounds.width) / 2; @@ -155,10 +179,10 @@ export const ImageEditorOverlay: React.FC = ( }; }; - const handleDragStart = (e) => { + const handleDragStart: React.MouseEventHandler = (e) => { if (currentTab !== "crop") return; - const rect = cropBoxRef.current.getBoundingClientRect(); + const rect = cropBoxRef.current!.getBoundingClientRect(); const offsetX = e.pageX - rect.left - rect.width / 2; const offsetY = e.pageY - rect.top - rect.height / 2; @@ -189,7 +213,7 @@ export const ImageEditorOverlay: React.FC = ( setStartY(e.pageY - offsetY); }; - const handleDrag = (e) => { + const handleDrag: React.MouseEventHandler = (e) => { if (!isDragging && !isGrowing) return; // d- variables are the delta change between start and now @@ -241,7 +265,7 @@ export const ImageEditorOverlay: React.FC = ( } }; - const handleDragEnd = () => { + const handleDragEnd: React.MouseEventHandler = () => { setStartX(0); setStartY(0); @@ -268,7 +292,7 @@ export const ImageEditorOverlay: React.FC = ( return; } try { - applyFilters([canvasRef.current, originalSizeCanvasRef.current]); + applyFilters([canvasRef.current, originalSizeCanvasRef.current!]); setColoursAdjusted( brightness !== filterDefaultValues.brightness || contrast !== filterDefaultValues.contrast || @@ -292,37 +316,35 @@ export const ImageEditorOverlay: React.FC = ( for (const canvas of canvases) { const blurSizeRatio = Math.min(canvas.width, canvas.height) / - Math.min(canvasRef.current.width, canvasRef.current.height); + Math.min( + canvasRef.current!.width, + canvasRef.current!.height, + ); const blurRadius = blurSizeRatio * blur; const filterString = `brightness(${brightness}%) contrast(${contrast}%) blur(${blurRadius}px) saturate(${saturation}%) invert(${ invert ? 1 : 0 })`; - const context = canvas.getContext("2d"); - context.imageSmoothingEnabled = false; + const ctx = canvas.getContext("2d")!; + ctx.imageSmoothingEnabled = false; - context.filter = filterString; + ctx.filter = filterString; const image = new Image(); - image.src = fileURL; + image.src = fileURL!; await new Promise((resolve, reject) => { image.onload = () => { try { - context.clearRect( - 0, - 0, - canvas.width, - canvas.height, - ); - context.save(); - context.drawImage( + ctx.clearRect(0, 0, canvas.width, canvas.height); + ctx.save(); + ctx.drawImage( image, 0, 0, canvas.width, canvas.height, ); - context.restore(); + ctx.restore(); resolve(true); } catch (e) { reject(e); @@ -367,12 +389,11 @@ export const ImageEditorOverlay: React.FC = ( setCurrentRotationAngle(0); const img = new Image(); - const ctx = canvasRef.current.getContext("2d"); + const ctx = canvasRef.current.getContext("2d")!; ctx.imageSmoothingEnabled = false; if (!fileURL) { - const srcURLs = await downloadManager.renderableSourceURLs( - props.file, - ); + const srcURLs = + await downloadManager.renderableSourceURLs(file); img.src = srcURLs.url as string; setFileURL(srcURLs.url as string); // The image editing works for images (not live photos or @@ -387,23 +408,23 @@ export const ImageEditorOverlay: React.FC = ( img.onload = () => { try { const scale = Math.min( - parentRef.current.clientWidth / img.width, - parentRef.current.clientHeight / img.height, + parentRef.current!.clientWidth / img.width, + parentRef.current!.clientHeight / img.height, ); setPreviewCanvasScale(scale); const width = img.width * scale; const height = img.height * scale; - canvasRef.current.width = width; - canvasRef.current.height = height; + canvasRef.current!.width = width; + canvasRef.current!.height = height; ctx?.drawImage(img, 0, 0, width, height); - originalSizeCanvasRef.current.width = img.width; - originalSizeCanvasRef.current.height = img.height; + originalSizeCanvasRef.current!.width = img.width; + originalSizeCanvasRef.current!.height = img.height; const oSCtx = - originalSizeCanvasRef.current.getContext("2d"); + originalSizeCanvasRef.current!.getContext("2d"); oSCtx?.drawImage(img, 0, 0, img.width, img.height); @@ -433,13 +454,13 @@ export const ImageEditorOverlay: React.FC = ( }; useEffect(() => { - if (!props.show || !props.file) return; - loadCanvas(); - }, [props.show, props.file]); + if (!open || !file) return; + void loadCanvas(); + }, [open, file]); const handleClose = () => { - setFileURL(null); - props.onClose(); + setFileURL(undefined); + onClose(); }; const handleCloseWithConfirmation = () => { @@ -450,13 +471,13 @@ export const ImageEditorOverlay: React.FC = ( } }; - if (!props.show) { + if (!open) { return <>; } const getEditedFile = async () => { const originalSizeCanvas = originalSizeCanvasRef.current!; - const originalFileName = props.file.metadata.title; + const originalFileName = file.metadata.title; return canvasToFile(originalSizeCanvas, originalFileName, mimeType); }; @@ -471,19 +492,11 @@ export const ImageEditorOverlay: React.FC = ( if (!canvasRef.current) return; try { const collections = await getLocalCollections(); - const collection = collections.find( - (c) => c.id === props.file.collectionID, + (c) => c.id == file.collectionID, ); - - const editedFile = await getEditedFile(); - - uploadManager.prepareForNewUpload(); - uploadManager.showUploadProgressDialog(); - uploadManager.uploadFile(editedFile, collection, props.file); - setFileURL(null); - props.onClose(); - props.closePhotoViewer(); + onSaveEditedCopy(await getEditedFile(), collection!, file); + setFileURL(undefined); } catch (e) { log.error("Error saving copy to ente", e); } @@ -500,7 +513,7 @@ export const ImageEditorOverlay: React.FC = ( setTransformationPerformed(true); cropRegionOfCanvas(canvasRef.current, x1, y1, x2, y2); cropRegionOfCanvas( - originalSizeCanvasRef.current, + originalSizeCanvasRef.current!, x1 / previewCanvasScale, y1 / previewCanvasScale, x2 / previewCanvasScale, @@ -558,7 +571,7 @@ export const ImageEditorOverlay: React.FC = ( = ( position: "relative", }} > - {(fileURL === null || canvasLoading) && ( + {(!fileURL || canvasLoading) && ( )} @@ -597,7 +610,7 @@ export const ImageEditorOverlay: React.FC = ( style={{ objectFit: "contain", display: - fileURL === null || canvasLoading + !fileURL || canvasLoading ? "none" : "block", position: "absolute", @@ -660,6 +673,7 @@ export const ImageEditorOverlay: React.FC = ( { + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument setCurrentTab(value); }} > @@ -783,7 +797,10 @@ const canvasToFile = async ( break; } + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion const blob = (await new Promise((resolve) => + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore canvas.toBlob(resolve, mimeType), ))!; @@ -796,8 +813,8 @@ const canvasToFile = async ( }; interface CommonMenuProps { - canvasRef: RefObject; - originalSizeCanvasRef: RefObject; + canvasRef: RefObject; + originalSizeCanvasRef: RefObject; setTransformationPerformed: (v: boolean) => void; canvasLoading: boolean; setCanvasLoading: (v: boolean) => void; @@ -807,7 +824,7 @@ interface CommonMenuProps { type CropMenuProps = CommonMenuProps & { previewScale: number; cropBoxProps: CropBoxProps; - cropBoxRef: RefObject; + cropBoxRef: RefObject; resetCropBox: () => void; }; @@ -841,7 +858,7 @@ const CropMenu: React.FC = (props) => { setTransformationPerformed(true); cropRegionOfCanvas(canvasRef.current, x1, y1, x2, y2); cropRegionOfCanvas( - originalSizeCanvasRef.current, + originalSizeCanvasRef.current!, x1 / props.previewScale, y1 / props.previewScale, x2 / props.previewScale, @@ -864,7 +881,7 @@ const cropRegionOfCanvas = ( topLeftY: number, bottomRightX: number, bottomRightY: number, - scale: number = 1, + scale = 1, ) => { const context = canvas.getContext("2d"); if (!context || !canvas) return; @@ -938,7 +955,7 @@ const FreehandCropRegion = forwardRef( {/* Top overlay */} {/* Bottom overlay */} @@ -956,25 +973,25 @@ const FreehandCropRegion = forwardRef( {/* Left overlay */} {/* Right overlay */} @@ -982,10 +999,10 @@ const FreehandCropRegion = forwardRef( style={{ display: "grid", position: "absolute", - left: cropBox.x + "px", - top: cropBox.y + "px", - width: cropBox.width + "px", - height: cropBox.height + "px", + left: `${cropBox.x}px`, + top: `${cropBox.y}px`, + width: `${cropBox.width}px`, + height: `${cropBox.height}px`, border: "1px solid white", gridTemplateColumns: "1fr 1fr 1fr", gridTemplateRows: "1fr 1fr 1fr", @@ -1182,9 +1199,9 @@ const TransformMenu: React.FC = ({ (widthRatio: number, heightRatio: number) => () => { try { setCanvasLoading(true); - cropCanvas(canvasRef.current, widthRatio, heightRatio); + cropCanvas(canvasRef.current!, widthRatio, heightRatio); cropCanvas( - originalSizeCanvasRef.current, + originalSizeCanvasRef.current!, widthRatio, heightRatio, ); @@ -1203,9 +1220,9 @@ const TransformMenu: React.FC = ({ const createRotationHandler = (rotation: "left" | "right") => () => { try { setCanvasLoading(true); - rotateCanvas(canvasRef.current, rotation === "left" ? -90 : 90); + rotateCanvas(canvasRef.current!, rotation === "left" ? -90 : 90); rotateCanvas( - originalSizeCanvasRef.current, + originalSizeCanvasRef.current!, rotation === "left" ? -90 : 90, ); setCanvasLoading(false); @@ -1219,8 +1236,8 @@ const TransformMenu: React.FC = ({ (direction: "vertical" | "horizontal") => () => { try { setCanvasLoading(true); - flipCanvas(canvasRef.current, direction); - flipCanvas(originalSizeCanvasRef.current, direction); + flipCanvas(canvasRef.current!, direction); + flipCanvas(originalSizeCanvasRef.current!, direction); setCanvasLoading(false); setTransformationPerformed(true); } catch (e) { diff --git a/web/packages/new/photos/components/PlanSelector.tsx b/web/packages/new/photos/components/PlanSelector.tsx index de5224e883..7fc646286d 100644 --- a/web/packages/new/photos/components/PlanSelector.tsx +++ b/web/packages/new/photos/components/PlanSelector.tsx @@ -6,7 +6,9 @@ import { genericRetriableErrorDialogAttributes, } from "@/base/components/utils/dialog"; import type { ModalVisibilityProps } from "@/base/components/utils/modal"; +import { useBaseContext } from "@/base/context"; import log from "@/base/log"; +import { bytesInGB, formattedStorageByteSize } from "@/gallery/utils/units"; import { useUserDetailsSnapshot } from "@/new/photos/components/utils/use-snapshot"; import { useWrapAsyncOperation } from "@/new/photos/components/utils/use-wrap-async"; import type { @@ -32,8 +34,6 @@ import { redirectToPaymentsApp, userDetailsAddOnBonuses, } from "@/new/photos/services/user-details"; -import { useAppContext } from "@/new/photos/types/context"; -import { bytesInGB, formattedStorageByteSize } from "@/new/photos/utils/units"; import { openURL } from "@/new/photos/utils/web"; import ArrowForwardIcon from "@mui/icons-material/ArrowForward"; import ChevronRightIcon from "@mui/icons-material/ChevronRight"; @@ -111,7 +111,7 @@ const PlanSelectorCard: React.FC = ({ onClose, setLoading, }) => { - const { showMiniDialog } = useAppContext(); + const { showMiniDialog } = useBaseContext(); const userDetails = useUserDetailsSnapshot(); @@ -681,7 +681,7 @@ function ManageSubscription({ subscription, hasAddOnBonus, }: ManageSubscriptionProps) { - const { onGenericError } = useAppContext(); + const { onGenericError } = useBaseContext(); const openFamilyPortal = async () => { setLoading(true); @@ -717,7 +717,7 @@ const StripeSubscriptionOptions: React.FC = ({ subscription, hasAddOnBonus, }) => { - const { showMiniDialog } = useAppContext(); + const { showMiniDialog } = useBaseContext(); const confirmReactivation = () => showMiniDialog({ diff --git a/web/packages/new/photos/components/Tiles.tsx b/web/packages/new/photos/components/Tiles.tsx index 5a6d88dff0..06d0ed96bc 100644 --- a/web/packages/new/photos/components/Tiles.tsx +++ b/web/packages/new/photos/components/Tiles.tsx @@ -201,9 +201,11 @@ export const LargeTileTextOverlay = styled(Overlay)` padding: 8px; color: white; background: linear-gradient( - 0deg, + -10deg, rgba(0, 0, 0, 0.1) 0%, - rgba(0, 0, 0, 0.5) 86% + rgba(0, 0, 0, 0.2) 50%, + rgba(0, 0, 0, 0.4) 60%, + rgba(0, 0, 0, 0.6) 100% ); `; diff --git a/web/packages/new/photos/components/gallery/PeopleHeader.tsx b/web/packages/new/photos/components/gallery/PeopleHeader.tsx index 40e24cd74f..606f09d96c 100644 --- a/web/packages/new/photos/components/gallery/PeopleHeader.tsx +++ b/web/packages/new/photos/components/gallery/PeopleHeader.tsx @@ -1,6 +1,7 @@ import { CenteredFill, SpacedRow } from "@/base/components/containers"; import { ActivityErrorIndicator } from "@/base/components/ErrorIndicator"; import { ActivityIndicator } from "@/base/components/mui/ActivityIndicator"; +import { DialogCloseIconButton } from "@/base/components/mui/DialogCloseIconButton"; import { FocusVisibleButton } from "@/base/components/mui/FocusVisibleButton"; import { LoadingButton } from "@/base/components/mui/LoadingButton"; import { @@ -13,6 +14,7 @@ import { useModalVisibility, type ModalVisibilityProps, } from "@/base/components/utils/modal"; +import { useBaseContext } from "@/base/context"; import log from "@/base/log"; import { addCGroup, @@ -58,8 +60,6 @@ import { import { t } from "i18next"; import React, { useEffect, useReducer, useState } from "react"; import type { FaceCluster } from "../../services/ml/cluster"; -import { useAppContext } from "../../types/context"; -import { DialogCloseIconButton } from "../mui/Dialog"; import { SuggestionFaceList } from "../PeopleList"; import { ItemCard, @@ -110,7 +110,7 @@ interface CGroupPersonHeaderProps { const CGroupPersonHeader: React.FC = ({ person }) => { const cgroup = person.cgroup; - const { showMiniDialog } = useAppContext(); + const { showMiniDialog } = useBaseContext(); const { show: showNameInput, props: nameInputVisibilityProps } = useModalVisibility(); @@ -225,7 +225,7 @@ const ClusterPersonHeader: React.FC = ({ }) => { const cluster = person.cluster; - const { showMiniDialog } = useAppContext(); + const { showMiniDialog } = useBaseContext(); const { show: showAddPerson, props: addPersonVisibilityProps } = useModalVisibility(); @@ -545,7 +545,7 @@ const SuggestionsDialog: React.FC = ({ onClose, person, }) => { - const { showMiniDialog, onGenericError } = useAppContext(); + const { showMiniDialog, onGenericError } = useBaseContext(); const [state, dispatch] = useReducer( suggestionsDialogReducer, diff --git a/web/packages/new/photos/components/gallery/reducer.ts b/web/packages/new/photos/components/gallery/reducer.ts index 72c6d31b4f..adc044d672 100644 --- a/web/packages/new/photos/components/gallery/reducer.ts +++ b/web/packages/new/photos/components/gallery/reducer.ts @@ -1,3 +1,8 @@ +import { + isArchivedCollection, + isArchivedFile, + isPinnedCollection, +} from "@/gallery/services/magic-metadata"; import { COLLECTION_ROLE, CollectionType, @@ -34,11 +39,6 @@ import { sortFiles, uniqueFilesByID, } from "../../services/files"; -import { - isArchivedCollection, - isArchivedFile, - isPinnedCollection, -} from "../../services/magic-metadata"; import type { PeopleState, Person } from "../../services/ml/people"; import type { SearchSuggestion } from "../../services/search/types"; import type { FamilyData } from "../../services/user-details"; @@ -195,14 +195,13 @@ export interface GalleryState { */ favoriteFileIDs: Set; /** - * User visible collection names indexed by collection IDs for fast lookup. + * A map from collection IDs to their user visible name. * - * This map will contain entries for all (both normal and hidden) - * collections. + * It will contain entries for all collections (both normal and hidden). */ - allCollectionNameByID: Map; + allCollectionsNameByID: Map; /** - * A list of collection IDs to which a file belongs, indexed by file ID. + * A map from file IDs to the IDs of the collections that they're a part of. */ fileCollectionIDs: Map; @@ -420,7 +419,7 @@ const initialGalleryState: GalleryState = { hiddenFileIDs: new Set(), archivedFileIDs: new Set(), favoriteFileIDs: new Set(), - allCollectionNameByID: new Map(), + allCollectionsNameByID: new Map(), fileCollectionIDs: new Map(), collectionSummaries: new Map(), hiddenCollectionSummaries: new Map(), @@ -481,7 +480,7 @@ const galleryReducer: React.Reducer = ( action.files, state.unsyncedFavoriteUpdates, ), - allCollectionNameByID: createCollectionNameByID( + allCollectionsNameByID: createCollectionNameByID( action.allCollections, ), fileCollectionIDs: createFileCollectionIDs(action.files), @@ -539,7 +538,7 @@ const galleryReducer: React.Reducer = ( state.files, state.unsyncedFavoriteUpdates, ), - allCollectionNameByID: createCollectionNameByID( + allCollectionsNameByID: createCollectionNameByID( collections.concat(state.hiddenCollections), ), collectionSummaries, @@ -608,7 +607,7 @@ const galleryReducer: React.Reducer = ( state.files, state.unsyncedFavoriteUpdates, ), - allCollectionNameByID: createCollectionNameByID( + allCollectionsNameByID: createCollectionNameByID( collections.concat(hiddenCollections), ), collectionSummaries, diff --git a/web/packages/new/photos/components/mui/ChipButton.tsx b/web/packages/new/photos/components/mui/ChipButton.tsx deleted file mode 100644 index 2bf7ec7e6b..0000000000 --- a/web/packages/new/photos/components/mui/ChipButton.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import { Button, type ButtonProps, styled } from "@mui/material"; - -export const ChipButton = styled((props: ButtonProps) => ( -