diff --git a/.github/workflows/web-deploy-staff.yml b/.github/workflows/infra-deploy-staff.yml similarity index 63% rename from .github/workflows/web-deploy-staff.yml rename to .github/workflows/infra-deploy-staff.yml index 854e163644..dd68a14a26 100644 --- a/.github/workflows/web-deploy-staff.yml +++ b/.github/workflows/infra-deploy-staff.yml @@ -1,45 +1,43 @@ name: "Deploy (staff)" on: - # Run on every push to main that changes web/apps/staff/ + # Run on every push to main that changes infra/staff/ push: branches: [main] paths: - - "web/apps/staff/**" - - ".github/workflows/web-deploy-staff.yml" + - "infra/staff/**" + - ".github/workflows/infra-deploy-staff.yml" # Also allow manually running the workflow workflow_dispatch: jobs: - deploy: + lint: runs-on: ubuntu-latest defaults: run: - working-directory: web + working-directory: infra/staff steps: - name: Checkout code uses: actions/checkout@v4 - with: - submodules: recursive - name: Setup node and enable yarn caching uses: actions/setup-node@v4 with: node-version: 20 cache: "yarn" - cache-dependency-path: "web/yarn.lock" + cache-dependency-path: "infra/staff/yarn.lock" - name: Install dependencies run: yarn install - - name: Build staff - run: yarn build:staff + - name: Build + run: yarn build - - name: Publish staff + - name: Publish uses: cloudflare/wrangler-action@v3 with: accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} - command: pages deploy --project-name=ente --commit-dirty=true --branch=deploy/staff web/apps/staff/dist + command: pages deploy --project-name=ente --commit-dirty=true --branch=deploy/staff infra/staff/dist diff --git a/.github/workflows/infra-lint-staff.yml b/.github/workflows/infra-lint-staff.yml new file mode 100644 index 0000000000..5c2894281e --- /dev/null +++ b/.github/workflows/infra-lint-staff.yml @@ -0,0 +1,34 @@ +name: "Lint (staff)" + +on: + # Run on every push to a branch other than main that changes infra/staff/ + push: + branches-ignore: [main] + paths: + - "infra/staff/**" + - ".github/workflows/infra-deploy-staff.yml" + +jobs: + deploy: + runs-on: ubuntu-latest + + defaults: + run: + working-directory: infra/staff + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup node and enable yarn caching + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: "yarn" + cache-dependency-path: "infra/staff/yarn.lock" + + - name: Install dependencies + run: yarn install + + - name: Lint + run: yarn lint diff --git a/.github/workflows/mobile-lint.yml b/.github/workflows/mobile-lint.yml index 3a43924a35..8f231079f3 100644 --- a/.github/workflows/mobile-lint.yml +++ b/.github/workflows/mobile-lint.yml @@ -9,6 +9,7 @@ on: - ".github/workflows/mobile-lint.yml" env: + FLUTTER_VERSION: "3.22.0" jobs: diff --git a/.github/workflows/mobile-release.yml b/.github/workflows/mobile-release.yml index 6211f2c262..35cc217e67 100644 --- a/.github/workflows/mobile-release.yml +++ b/.github/workflows/mobile-release.yml @@ -9,7 +9,7 @@ on: - "photos-v*" env: - FLUTTER_VERSION: "3.19.3" + FLUTTER_VERSION: "3.22.0" jobs: build: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 857e18fb54..e92fe9c6cf 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -12,9 +12,10 @@ There are many ways to contribute, and most of them don't require writing code. ## Spread the word -This is perhaps the most impactful contribution you can make. Spread the word. -Online on your favorite social media channels. Offline to your friends and -family who are looking for a privacy-friendly alternative to big tech. +This is perhaps the most impactful contribution you can make. [Spread the +word](https://help.ente.io/photos/features/referral-program/). Online on your +favorite social media channels. Offline to your friends and family who are +looking for a privacy-friendly alternative to big tech. ## Engage with the community @@ -76,7 +77,10 @@ us](https://github.com/ente-io/ente/discussions). Discussing your idea with us first ensures that everyone is on the same page before you start working on your change. -## Star +## Leave a review or star If you haven't already done so, consider [starring this -repository](https://github.com/ente-io/ente/stargazers). +repository](https://github.com/ente-io/ente/stargazers) or leaving a review on +[PlayStore](https://play.google.com/store/apps/details?id=io.ente.auth), +[AppStore](https://apps.apple.com/us/app/ente-authenticator/id6444121398) or +[AlternativeTo](https://alternativeto.net/software/ente-authenticator/). diff --git a/README.md b/README.md index 88488d11ca..00a786b96e 100644 --- a/README.md +++ b/README.md @@ -60,8 +60,8 @@ Our labour of love. Two years ago, while building Ente Photos, we realized that there was no open source end-to-end encrypted authenticator app. We already had the building blocks, so we built one. -Ente Auth is currently free. If in the future we convert this to a paid service, -existing users will be grandfathered in. +Ente Auth is free, and will remain free forever. If you like the service and +want to give back, please check out Ente Photos or spread the word.
diff --git a/auth/README.md b/auth/README.md index e2e03f0230..6382812de2 100644 --- a/auth/README.md +++ b/auth/README.md @@ -95,13 +95,10 @@ more, see [docs/adding-icons](docs/adding-icons.md). ## 💚 Contribute -For more ways to contribute, see [../CONTRIBUTING.md](../CONTRIBUTING.md). +The best way to support this project is by checking out [Ente +Photos](../mobile/README.md) or spreading the word. -You can also support us by giving this project a ⭐ star on GitHub or by leaving -a review on -[PlayStore](https://play.google.com/store/apps/details?id=io.ente.auth), -[AppStore](https://apps.apple.com/us/app/ente-authenticator/id6444121398) or -[AlternativeTo](https://alternativeto.net/software/ente-authenticator/). +For more ways to contribute, see [../CONTRIBUTING.md](../CONTRIBUTING.md). ## ⭐ About diff --git a/auth/assets/custom-icons/_data/custom-icons.json b/auth/assets/custom-icons/_data/custom-icons.json index 3e8f8ce679..f532eb00fd 100644 --- a/auth/assets/custom-icons/_data/custom-icons.json +++ b/auth/assets/custom-icons/_data/custom-icons.json @@ -18,10 +18,10 @@ "title": "AscendEX" }, { - "title": "BitMEX" + "title": "Bitfinex" }, { - "title": "Bitfinex" + "title": "BitMEX" }, { "title": "Bitvavo", @@ -32,13 +32,12 @@ }, { "title": "Bloom Host", - "slug": "bloom_host" + "slug": "bloom_host", + "altNames": ["Bloom Host Billing"] }, { "title": "BorgBase", - "altNames": [ - "borg" - ], + "altNames": ["borg"], "slug": "BorgBase" }, { @@ -57,10 +56,10 @@ "hex": "D14633" }, { - "title": "ConfigCat" + "title": "Cloudflare" }, { - "title": "Cloudflare" + "title": "ConfigCat" }, { "title": "Control D", @@ -72,9 +71,7 @@ }, { "title": "DCS", - "altNames": [ - "Digital Combat Simulator" - ], + "altNames": ["Digital Combat Simulator"], "slug": "dcs" }, { @@ -83,6 +80,9 @@ { "title": "Discourse" }, + { + "title": "Doppler" + }, { "title": "dus.net", "slug": "dusnet" @@ -117,14 +117,15 @@ { "title": "GitLab" }, + { + "title": "GMX" + }, { "title": "Google" }, { "title": "Gosuslugi", - "altNames": [ - "Đ“ĐŸŃŃƒŃĐ»ŃƒĐłĐž" - ], + "altNames": ["Đ“ĐŸŃŃƒŃĐ»ŃƒĐłĐž"], "slug": "Gosuslugi" }, { @@ -135,21 +136,21 @@ "slug": "healthchecks" }, { - "title": "ING" + "title": "IceDrive", + "slug": "Icedrive" }, { - "title": "INWX" + "title": "ING" }, { "title": "Instagram" }, { - "title": "IVPN", - "slug": "IVPN" + "title": "INWX" }, { - "title": "IceDrive", - "slug": "Icedrive" + "title": "IVPN", + "slug": "IVPN" }, { "title": "Jagex", @@ -158,10 +159,6 @@ { "title": "Kagi" }, - { - "title": "KPN", - "color": "00CC00" - }, { "title": "Kick", "hex": "53FC19" @@ -172,6 +169,10 @@ { "title": "Koofr" }, + { + "title": "KPN", + "color": "00CC00" + }, { "title": "Kraken", "hex": "5848D5" @@ -190,27 +191,21 @@ { "title": "Letterboxd" }, + { + "title": "Local", + "slug": "local_wp", + "altNames": ["LocalWP", "Local WP", "Local Wordpress"] + }, { "title": "Mastodon", - "altNames": [ - "mstdn", - "fediscience", - "mathstodon", - "fosstodon" - ], + "altNames": ["mstdn", "fediscience", "mathstodon", "fosstodon"], "slug": "mastodon", "hex": "6364FF" }, { "title": "Mercado Livre", - "slug": "mercado_livre" - }, - { - "title": "Murena", - "altNames": [ - "eCloud" - ], - "slug": "ecloud" + "slug": "mercado_livre", + "altNames": ["Mercado Libre", "MercadoLibre", "MercadoLivre"] }, { "title": "Microsoft" @@ -221,6 +216,11 @@ { "title": "Mozilla" }, + { + "title": "Murena", + "altNames": ["eCloud"], + "slug": "ecloud" + }, { "title": "MyFRITZ!Net", "slug": "myfritz" @@ -294,6 +294,10 @@ { "title": "Proxmox" }, + { + "title": "Real-Debrid", + "slug": "real_debrid" + }, { "title": "Revolt", "hex": "858585" @@ -302,6 +306,10 @@ "title": "Rockstar Games", "slug": "rockstar_games" }, + { + "title": "RuneMate", + "hex": "2ECC71" + }, { "title": "Rust Language Forum", "slug": "rust_language_forum", @@ -331,6 +339,9 @@ "slug": "standardnotes", "hex": "2173E6" }, + { + "title": "Surfshark" + }, { "title": "Synology DSM", "slug": "synology_dsm" @@ -341,7 +352,8 @@ "hex": "FFFFFF" }, { - "title": "Techlore" + "title": "Techlore", + "altNames": ["Techlore Courses", "Techlore Forums"] }, { "title": "Termius", @@ -399,19 +411,27 @@ "slug": "wyze" }, { - "title": "X", + "title": "WorkOS", + "slug": "workos", "altNames": [ - "twitter" - ], + "Work OS" + ] + }, + { + "title": "X", + "altNames": ["twitter"], "slug": "x" }, { "title": "Yandex", - "altNames": [ - "Ya", - "ĐŻĐœĐŽĐ”Đșс" - ], + "altNames": ["Ya", "ĐŻĐœĐŽĐ”Đșс"], "slug": "Yandex" + }, + { + "title": "YNAB", + "altNames": ["You Need A Budget"], + "slug": "ynab", + "hex": "3B5EDA" } ] -} \ No newline at end of file +} diff --git a/auth/assets/custom-icons/icons/configcat.svg b/auth/assets/custom-icons/icons/configcat.svg index cfecd22b02..12f7e4e81a 100644 --- a/auth/assets/custom-icons/icons/configcat.svg +++ b/auth/assets/custom-icons/icons/configcat.svg @@ -1,7 +1,7 @@ - - + + - + diff --git a/auth/assets/custom-icons/icons/doppler.svg b/auth/assets/custom-icons/icons/doppler.svg new file mode 100644 index 0000000000..a11a7866b2 --- /dev/null +++ b/auth/assets/custom-icons/icons/doppler.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/auth/assets/custom-icons/icons/gmx.svg b/auth/assets/custom-icons/icons/gmx.svg new file mode 100644 index 0000000000..293cbdaf06 --- /dev/null +++ b/auth/assets/custom-icons/icons/gmx.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/auth/assets/custom-icons/icons/habbo.svg b/auth/assets/custom-icons/icons/habbo.svg index 746bcdb229..407399507e 100644 --- a/auth/assets/custom-icons/icons/habbo.svg +++ b/auth/assets/custom-icons/icons/habbo.svg @@ -1,9 +1,23 @@ - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + diff --git a/auth/assets/custom-icons/icons/local_wp.svg b/auth/assets/custom-icons/icons/local_wp.svg new file mode 100644 index 0000000000..f37d988ec4 --- /dev/null +++ b/auth/assets/custom-icons/icons/local_wp.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/auth/assets/custom-icons/icons/mercado_livre.svg b/auth/assets/custom-icons/icons/mercado_livre.svg index 7f4db5fd53..c4401f6945 100644 --- a/auth/assets/custom-icons/icons/mercado_livre.svg +++ b/auth/assets/custom-icons/icons/mercado_livre.svg @@ -1,9 +1,11 @@ - - - - - - - - + + + + + + + + + + diff --git a/auth/assets/custom-icons/icons/real_debrid.svg b/auth/assets/custom-icons/icons/real_debrid.svg new file mode 100644 index 0000000000..d6b616c9e3 --- /dev/null +++ b/auth/assets/custom-icons/icons/real_debrid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/auth/assets/custom-icons/icons/runemate.svg b/auth/assets/custom-icons/icons/runemate.svg new file mode 100644 index 0000000000..1855afb8d2 --- /dev/null +++ b/auth/assets/custom-icons/icons/runemate.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/auth/assets/custom-icons/icons/sendgrid.svg b/auth/assets/custom-icons/icons/sendgrid.svg index 1562adab90..554bb37aae 100644 --- a/auth/assets/custom-icons/icons/sendgrid.svg +++ b/auth/assets/custom-icons/icons/sendgrid.svg @@ -1,9 +1,6 @@ - - - - - - - - + + + + + diff --git a/auth/assets/custom-icons/icons/surfshark.svg b/auth/assets/custom-icons/icons/surfshark.svg new file mode 100644 index 0000000000..745b066ce9 --- /dev/null +++ b/auth/assets/custom-icons/icons/surfshark.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/auth/assets/custom-icons/icons/workos.svg b/auth/assets/custom-icons/icons/workos.svg new file mode 100644 index 0000000000..d01eaad932 --- /dev/null +++ b/auth/assets/custom-icons/icons/workos.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/auth/assets/custom-icons/icons/ynab.svg b/auth/assets/custom-icons/icons/ynab.svg new file mode 100644 index 0000000000..4ddfc9fa07 --- /dev/null +++ b/auth/assets/custom-icons/icons/ynab.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/auth/lib/l10n/arb/app_de.arb b/auth/lib/l10n/arb/app_de.arb index 0c4d29eaf3..3f027c8959 100644 --- a/auth/lib/l10n/arb/app_de.arb +++ b/auth/lib/l10n/arb/app_de.arb @@ -20,6 +20,8 @@ "codeIssuerHint": "Aussteller", "codeSecretKeyHint": "Geheimer SchlĂŒssel", "codeAccountHint": "Konto (you@domain.com)", + "codeTagHint": "Tag", + "accountKeyType": "Art des Keys", "sessionExpired": "Sitzung abgelaufen", "@sessionExpired": { "description": "Title of the dialog when the users current session is invalid/expired" @@ -156,6 +158,7 @@ } } }, + "invalidQRCode": "UngĂŒltiger QR-Code", "noRecoveryKeyTitle": "Kein WiederherstellungsschlĂŒssel?", "enterEmailHint": "Geben Sie Ihre E-Mail Adresse ein", "invalidEmailTitle": "UngĂŒltige E-Mail Adresse", @@ -420,5 +423,18 @@ "invalidEndpoint": "UngĂŒltiger Endpunkt", "invalidEndpointMessage": "Der eingegebene Endpunkt ist ungĂŒltig. Bitte geben Sie einen gĂŒltigen Endpunkt ein und versuchen Sie es erneut.", "endpointUpdatedMessage": "Endpunkt erfolgreich aktualisiert", - "customEndpoint": "Mit {endpoint} verbunden" + "customEndpoint": "Mit {endpoint} verbunden", + "pinText": "Anpinnen", + "unpinText": "Lösen", + "pinnedCodeMessage": "{code} wurde angepinnt", + "unpinnedCodeMessage": "{code} wurde Losgelöst", + "tags": "Tags", + "createNewTag": "Neuen Tag erstellen", + "tag": "Tag", + "create": "Erstellen", + "editTag": "Tag bearbeiten", + "deleteTagTitle": "Tag löschen?", + "deleteTagMessage": "Sind Sie sicher, dass Sie diesen Code löschen wollen? Diese Aktion ist unumkehrbar.", + "somethingWentWrongParsingCode": "Wir konnten {x} Codes nicht parsen.", + "updateNotAvailable": "Update ist nicht verfĂŒgbar" } \ 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 71ddc0b31c..d789fc48c2 100644 --- a/auth/lib/l10n/arb/app_fr.arb +++ b/auth/lib/l10n/arb/app_fr.arb @@ -20,6 +20,8 @@ "codeIssuerHint": "Émetteur", "codeSecretKeyHint": "ClĂ© secrĂšte", "codeAccountHint": "Compte (vous@exemple.com)", + "codeTagHint": "Tag", + "accountKeyType": "Type de clĂ©", "sessionExpired": "Session expirĂ©e", "@sessionExpired": { "description": "Title of the dialog when the users current session is invalid/expired" @@ -77,12 +79,14 @@ "data": "DonnĂ©es", "importCodes": "Importer les codes", "importTypePlainText": "Texte brut", + "importTypeEnteEncrypted": "Export chiffrĂ© Ente", "passwordForDecryptingExport": "Mot de passe pour dĂ©chiffrer l'exportation", "passwordEmptyError": "Le mot de passe ne peut pas ĂȘtre vide", "importFromApp": "Importer des codes depuis {appName}", "importGoogleAuthGuide": "Exportez vos comptes depuis Google Authenticator vers un code QR en utilisant l'option \"TransfĂ©rer des comptes\". Ensuite, en utilisant un autre appareil, scannez le code QR.\n\nAstuce : Vous pouvez utiliser la webcam de votre ordinateur portable pour prendre une photo du code QR.", "importSelectJsonFile": "SĂ©lectionnez un fichier JSON", "importSelectAppExport": "SĂ©lectionnez le fichier d'exportation {appName}", + "importEnteEncGuide": "SĂ©lectionnez le fichier chiffrĂ© JSON exportĂ© depuis Ente", "importRaivoGuide": "Utilisez l'option \"Exporter les OTPs vers l'archive Zip\" dans les paramĂštres de Raivo.\n\nExtrayez le fichier zip et importez le fichier JSON.", "importBitwardenGuide": "Utilisez l'option « Exporter le coffre » dans les outils Bitwarden et importez le fichier JSON non chiffrĂ©.", "importAegisGuide": "Utilisez l'option \"Exporter le coffre-fort\" dans les paramĂštres d'Aegis.\n\nSi votre coffre-fort est cryptĂ©, vous devrez saisir le mot de passe du coffre-fort pour dĂ©chiffrer le coffre-fort.", @@ -112,18 +116,22 @@ "copied": "CopiĂ©", "pleaseTryAgain": "Veuillez rĂ©essayer", "existingUser": "Utilisateur existant", + "newUser": "Nouveau dans Ente", "delete": "Supprimer", "enterYourPasswordHint": "Saisir votre mot de passe", "forgotPassword": "Mot de passe oubliĂ©", "oops": "Oups", "suggestFeatures": "SuggĂ©rer des fonctionnalitĂ©s", "faq": "FAQ", + "faq_q_1": "Quelle est la sĂ©curitĂ© de Auth?", + "faq_a_1": "Tous les codes que vous sauvegardez via ente sont chiffrĂ©s de bout en bout. Cela signifie que vous seul pouvez accĂ©der Ă  vos codes. Nos applications sont open source et notre cryptographie ont fait l'objet d'un audit externe.", "faq_q_2": "Puis-je accĂ©der Ă  mes codes sur mon ordinateur ?", "faq_a_2": "Vous pouvez accĂ©der Ă  vos codes sur le web via auth.ente.io.", "faq_q_3": "Comment puis-je supprimer des codes ?", "faq_a_3": "Vous pouvez supprimer un code en glissant vers la gauche.", "faq_q_4": "Comment puis-je soutenir le projet ?", "faq_a_4": "Vous pouvez soutenir le dĂ©veloppement de ce projet en vous abonnant Ă  notre application Photos, ente.io.", + "faq_q_5": "Comment puis-je activer le verrouillage FaceID dans Auth", "faq_a_5": "Vous pouvez activer le verrouillage FaceID dans ParamĂštres → SĂ©curitĂ© → Écran de verrouillage.", "somethingWentWrongMessage": "Quelque chose s'est mal passĂ©, veuillez recommencer", "leaveFamily": "Quitter le plan familial", @@ -150,6 +158,7 @@ } } }, + "invalidQRCode": "QR code non valide", "noRecoveryKeyTitle": "Pas de clĂ© de rĂ©cupĂ©ration ?", "enterEmailHint": "Entrez votre adresse e-mail", "invalidEmailTitle": "Adresse e-mail invalide", @@ -343,6 +352,7 @@ "deleteCodeAuthMessage": "Authentification requise pour supprimer le code", "showQRAuthMessage": "Authentification requise pour afficher le code QR", "confirmAccountDeleteTitle": "Confirmer la suppression du compte", + "confirmAccountDeleteMessage": "Ce compte est liĂ© Ă  d'autres applications ente, si vous en utilisez une.\n\nVos donnĂ©es tĂ©lĂ©chargĂ©es, dans toutes les applications ente, seront planifiĂ©es pour suppression, et votre compte sera dĂ©finitivement supprimĂ©.", "androidBiometricHint": "VĂ©rifier l’identitĂ©", "@androidBiometricHint": { "description": "Hint message advising the user how to authenticate with biometrics. It is used on Android side. Maximum 60 characters." @@ -413,5 +423,18 @@ "invalidEndpoint": "Point de terminaison non valide", "invalidEndpointMessage": "DĂ©solĂ©, le point de terminaison que vous avez entrĂ© n'est pas valide. Veuillez en entrer un valide puis rĂ©essayez.", "endpointUpdatedMessage": "Point de terminaison mis Ă  jour avec succĂšs", - "customEndpoint": "ConnectĂ© Ă  {endpoint}" + "customEndpoint": "ConnectĂ© Ă  {endpoint}", + "pinText": "Épingler", + "unpinText": "DĂ©sĂ©pingler", + "pinnedCodeMessage": "{code} a Ă©tĂ© Ă©pinglĂ©", + "unpinnedCodeMessage": "{code} a Ă©tĂ© dĂ©sĂ©pinglĂ©", + "tags": "Tags", + "createNewTag": "CrĂ©er un nouveau tag", + "tag": "Tag", + "create": "CrĂ©er", + "editTag": "Modifier le tag", + "deleteTagTitle": "Supprimer le tag ?", + "deleteTagMessage": "Êtes-vous sĂ»r de vouloir supprimer ce tag ? Cette action est irrĂ©versible.", + "somethingWentWrongParsingCode": "Impossible d'analyser les codes {x}.", + "updateNotAvailable": "Mise Ă  jour non disponible" } \ No newline at end of file diff --git a/auth/lib/l10n/arb/app_pt.arb b/auth/lib/l10n/arb/app_pt.arb index 3f92822d9a..52960987f6 100644 --- a/auth/lib/l10n/arb/app_pt.arb +++ b/auth/lib/l10n/arb/app_pt.arb @@ -7,7 +7,7 @@ "description": "Text shown in the AppBar of the Counter Page" }, "onBoardingBody": "Proteja seus cĂłdigos 2FA", - "onBoardingGetStarted": "Vamos Começar", + "onBoardingGetStarted": "Introdução", "setupFirstAccount": "Configure sua primeira conta", "importScanQrCode": "Escanear QR code", "qrCode": "QR Code", @@ -31,13 +31,13 @@ "timeBasedKeyType": "Baseado no horĂĄrio (TOTP)", "counterBasedKeyType": "Baseado em um contador (HOTP)", "saveAction": "Salvar", - "nextTotpTitle": "prĂłximo", - "deleteCodeTitle": "Excluir cĂłdigo?", + "nextTotpTitle": "avançar", + "deleteCodeTitle": "Apagar cĂłdigo?", "deleteCodeMessage": "Tem certeza de que deseja excluir este cĂłdigo? Esta ação Ă© irreversĂ­vel.", "viewLogsAction": "Ver logs", "sendLogsDescription": "Isto irĂĄ compartilhar seus logs para nos ajudar a depurar seu problema. Embora tomemos precauçÔes para garantir que informaçÔes sensĂ­veis nĂŁo sejam enviadas, encorajamos vocĂȘ a ver esses logs antes de compartilhĂĄ-los.", "preparingLogsTitle": "Preparando logs...", - "emailLogsTitle": "Logs por e-mail", + "emailLogsTitle": "Logs (e-mail)", "emailLogsMessage": "Por favor, envie os logs para {email}", "@emailLogsMessage": { "placeholders": { @@ -48,9 +48,9 @@ }, "copyEmailAction": "Copiar e-mail", "exportLogsAction": "Exportar logs", - "reportABug": "Reportar um problema", + "reportABug": "Informar um problema", "crashAndErrorReporting": "Reporte de erros e falhas", - "reportBug": "Reportar problema", + "reportBug": "Informar problema", "emailUsMessage": "Por favor, envie um e-mail para {email}", "@emailUsMessage": { "placeholders": { @@ -105,14 +105,14 @@ "authToChangeYourPassword": "Por favor, autentique-se para alterar sua senha", "authToViewSecrets": "Por favor, autentique-se para ver as suas chaves secretas", "authToInitiateSignIn": "Por favor, autentique-se para iniciar o login para um backup.", - "ok": "Ok", + "ok": "OK", "cancel": "Cancelar", "yes": "Sim", "no": "NĂŁo", "email": "E-mail", "support": "Suporte", "general": "Geral", - "settings": "ConfiguraçÔes", + "settings": "Ajustes", "copied": "Copiado", "pleaseTryAgain": "Por favor, tente novamente", "existingUser": "UsuĂĄrio Existente", @@ -120,7 +120,7 @@ "delete": "Excluir", "enterYourPasswordHint": "Insira sua senha", "forgotPassword": "Esqueci a senha", - "oops": "Oops", + "oops": "Opa", "suggestFeatures": "Sugerir funcionalidades", "faq": "Perguntas frequentes", "faq_q_1": "QuĂŁo seguro Ă© o Auth?", @@ -139,7 +139,7 @@ "inFamilyPlanMessage": "VocĂȘ estĂĄ em um plano familiar!", "swipeHint": "Deslize para a esquerda para editar ou remover os cĂłdigos", "scan": "Escanear", - "scanACode": "Escanear um cĂłdigo", + "scanACode": "Escanear cĂłdigo", "verify": "Verificar", "verifyEmail": "Verificar e-mail", "enterCodeHint": "Digite o cĂłdigo de 6 dĂ­gitos de\nseu aplicativo autenticador", @@ -185,7 +185,7 @@ "lockScreenEnablePreSteps": "Para ativar o bloqueio de tela, por favor ative um mĂ©todo de autenticação nas configuraçÔes do sistema do seu dispositivo.", "viewActiveSessions": "Ver sessĂ”es ativas", "authToViewYourActiveSessions": "Por favor, autentique-se para ver as sessĂ”es ativas", - "searchHint": "Pesquisar...", + "searchHint": "Buscar...", "search": "Pesquisar", "sorryUnableToGenCode": "Desculpe, nĂŁo foi possĂ­vel gerar um cĂłdigo para {issuerName}", "noResult": "Nenhum resultado", @@ -239,10 +239,10 @@ "howItWorks": "Como funciona", "ackPasswordLostWarning": "Eu entendo que se eu perder minha senha, posso perder meus dados, jĂĄ que meus dados sĂŁo criptografados de ponta a ponta.", "loginTerms": "Ao clicar em login, eu concordo com os termos de serviço e a polĂ­tica de privacidade", - "logInLabel": "Login", - "logout": "Encerrar sessĂŁo", + "logInLabel": "Entrar", + "logout": "Sair", "areYouSureYouWantToLogout": "VocĂȘ tem certeza que deseja encerrar a sessĂŁo?", - "yesLogout": "Sim, encerrar sessĂŁo", + "yesLogout": "Sim, sair", "exit": "Sair", "verifyingRecoveryKey": "Verificando chave de recuperação...", "recoveryKeyVerified": "Chave de recuperação verificada", @@ -282,7 +282,7 @@ "description": "Text for the button to confirm the user understands the warning" }, "authToExportCodes": "Por favor, autentique-se para exportar seus cĂłdigos", - "importSuccessTitle": "Yay!", + "importSuccessTitle": "Oba!", "importSuccessDesc": "VocĂȘ importou {count} cĂłdigos!", "@importSuccessDesc": { "placeholders": { @@ -317,7 +317,7 @@ "thisWillLogYouOutOfTheFollowingDevice": "Isso farĂĄ com que vocĂȘ saia do seguinte dispositivo:", "terminateSession": "Encerrar sessĂŁo?", "terminate": "Encerrar", - "thisDevice": "Este dispositivo", + "thisDevice": "Esse dispositivo", "toResetVerifyEmail": "Para redefinir a sua senha, por favor verifique o seu email primeiro.", "thisEmailIsAlreadyInUse": "Este e-mail jĂĄ estĂĄ em uso", "verificationFailedPleaseTryAgain": "Falha na verificação. Por favor, tente novamente", @@ -339,7 +339,7 @@ "export": "Exportar", "useOffline": "Usar sem backups", "signInToBackup": "Entre para fazer backup de seus cĂłdigos", - "singIn": "Iniciar sessĂŁo", + "singIn": "Entrar", "sigInBackupReminder": "Por favor, exporte seus cĂłdigos para garantir que vocĂȘ tenha um backup do qual vocĂȘ possa restaurar.", "offlineModeWarning": "VocĂȘ escolheu prosseguir sem backups. Por favor, faça backups manuais para ter certeza de que seus cĂłdigos estĂŁo seguros.", "showLargeIcons": "Mostrar Ă­cones grandes", @@ -361,7 +361,7 @@ "@androidBiometricNotRecognized": { "description": "Message to let the user know that authentication was failed. It is used on Android side. Maximum 60 characters." }, - "androidBiometricSuccess": "Bem-sucedido", + "androidBiometricSuccess": "Êxito", "@androidBiometricSuccess": { "description": "Message to let the user know that authentication was successful. It is used on Android side. Maximum 60 characters." }, @@ -401,7 +401,7 @@ "@iOSGoToSettingsDescription": { "description": "Message advising the user to go to the settings and configure Biometrics for their device. It shows in a dialog on iOS side." }, - "iOSOkButton": "Ok", + "iOSOkButton": "OK", "@iOSOkButton": { "description": "Message showed on a button that the user can click to leave the current dialog. It is used on iOS side. Maximum 30 characters." }, @@ -425,13 +425,15 @@ "endpointUpdatedMessage": "Endpoint atualizado com sucesso", "customEndpoint": "Conectado a {endpoint}", "pinText": "Fixar", + "unpinText": "Desafixar", "pinnedCodeMessage": "{code} foi fixado", + "unpinnedCodeMessage": "{code} foi desafixado", "tags": "Etiquetas", "createNewTag": "Criar etiqueta", "tag": "Etiqueta", "create": "Criar", "editTag": "Editar etiqueta", - "deleteTagTitle": "Excluir etiqueta?", + "deleteTagTitle": "Apagar etiqueta?", "deleteTagMessage": "Tem certeza de que deseja excluir esta etiqueta? Essa ação Ă© irreversĂ­vel.", "somethingWentWrongParsingCode": "NĂŁo foi possĂ­vel analisar os cĂłdigos {x}.", "updateNotAvailable": "Atualização nĂŁo estĂĄ disponĂ­vel" diff --git a/auth/lib/l10n/arb/app_ru.arb b/auth/lib/l10n/arb/app_ru.arb index 42571a166b..9c5dceaaba 100644 --- a/auth/lib/l10n/arb/app_ru.arb +++ b/auth/lib/l10n/arb/app_ru.arb @@ -20,6 +20,8 @@ "codeIssuerHint": "Đ­ĐŒĐžŃ‚Đ”ĐœŃ‚", "codeSecretKeyHint": "ĐĄĐ”ĐșŃ€Đ”Ń‚ĐœŃ‹Đč Đșлюч", "codeAccountHint": "АĐșĐșĐ°ŃƒĐœŃ‚ (you@domain.com)", + "codeTagHint": "ĐœĐ”Ń‚Đșа", + "accountKeyType": "йОп Đșлюча", "sessionExpired": "ĐĄĐ”Đ°ĐœŃ ОстДĐș", "@sessionExpired": { "description": "Title of the dialog when the users current session is invalid/expired" @@ -77,16 +79,19 @@ "data": "Đ”Đ°ĐœĐœŃ‹Đ”", "importCodes": "Đ˜ĐŒĐżĐŸŃ€Ń‚ĐžŃ€ĐŸĐČать ĐșĐŸĐŽŃ‹", "importTypePlainText": "ĐžĐ±Ń‹Ń‡ĐœŃ‹Đč тДĐșст", + "importTypeEnteEncrypted": "Ente Đ—Đ°ŃˆĐžŃ„Ń€ĐŸĐČĐ°ĐœĐœŃ‹Đč эĐșŃĐżĐŸŃ€Ń‚", "passwordForDecryptingExport": "ĐŸĐ°Ń€ĐŸĐ»ŃŒ ĐŽĐ»Ń Ń€Đ°ŃŃˆĐžŃ„Ń€ĐŸĐČĐșĐž эĐșŃĐżĐŸŃ€Ń‚Đ°", "passwordEmptyError": "ĐŸĐ°Ń€ĐŸĐ»ŃŒ ĐœĐ” ĐŒĐŸĐ¶Đ”Ń‚ Đ±Ń‹Ń‚ŃŒ ĐżŃƒŃŃ‚Ń‹ĐŒ", "importFromApp": "Đ˜ĐŒĐżĐŸŃ€Ń‚ ĐșĐŸĐŽĐŸĐČ ĐžĐ· {appName}", "importGoogleAuthGuide": "Đ­ĐșŃĐżĐŸŃ€Ń‚ĐžŃ€ŃƒĐčтД ŃƒŃ‡Đ”Ń‚ĐœŃ‹Đ” запОсО Оз Google Authenticator ĐČ QR-ĐșĐŸĐŽ, ĐžŃĐżĐŸĐ»ŃŒĐ·ŃƒŃ ĐŸĐżŃ†ĐžŃŽ Â«ĐŸĐ”Ń€Đ”ĐœĐ”ŃŃ‚Đž ŃƒŃ‡Đ”Ń‚ĐœŃ‹Đ” запОсО». Đ—Đ°Ń‚Đ”ĐŒ с ĐżĐŸĐŒĐŸŃ‰ŃŒŃŽ ĐŽŃ€ŃƒĐłĐŸĐłĐŸ ŃƒŃŃ‚Ń€ĐŸĐčстĐČа ĐŸŃ‚ŃĐșĐ°ĐœĐžŃ€ŃƒĐčтД QR-ĐșĐŸĐŽ.\n\nĐĄĐŸĐČДт: Đ§Ń‚ĐŸĐ±Ń‹ ŃŃ„ĐŸŃ‚ĐŸĐłŃ€Đ°Ń„ĐžŃ€ĐŸĐČать QR-ĐșĐŸĐŽ, ĐŒĐŸĐ¶ĐœĐŸ ĐČĐŸŃĐżĐŸĐ»ŃŒĐ·ĐŸĐČаться ĐČДб-ĐșĐ°ĐŒĐ”Ń€ĐŸĐč ĐœĐŸŃƒŃ‚Đ±ŃƒĐșа.", "importSelectJsonFile": "Đ’Ń‹Đ±Ń€Đ°Ń‚ŃŒ JSON-фаĐčĐ»", "importSelectAppExport": "Đ’Ń‹Đ±Ń€Đ°Ń‚ŃŒ фаĐčĐ» эĐșŃĐżĐŸŃ€Ń‚Đ° {appName}", + "importEnteEncGuide": "ВыбДрОтД Đ·Đ°ŃˆĐžŃ„Ń€ĐŸĐČĐ°ĐœĐœŃ‹Đč JSON фаĐčĐ», эĐșŃĐżĐŸŃ€Ń‚ĐžŃ€ĐŸĐČĐ°ĐœĐœŃ‹Đč Оз Ente", "importRaivoGuide": "Đ˜ŃĐżĐŸĐ»ŃŒĐ·ŃƒĐčтД ĐŸĐżŃ†ĐžŃŽ «Export OTPs to Zip archive» ĐČ ĐœĐ°ŃŃ‚Ń€ĐŸĐčĐșах Raivo.\n\nРаспаĐșуĐčтД zip-архоĐČ Đž ĐžĐŒĐżĐŸŃ€Ń‚ĐžŃ€ŃƒĐčтД JSON-фаĐčĐ».", "importBitwardenGuide": "Đ˜ŃĐżĐŸĐ»ŃŒĐ·ŃƒĐčтД ĐŸĐżŃ†ĐžŃŽ \"Đ­ĐșŃĐżĐŸŃ€Ń‚ĐžŃ€ĐŸĐČать Ń…Ń€Đ°ĐœĐžĐ»ĐžŃ‰Đ”\" ĐČ Bitwarden Tools Đž ĐžĐŒĐżĐŸŃ€Ń‚ĐžŃ€ŃƒĐčтД ĐœĐ”Đ·Đ°ŃˆĐžŃ„Ń€ĐŸĐČĐ°ĐœĐœŃ‹Đč JSON фаĐčĐ».", "importAegisGuide": "Đ˜ŃĐżĐŸĐ»ŃŒĐ·ŃƒĐčтД ĐŸĐżŃ†ĐžŃŽ «ЭĐșŃĐżĐŸŃ€Ń‚ĐžŃ€ĐŸĐČать Ń…Ń€Đ°ĐœĐžĐ»ĐžŃ‰Đ”Â» ĐČ ĐœĐ°ŃŃ‚Ń€ĐŸĐčĐșах Aegis.\n\nЕслО ĐČашД Ń…Ń€Đ°ĐœĐžĐ»ĐžŃ‰Đ” Đ·Đ°ŃˆĐžŃ„Ń€ĐŸĐČĐ°ĐœĐŸ, Ń‚ĐŸ ĐŽĐ»Ń Đ”ĐłĐŸ Ń€Đ°ŃŃˆĐžŃ„Ń€ĐŸĐČĐșĐž ĐżĐŸŃ‚Ń€Đ”Đ±ŃƒĐ”Ń‚ŃŃ ĐČĐČДстО ĐżĐ°Ń€ĐŸĐ»ŃŒ Ń…Ń€Đ°ĐœĐžĐ»ĐžŃ‰Đ°.", "import2FasGuide": "Đ˜ŃĐżĐŸĐ»ŃŒĐ·ŃƒĐčтД ĐŸĐżŃ†ĐžŃŽ \"Settings->Backup -Export\" ĐČ 2FAS.\n\nЕслО ĐČаша рДзДрĐČĐœĐ°Ń ĐșĐŸĐżĐžŃ Đ·Đ°ŃˆĐžŃ„Ń€ĐŸĐČĐ°ĐœĐ°, Ń‚ĐŸ ĐŽĐ»Ń Ń€Đ°ŃŃˆĐžŃ„Ń€ĐŸĐČĐșĐž рДзДрĐČĐœĐŸĐč ĐșĐŸĐżĐžĐž ĐœĐ”ĐŸĐ±Ń…ĐŸĐŽĐžĐŒĐŸ ĐČĐČДстО ĐżĐ°Ń€ĐŸĐ»ŃŒ", + "importLastpassGuide": "Đ˜ŃĐżĐŸĐ»ŃŒĐ·ŃƒĐčтД ĐŸĐżŃ†ĐžŃŽ \"ĐŸĐ”Ń€Đ”ĐœĐ”ŃŃ‚Đž аĐșĐșĐ°ŃƒĐœŃ‚Ń‹\" ĐČ ĐœĐ°ŃŃ‚Ń€ĐŸĐčĐșах Lastpass Authenticator Đž ĐœĐ°Đ¶ĐŒĐžŃ‚Đ” ĐœĐ° \"Đ­ĐșŃĐżĐŸŃ€Ń‚ĐžŃ€ĐŸĐČать ŃƒŃ‡Đ”Ń‚ĐœŃ‹Đ” запОсО ĐČ Ń„Đ°ĐčĐ»\". Đ˜ĐŒĐżĐŸŃ€Ń‚ĐžŃ€ŃƒĐčтД Đ·Đ°ĐłŃ€ŃƒĐ¶Ń‘ĐœĐœŃ‹Đč JSON фаĐčĐ».", "exportCodes": "Đ­ĐșŃĐżĐŸŃ€Ń‚ĐžŃ€ĐŸĐČать ĐșĐŸĐŽŃ‹", "importLabel": "Đ˜ĐŒĐżĐŸŃ€Ń‚", "importInstruction": "ĐŸĐŸĐ¶Đ°Đ»ŃƒĐčста, ĐČыбДрОтД фаĐčĐ», ŃĐŸĐŽĐ”Ń€Đ¶Đ°Ń‰ĐžĐč ŃĐżĐžŃĐŸĐș ĐČашох ĐșĐŸĐŽĐŸĐČ ĐČ ŃĐ»Đ”ĐŽŃƒŃŽŃ‰Đ”ĐŒ Ń„ĐŸŃ€ĐŒĐ°Ń‚Đ”", @@ -99,6 +104,7 @@ "authToChangeYourEmail": "ĐŸĐŸĐ¶Đ°Đ»ŃƒĐčста, аĐČŃ‚ĐŸŃ€ĐžĐ·ŃƒĐčŃ‚Đ”ŃŃŒ, Ń‡Ń‚ĐŸĐ±Ń‹ ĐžĐ·ĐŒĐ”ĐœĐžŃ‚ŃŒ аЎрДс ŃĐ»Đ”ĐșŃ‚Ń€ĐŸĐœĐœĐŸĐč ĐżĐŸŃ‡Ń‚Ń‹", "authToChangeYourPassword": "ĐŸĐŸĐ¶Đ°Đ»ŃƒĐčста, аĐČŃ‚ĐŸŃ€ĐžĐ·ŃƒĐčŃ‚Đ”ŃŃŒ, Ń‡Ń‚ĐŸĐ±Ń‹ ĐžĐ·ĐŒĐ”ĐœĐžŃ‚ŃŒ ĐżĐ°Ń€ĐŸĐ»ŃŒ", "authToViewSecrets": "ĐŸĐŸĐ¶Đ°Đ»ŃƒĐčста, аĐČŃ‚ĐŸŃ€ĐžĐ·ŃƒĐčŃ‚Đ”ŃŃŒ ĐŽĐ»Ń ĐżŃ€ĐŸŃĐŒĐŸŃ‚Ń€Đ° ĐČашох сДĐșŃ€Đ”Ń‚ĐŸĐČ", + "authToInitiateSignIn": "ĐŸĐŸĐ¶Đ°Đ»ŃƒĐčста, аĐČŃ‚ĐŸŃ€ĐžĐ·ŃƒĐčŃ‚Đ”ŃŃŒ, Ń‡Ń‚ĐŸĐ±Ń‹ ĐœĐ°Ń‡Đ°Ń‚ŃŒ ĐČŃ…ĐŸĐŽ ĐŽĐ»Ń рДзДрĐČĐœĐŸĐłĐŸ ĐșĐŸĐżĐžŃ€ĐŸĐČĐ°ĐœĐžŃ.", "ok": "ОĐș", "cancel": "ĐžŃ‚ĐŒĐ”ĐœĐžŃ‚ŃŒ", "yes": "Да", @@ -110,18 +116,22 @@ "copied": "ĐĄĐșĐŸĐżĐžŃ€ĐŸĐČĐ°ĐœĐŸ", "pleaseTryAgain": "ĐŸĐŸĐ¶Đ°Đ»ŃƒĐčста, ĐżĐŸĐżŃ€ĐŸĐ±ŃƒĐčтД Дщё раз", "existingUser": "ĐĄŃƒŃ‰Đ”ŃŃ‚ĐČующоĐč ĐżĐŸĐ»ŃŒĐ·ĐŸĐČĐ°Ń‚Đ”Đ»ŃŒ", + "newUser": "ВпДрĐČыД Đ·ĐŽĐ”ŃŃŒ, ĐČ Ente", "delete": "ĐŁĐŽĐ°Đ»ĐžŃ‚ŃŒ", "enterYourPasswordHint": "ВĐČДЎОтД ĐżĐ°Ń€ĐŸĐ»ŃŒ", "forgotPassword": "Забыл ĐżĐ°Ń€ĐŸĐ»ŃŒ", "oops": "ОĐč", "suggestFeatures": "ĐŸŃ€Đ”ĐŽĐ»ĐŸĐ¶ĐžŃ‚ŃŒ ОЎДО", "faq": "FAQ", + "faq_q_1": "НасĐșĐŸĐ»ŃŒĐșĐŸ Đ±Đ”Đ·ĐŸĐżĐ°ŃĐ”Đœ Auth?", + "faq_a_1": "ВсД ĐșĐŸĐŽŃ‹, ĐșĐŸŃ‚ĐŸŃ€Ń‹Đ” ĐČы рДзДрĐČĐžŃ€ŃƒĐ”Ń‚Đ” с ĐżĐŸĐŒĐŸŃ‰ŃŒŃŽ Auth, Ń…Ń€Đ°ĐœŃŃ‚ŃŃ ĐČ Đ·Đ°ŃˆĐžŃ„Ń€ĐŸĐČĐ°ĐœĐœĐŸĐŒ ĐČОЎД. Đ­Ń‚ĐŸ ĐŸĐ·ĐœĐ°Ń‡Đ°Đ”Ń‚, Ń‡Ń‚ĐŸ Ń‚ĐŸĐ»ŃŒĐșĐŸ ĐČы ĐŒĐŸĐ¶Đ”Ń‚Đ” ĐżĐŸĐ»ŃƒŃ‡ĐžŃ‚ŃŒ ĐŽĐŸŃŃ‚ŃƒĐż Đș сĐČĐŸĐžĐŒ ĐșĐŸĐŽĐ°ĐŒ. Нашо ĐżŃ€ĐžĐ»ĐŸĐ¶Đ”ĐœĐžŃ ĐžĐŒĐ”ŃŽŃ‚ ĐŸŃ‚ĐșрытыĐč ĐžŃŃ…ĐŸĐŽĐœŃ‹Đč ĐșĐŸĐŽ, а ĐœĐ°ŃˆĐ° ĐșŃ€ĐžĐżŃ‚ĐŸĐłŃ€Đ°Ń„ĐžŃ ĐżŃ€ĐŸŃˆĐ»Đ° ĐČĐœĐ”ŃˆĐœĐžĐč ауЮот.", "faq_q_2": "ĐœĐŸĐłŃƒ лО я ĐżĐŸĐ»ŃƒŃ‡ĐžŃ‚ŃŒ ĐŽĐŸŃŃ‚ŃƒĐż Đș ĐŒĐŸĐžĐŒ ĐșĐŸĐŽĐ°ĐŒ ĐœĐ° ĐșĐŸĐŒĐżŃŒŃŽŃ‚Đ”Ń€Đ”?", "faq_a_2": "Вы ĐŒĐŸĐ¶Đ”Ń‚Đ” ĐżĐŸĐ»ŃƒŃ‡ĐžŃ‚ŃŒ ĐŽĐŸŃŃ‚ŃƒĐż Đș сĐČĐŸĐžĐŒ ĐșĐŸĐŽĐ°ĐŒ ĐœĐ° саĐčтД @ auth.ente.io.", "faq_q_3": "КаĐș я ĐŒĐŸĐłŃƒ ŃƒĐŽĐ°Đ»ĐžŃ‚ŃŒ ĐșĐŸĐŽŃ‹?", "faq_a_3": "Вы ĐŒĐŸĐ¶Đ”Ń‚Đ” ŃƒĐŽĐ°Đ»ĐžŃ‚ŃŒ ĐșĐŸĐŽ, ĐżŃ€ĐŸĐČĐ”ĐŽŃ ĐżĐ°Đ»ŃŒŃ†Đ”ĐŒ ĐČлДĐČĐŸ ĐżĐŸ ŃŃ‚ĐŸĐŒŃƒ ŃĐ»Đ”ĐŒĐ”ĐœŃ‚Ńƒ.", "faq_q_4": "КаĐș я ĐŒĐŸĐłŃƒ ĐżĐŸĐŽĐŽĐ”Ń€Đ¶Đ°Ń‚ŃŒ ŃŃ‚ĐŸŃ‚ ĐżŃ€ĐŸĐ”Đșт?", "faq_a_4": "Вы ĐŒĐŸĐ¶Đ”Ń‚Đ” ĐżĐŸĐŽĐŽĐ”Ń€Đ¶Đ°Ń‚ŃŒ разĐČОтОД ŃŃ‚ĐŸĐłĐŸ ĐżŃ€ĐŸĐ”Đșта, ĐżĐŸĐŽĐżĐžŃĐ°ĐČшось ĐœĐ° ĐœĐ°ŃˆĐ” ĐżŃ€ĐžĐ»ĐŸĐ¶Đ”ĐœĐžĐ” Photos @ ente.io.", + "faq_q_5": "КаĐș ĐŒĐœĐ” ĐČĐșĐ»ŃŽŃ‡ĐžŃ‚ŃŒ FaceID ĐČ Auth", "faq_a_5": "Вы ĐŒĐŸĐ¶Đ”Ń‚Đ” ĐČĐșĐ»ŃŽŃ‡ĐžŃ‚ŃŒ Đ±Đ»ĐŸĐșĐžŃ€ĐŸĐČĐșу FaceID ĐČ ĐĐ°ŃŃ‚Ń€ĐŸĐčĐșĐž → Đ‘Đ”Đ·ĐŸĐżĐ°ŃĐœĐŸŃŃ‚ŃŒ → Đ­ĐșŃ€Đ°Đœ Đ±Đ»ĐŸĐșĐžŃ€ĐŸĐČĐșĐž.", "somethingWentWrongMessage": "Đ§Ń‚ĐŸ-Ń‚ĐŸ ĐżĐŸŃˆĐ»ĐŸ ĐœĐ” таĐș. ĐŸĐŸĐżŃ€ĐŸĐ±ŃƒĐčтД ДщД раз", "leaveFamily": "ĐŸĐŸĐșĐžĐœŃƒŃ‚ŃŒ ŃĐ”ĐŒŃŒŃŽ", @@ -135,6 +145,8 @@ "enterCodeHint": "ВĐČДЎОтД 6-Đ·ĐœĐ°Ń‡ĐœŃ‹Đč ĐșĐŸĐŽ Оз\nĐČĐ°ŃˆĐ”ĐłĐŸ ĐżŃ€ĐžĐ»ĐŸĐ¶Đ”ĐœĐžŃ-Đ°ŃƒŃ‚Đ”ĐœŃ‚ĐžŃ„ĐžĐșĐ°Ń‚ĐŸŃ€Đ°", "lostDeviceTitle": "ĐŸĐŸŃ‚Đ”Ń€ŃĐœĐŸ ŃƒŃŃ‚Ń€ĐŸĐčстĐČĐŸ?", "twoFactorAuthTitle": "ДĐČухфаĐșŃ‚ĐŸŃ€ĐœĐ°Ń Đ°ŃƒŃ‚Đ”ĐœŃ‚ĐžŃ„ĐžĐșацоя", + "passkeyAuthTitle": "ĐŸŃ€ĐŸĐČДрĐșа с ĐżĐŸĐŒĐŸŃ‰ŃŒŃŽ ĐżĐ°Ń€ĐŸĐ»Ń", + "verifyPasskey": "ĐŸĐŸĐŽŃ‚ĐČĐ”Ń€ĐŽĐžŃ‚ŃŒ ĐżĐ°Ń€ĐŸĐ»ŃŒ", "recoverAccount": "Đ’ĐŸŃŃŃ‚Đ°ĐœĐŸĐČоть аĐșĐșĐ°ŃƒĐœŃ‚", "enterRecoveryKeyHint": "ВĐČДЎОтД сĐČĐŸĐč Đșлюч ĐČĐŸŃŃŃ‚Đ°ĐœĐŸĐČĐ»Đ”ĐœĐžŃ", "recover": "Đ’ĐŸŃŃŃ‚Đ°ĐœĐŸĐČоть", @@ -146,6 +158,7 @@ } } }, + "invalidQRCode": "ĐĐ”ĐČĐ”Ń€ĐœŃ‹Đč QR-ĐșĐŸĐŽ", "noRecoveryKeyTitle": "ĐĐ”Ń‚ Đșлюча ĐČĐŸŃŃŃ‚Đ°ĐœĐŸĐČĐ»Đ”ĐœĐžŃ?", "enterEmailHint": "ВĐČДЎОтД сĐČĐŸŃŽ ĐżĐŸŃ‡Ń‚Ńƒ", "invalidEmailTitle": "ĐĐ”ĐČĐ”Ń€ĐœŃ‹Đč аЎрДс ŃĐ»Đ”ĐșŃ‚Ń€ĐŸĐœĐœĐŸĐč ĐżĐŸŃ‡Ń‚Ń‹", @@ -190,6 +203,8 @@ "saveKey": "ĐĄĐŸŃ…Ń€Đ°ĐœĐžŃ‚ŃŒ Đșлюч", "save": "ĐĄĐŸŃ…Ń€Đ°ĐœĐžŃ‚ŃŒ", "send": "ОтпраĐČоть", + "saveOrSendDescription": "Вы Ń…ĐŸŃ‚ĐžŃ‚Đ” ŃĐŸŃ…Ń€Đ°ĐœĐžŃ‚ŃŒ ŃŃ‚ĐŸ ĐČ Ń…Ń€Đ°ĐœĐžĐ»ĐžŃ‰Đ” (папĐșу Đ·Đ°ĐłŃ€ŃƒĐ·ĐŸĐș ĐżĐŸ ŃƒĐŒĐŸĐ»Ń‡Đ°ĐœĐžŃŽ) ОлО ĐŸŃ‚ĐżŃ€Đ°ĐČоть ĐČ ĐŽŃ€ŃƒĐłĐžĐ” ĐżŃ€ĐžĐ»ĐŸĐ¶Đ”ĐœĐžŃ?", + "saveOnlyDescription": "Вы Ń…ĐŸŃ‚ĐžŃ‚Đ” ŃĐŸŃ…Ń€Đ°ĐœĐžŃ‚ŃŒ ŃŃ‚ĐŸ ĐČ Ń…Ń€Đ°ĐœĐžĐ»ĐžŃ‰Đ” (ĐżĐŸ ŃƒĐŒĐŸĐ»Ń‡Đ°ĐœĐžŃŽ папĐșа Đ·Đ°ĐłŃ€ŃƒĐ·ĐŸĐș)?", "back": "Đ’Đ”Ń€ĐœŃƒŃ‚ŃŒŃŃ", "createAccount": "ĐĄĐŸĐ·ĐŽĐ°Ń‚ŃŒ аĐșĐșĐ°ŃƒĐœŃ‚", "passwordStrength": "ĐœĐŸŃ‰ĐœĐŸŃŃ‚ŃŒ ĐżĐ°Ń€ĐŸĐ»Ń: {passwordStrengthValue}", @@ -337,6 +352,7 @@ "deleteCodeAuthMessage": "ĐŃƒŃ‚Đ”ĐœŃ‚ĐžŃ„ĐžĐșацоя ĐŽĐ»Ń ŃƒĐŽĐ°Đ»Đ”ĐœĐžŃ ĐșĐŸĐŽĐ°", "showQRAuthMessage": "ĐŃƒŃ‚Đ”ĐœŃ‚ĐžŃ„ĐžĐșацоя ĐŽĐ»Ń ĐŸŃ‚ĐŸĐ±Ń€Đ°Đ¶Đ”ĐœĐžŃ QR-ĐșĐŸĐŽĐ°", "confirmAccountDeleteTitle": "ĐŸĐŸĐŽŃ‚ĐČĐ”Ń€ĐŽĐžŃ‚ŃŒ ŃƒĐŽĐ°Đ»Đ”ĐœĐžĐ” аĐșĐșĐ°ŃƒĐœŃ‚Đ°", + "confirmAccountDeleteMessage": "Эта ŃƒŃ‡Đ”Ń‚ĐœĐ°Ń Đ·Đ°ĐżĐžŃŃŒ сĐČŃĐ·Đ°ĐœĐ° с ĐŽŃ€ŃƒĐłĐžĐŒĐž ĐżŃ€ĐžĐ»ĐŸĐ¶Đ”ĐœĐžŃĐŒĐž Ente, ДслО ĐČы ĐžĐŒĐž ĐżĐŸĐ»ŃŒĐ·ŃƒĐ”Ń‚Đ”ŃŃŒ.\n\nĐ—Đ°ĐłŃ€ŃƒĐ¶Đ”ĐœĐœŃ‹Đ” ĐČĐ°ĐŒĐž ĐŽĐ°ĐœĐœŃ‹Đ” ĐČĐŸ ĐČсДх ĐżŃ€ĐžĐ»ĐŸĐ¶Đ”ĐœĐžŃŃ… ente Đ±ŃƒĐŽŃƒŃ‚ Đ·Đ°ĐżĐ»Đ°ĐœĐžŃ€ĐŸĐČĐ°ĐœŃ‹ Đș ŃƒĐŽĐ°Đ»Đ”ĐœĐžŃŽ, а ĐČаша ŃƒŃ‡Đ”Ń‚ĐœĐ°Ń Đ·Đ°ĐżĐžŃŃŒ Đ±ŃƒĐŽĐ”Ń‚ ŃƒĐŽĐ°Đ»Đ”ĐœĐ° бДз ĐČĐŸĐ·ĐŒĐŸĐ¶ĐœĐŸŃŃ‚Đž ĐČĐŸŃŃŃ‚Đ°ĐœĐŸĐČĐ»Đ”ĐœĐžŃ.", "androidBiometricHint": "ĐŸĐŸĐŽŃ‚ĐČДрЎОтД Đ»ĐžŃ‡ĐœĐŸŃŃ‚ŃŒ", "@androidBiometricHint": { "description": "Hint message advising the user how to authenticate with biometrics. It is used on Android side. Maximum 60 characters." @@ -397,12 +413,28 @@ "doNotSignOut": "ĐĐ” ĐČŃ‹Ń…ĐŸĐŽĐžŃ‚ŃŒ", "hearUsWhereTitle": "КаĐș ĐČы ŃƒĐ·ĐœĐ°Đ»Đž ĐŸ Ente? (ĐœĐ”ĐŸĐ±ŃĐ·Đ°Ń‚Đ”Đ»ŃŒĐœĐŸ)", "hearUsExplanation": "Đ‘ŃƒĐŽĐ”Ń‚ ĐżĐŸĐ»Đ”Đ·ĐœĐŸ, ДслО ĐČы уĐșажДтД, гЎД ĐœĐ°ŃˆĐ»Đž ĐœĐ°Ń, таĐș ĐșаĐș ĐŒŃ‹ ĐœĐ” ĐŸŃ‚ŃĐ»Đ”Đ¶ĐžĐČĐ°Đ”ĐŒ ŃƒŃŃ‚Đ°ĐœĐŸĐČĐșĐž ĐżŃ€ĐžĐ»ĐŸĐ¶Đ”ĐœĐžŃ", + "recoveryKeySaved": "Ключ ĐČĐŸŃŃŃ‚Đ°ĐœĐŸĐČĐ»Đ”ĐœĐžŃ ŃĐŸŃ…Ń€Đ°ĐœŃ‘Đœ ĐČ ĐżĐ°ĐżĐșĐ” Đ—Đ°ĐłŃ€ŃƒĐ·ĐșĐž!", + "waitingForBrowserRequest": "ĐžĐ¶ĐžĐŽĐ°ĐœĐžĐ” Đ·Đ°ĐżŃ€ĐŸŃĐ° Đ±Ń€Đ°ŃƒĐ·Đ”Ń€Đ°...", "waitingForVerification": "ĐžĐ¶ĐžĐŽĐ°ĐœĐžĐ” ĐżĐŸĐŽŃ‚ĐČĐ”Ń€Đ¶ĐŽĐ”ĐœĐžŃ...", + "passkey": "Ключ", "developerSettingsWarning": "Вы уĐČĐ”Ń€Đ”ĐœŃ‹, Ń‡Ń‚ĐŸ Ń…ĐŸŃ‚ĐžŃ‚Đ” ĐžĐ·ĐŒĐ”ĐœĐžŃ‚ŃŒ ĐœĐ°ŃŃ‚Ń€ĐŸĐčĐșĐž Ń€Đ°Đ·Ń€Đ°Đ±ĐŸŃ‚Ń‡ĐžĐșа?", "developerSettings": "ĐĐ°ŃŃ‚Ń€ĐŸĐčĐșĐž Ń€Đ°Đ·Ń€Đ°Đ±ĐŸŃ‚Ń‡ĐžĐșа", "serverEndpoint": "ĐšĐŸĐœĐ”Ń‡ĐœĐ°Ń Ń‚ĐŸŃ‡Đșа сДрĐČДра", "invalidEndpoint": "ĐĐ”ĐČĐ”Ń€ĐœĐ°Ń ĐșĐŸĐœĐ”Ń‡ĐœĐ°Ń Ń‚ĐŸŃ‡Đșа", "invalidEndpointMessage": "ИзĐČĐžĐœĐžŃ‚Đ”, ĐČĐČĐ”ĐŽĐ”ĐœĐœĐ°Ń ĐČĐ°ĐŒĐž ĐșĐŸĐœĐ”Ń‡ĐœĐ°Ń Ń‚ĐŸŃ‡Đșа ĐœĐ”ĐČĐ”Ń€ĐœĐ°. ĐŸĐŸĐ¶Đ°Đ»ŃƒĐčста, ĐČĐČДЎОтД ĐșĐŸŃ€Ń€Đ”ĐșŃ‚ĐœŃƒŃŽ ĐșĐŸĐœĐ”Ń‡ĐœŃƒŃŽ Ń‚ĐŸŃ‡Đșу Đž ĐżĐŸĐČŃ‚ĐŸŃ€ĐžŃ‚Đ” ĐżĐŸĐżŃ‹Ń‚Đșу.", "endpointUpdatedMessage": "ĐšĐŸĐœĐ”Ń‡ĐœĐ°Ń Ń‚ĐŸŃ‡Đșа ŃƒŃĐżĐ”ŃˆĐœĐŸ ĐŸĐ±ĐœĐŸĐČĐ»Đ”ĐœĐ°", - "customEndpoint": "ĐŸĐŸĐŽĐșĐ»ŃŽŃ‡Đ”ĐœĐŸ Đș {endpoint}" + "customEndpoint": "ĐŸĐŸĐŽĐșĐ»ŃŽŃ‡Đ”ĐœĐŸ Đș {endpoint}", + "pinText": "ПроĐșŃ€Đ”ĐżĐžŃ‚ŃŒ", + "unpinText": "ОтĐșŃ€Đ”ĐżĐžŃ‚ŃŒ", + "pinnedCodeMessage": "{code} проĐșŃ€Đ”ĐżĐ»Đ”Đœ", + "unpinnedCodeMessage": "{code} ĐŸŃ‚ĐșŃ€Đ”ĐżĐ»Đ”Đœ", + "tags": "ĐœĐ”Ń‚ĐșĐž", + "createNewTag": "ĐĄĐŸĐ·ĐŽĐ°Ń‚ŃŒ ĐœĐŸĐČую ĐŒĐ”Ń‚Đșу", + "tag": "ĐœĐ”Ń‚Đșа", + "create": "ĐĄĐŸĐ·ĐŽĐ°Ń‚ŃŒ", + "editTag": "Đ˜Đ·ĐŒĐ”ĐœĐžŃ‚ŃŒ ĐŒĐ”Ń‚Đșу", + "deleteTagTitle": "ĐŁĐŽĐ°Đ»ĐžŃ‚ŃŒ ĐŒĐ”Ń‚Đșу?", + "deleteTagMessage": "Вы уĐČĐ”Ń€Đ”ĐœŃ‹, Ń‡Ń‚ĐŸ Ń…ĐŸŃ‚ĐžŃ‚Đ” ŃƒĐŽĐ°Đ»ĐžŃ‚ŃŒ эту ĐŒĐ”Ń‚Đșу? Đ­Ń‚ĐŸ ĐŽĐ”ĐčстĐČОД ĐœĐ”ĐŸĐ±Ń€Đ°Ń‚ĐžĐŒĐŸ.", + "somethingWentWrongParsingCode": "Мы ĐœĐ” ŃĐŒĐŸĐłĐ»Đž Ń€Đ°Đ·ĐŸĐ±Ń€Đ°Ń‚ŃŒ ĐșĐŸĐŽŃ‹ {x}.", + "updateNotAvailable": "ĐžĐ±ĐœĐŸĐČĐ»Đ”ĐœĐžĐ” ĐœĐ”ĐŽĐŸŃŃ‚ŃƒĐżĐœĐŸ" } \ No newline at end of file diff --git a/auth/lib/l10n/arb/app_sv.arb b/auth/lib/l10n/arb/app_sv.arb index 9761325ce1..41aa2f8a86 100644 --- a/auth/lib/l10n/arb/app_sv.arb +++ b/auth/lib/l10n/arb/app_sv.arb @@ -61,6 +61,7 @@ "welcomeBack": "VĂ€lkommen tillbaka!", "changePassword": "Ändra lösenord", "importCodes": "Importera koder", + "exportCodes": "Exportera koder", "cancel": "Avbryt", "yes": "Ja", "no": "Nej", @@ -76,6 +77,7 @@ "scan": "Skanna", "twoFactorAuthTitle": "TvĂ„faktorsautentisering", "enterRecoveryKeyHint": "Ange din Ă„terstĂ€llningsnyckel", + "invalidQRCode": "Ogiltig QR-kod", "noRecoveryKeyTitle": "Ingen Ă„terstĂ€llningsnyckel?", "enterEmailHint": "Ange din e-postadress", "invalidEmailTitle": "Ogiltig e-postadress", @@ -143,6 +145,8 @@ }, "pendingSyncs": "Varning", "activeSessions": "Aktiva sessioner", + "incorrectCode": "Felaktig kod", + "incorrectRecoveryKey": "Felaktig Ă„terstĂ€llningsnyckel", "enterPassword": "Ange lösenord", "export": "Exportera", "singIn": "Logga in", diff --git a/auth/lib/l10n/arb/app_tr.arb b/auth/lib/l10n/arb/app_tr.arb index 322af5f48c..2473067b74 100644 --- a/auth/lib/l10n/arb/app_tr.arb +++ b/auth/lib/l10n/arb/app_tr.arb @@ -20,6 +20,8 @@ "codeIssuerHint": "Yayınlayan", "codeSecretKeyHint": "Gizli Anahtar", "codeAccountHint": "Hesap (ornek@domain.com)", + "codeTagHint": "Etiket", + "accountKeyType": "Anahtar tĂŒrĂŒ", "sessionExpired": "Oturum sĂŒresi doldu", "@sessionExpired": { "description": "Title of the dialog when the users current session is invalid/expired" @@ -46,7 +48,7 @@ }, "copyEmailAction": "E-postayı Kopyala", "exportLogsAction": "GĂŒnlĂŒÄŸĂŒ dıßa aktar", - "reportABug": "Bir hata bildir", + "reportABug": "Hata bildirin", "crashAndErrorReporting": "Çökme ve hata bildirimi", "reportBug": "Hata bildir", "emailUsMessage": "LĂŒtfen bize {email} adresinden ulaßın", @@ -77,12 +79,14 @@ "data": "Veri", "importCodes": "Kodu içe aktar", "importTypePlainText": "Salt metin", + "importTypeEnteEncrypted": "Ente ƞifreli dıßa aktarma", "passwordForDecryptingExport": "Dıßa aktarımın ßifresini çözmek için parola", "passwordEmptyError": "ƞifre boß olamaz", "importFromApp": "Kodları {appName} uygulamasından içe aktarın", "importGoogleAuthGuide": "\"Hesapları Aktar\" seçeneğini kullanarak hesaplarınızı Google Authenticator'dan bir QR koduna aktarın. Ardından baßka bir cihaz kullanarak QR kodunu tarayın.\n\nİpucu: QR kodunun fotoğrafını çekmek için dizĂŒstĂŒ bilgisayarınızın kamerasını kullanabilirsiniz.", "importSelectJsonFile": "JSON dosyasını seçin", "importSelectAppExport": "{appName} dıßarı aktarma dosyasını seçin", + "importEnteEncGuide": "Ente'den dıßa aktarılan ßifrelenmiß JSON dosyasını seçin", "importRaivoGuide": "Raivo'nun ayarlarında \"OTP'leri Zip arßivine aktar\" seçeneğini kullanın.\n\nZip dosyasını çıkarın ve JSON dosyasını içe aktarın.", "importBitwardenGuide": "Bitwarden Tools içindeki \"Kasayı dıßa aktar\" seçeneğini kullanın ve ßifrelenmemiß JSON dosyasını içe aktarın.", "importAegisGuide": "Aegis'in Ayarlarında \"Kasayı dıßa aktar\" seçeneğini kullanın.\n\nKasanız ßifrelenmißse, kasanın ßifresini çözmek için kasa parolasını girmeniz gerekecektir.", @@ -112,18 +116,22 @@ "copied": "Kopyalandı", "pleaseTryAgain": "LĂŒtfen tekrar deneyin", "existingUser": "Mevcut kullanıcı", + "newUser": "Ente'de Yeni", "delete": "Sil", "enterYourPasswordHint": "Parolanızı girin", "forgotPassword": "ƞifremi unuttum", "oops": "Hay aksi", "suggestFeatures": "Özellik önerin", "faq": "SSS", + "faq_q_1": "Kimlik doğrulayıcı ne kadar gĂŒvenli?", + "faq_a_1": "Auth aracılığıyla yedeklediğiniz tĂŒm kodlar uçtan uca ßifrelenmiß olarak saklanır. Böylece kodlarınıza yalnızca siz erißebilirsiniz. Uygulamalarımız açık kaynaklıdır ve ßifrelememiz dıß denetimden geçmißtir.", "faq_q_2": "Kodlarıma masaĂŒstĂŒnden erißebilir miyim?", "faq_a_2": "Kodlarınıza internet ĂŒzerinden @ auth.ente.io adresinden erißebilirsiniz.", "faq_q_3": "Kodları nasıl silebilirim?", "faq_a_3": "Bir kodu, o Ă¶ÄŸenin ĂŒzerinde sola kaydırarak silebilirsiniz.", "faq_q_4": "Bu projeye nasıl destek olabilirim?", "faq_a_4": "Fotoğraflar uygulamamıza @ ente.io abone olarak bu projenin gelißtirilmesine destek olabilirsiniz.", + "faq_q_5": "Auth'ta FaceID kilidini nasıl etkinleßtirebilirim", "faq_a_5": "FaceID kilidini Ayarlar → GĂŒvenlik → Kilit Ekranı altında etkinleßtirebilirsiniz.", "somethingWentWrongMessage": "Bir ßeyler ters gitti, lĂŒtfen tekrar deneyin", "leaveFamily": "Aile planından ayrıl", @@ -137,6 +145,8 @@ "enterCodeHint": "Kimlik doğrulayıcı uygulamanızdaki 6 haneli doğrulama kodunu girin", "lostDeviceTitle": "Cihazınızı mı kaybettiniz?", "twoFactorAuthTitle": "İki faktörlĂŒ kimlik doğrulama", + "passkeyAuthTitle": "Geçiß anahtarı doğrulaması", + "verifyPasskey": "Geçiß anahtarını doğrula", "recoverAccount": "Hesap kurtarma", "enterRecoveryKeyHint": "Kurtarma anahtarınızı girin", "recover": "Kurtar", @@ -148,6 +158,7 @@ } } }, + "invalidQRCode": "Geçersiz QR kodu", "noRecoveryKeyTitle": "Kurtarma anahtarınız yok mu?", "enterEmailHint": "E-posta adresinizi girin", "invalidEmailTitle": "Geçersiz e-posta adresi", @@ -190,6 +201,10 @@ "recoveryKeySaveDescription": "Biz bu anahtarı saklamıyoruz, lĂŒtfen. bu 24 kelimelik anahtarı gĂŒvenli bir yerde saklayın.", "doThisLater": "Bunu daha sonra yap", "saveKey": "Anahtarı kaydet", + "save": "Kaydet", + "send": "Gönder", + "saveOrSendDescription": "Bunu belleğinize mi kaydedeceksiniz (İndirilenler klasörĂŒ varsayılandır) yoksa diğer uygulamalara mı göndereceksiniz?", + "saveOnlyDescription": "Bunu belleğinize kaydetmek ister misiniz? (İndirilenler klasörĂŒ varsayılandır)", "back": "Geri", "createAccount": "Hesap olußtur", "passwordStrength": "ƞifre gĂŒcĂŒ: {passwordStrengthValue}", @@ -337,6 +352,7 @@ "deleteCodeAuthMessage": "Kodu silmek için doğrulama yapın", "showQRAuthMessage": "QR kodunu göstermek için doğrulama yapın", "confirmAccountDeleteTitle": "Hesap silme ißlemini onayla", + "confirmAccountDeleteMessage": "Kullandığınız Ente uygulamaları varsa bu hesap diğer Ente uygulamalarıyla bağlantılıdır.\n\nTĂŒm Ente uygulamalarına yĂŒklediğiniz veriler ve hesabınız kalıcı olarak silinecektir.", "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." @@ -396,5 +412,29 @@ "signOutOtherDevices": "Diğer cihazlardan çıkıß yap", "doNotSignOut": "Çıkıß yapma", "hearUsWhereTitle": "Ente'yi nereden duydunuz? (opsiyonel)", - "hearUsExplanation": "Biz uygulama kurulumlarını takip etmiyoruz. Bizi nereden duyduğunuzdan bahsetmeniz bize çok yardımcı olacak!" + "hearUsExplanation": "Biz uygulama kurulumlarını takip etmiyoruz. Bizi nereden duyduğunuzdan bahsetmeniz bize çok yardımcı olacak!", + "recoveryKeySaved": "Kurtarma anahtarı İndirilenler klasörĂŒne kaydedildi!", + "waitingForBrowserRequest": "Tarayıcı isteği bekleniyor...", + "waitingForVerification": "Doğrulama bekleniyor...", + "passkey": "Geçiß anahtarı", + "developerSettingsWarning": "Gelißtirici ayarlarını değißtirmekten emin misiniz?", + "developerSettings": "Gelißtirici ayarları", + "serverEndpoint": "Sunucu uç noktası", + "invalidEndpoint": "Geçersiz uç nokta", + "invalidEndpointMessage": "ÜzgĂŒnĂŒz, girdiğiniz uç nokta geçersiz. LĂŒtfen geçerli bir uç nokta girin ve tekrar deneyin.", + "endpointUpdatedMessage": "Uç nokta baßarıyla gĂŒncellendi", + "customEndpoint": "Bağlandı: {endpoint}", + "pinText": "Sabitle", + "unpinText": "Sabitlemeyi kaldır", + "pinnedCodeMessage": "{code} sabitlendi", + "unpinnedCodeMessage": "{code} sabitlemesi kaldırıldı", + "tags": "Etiketler", + "createNewTag": "Yeni etiket olußtur", + "tag": "Etiket", + "create": "Olußtur", + "editTag": "Etiketi dĂŒzenle", + "deleteTagTitle": "Etiket silinsin mi?", + "deleteTagMessage": "Bu etiketi silmek istediğinizden emin misiniz? Bu ißlem geri alınamaz.", + "somethingWentWrongParsingCode": "{x} kodu ayrıßtıramadık.", + "updateNotAvailable": "GĂŒncelleme mevcut değil" } \ No newline at end of file diff --git a/auth/lib/ui/settings/data/import/aegis_import.dart b/auth/lib/ui/settings/data/import/aegis_import.dart index f6dd872522..471ce943ce 100644 --- a/auth/lib/ui/settings/data/import/aegis_import.dart +++ b/auth/lib/ui/settings/data/import/aegis_import.dart @@ -6,6 +6,7 @@ import 'dart:typed_data'; import 'package:convert/convert.dart'; import 'package:ente_auth/l10n/l10n.dart'; import 'package:ente_auth/models/code.dart'; +import 'package:ente_auth/models/code_display.dart'; import 'package:ente_auth/services/authenticator_service.dart'; import 'package:ente_auth/store/code_store.dart'; import 'package:ente_auth/ui/common/progress_dialog.dart'; @@ -76,7 +77,7 @@ Future _pickAegisJsonFile(BuildContext context) async { await showErrorDialog( context, context.l10n.sorry, - context.l10n.importFailureDesc, + "${context.l10n.importFailureDesc}\n Error: ${e.toString()}", ); } } @@ -126,8 +127,19 @@ Future _processAegisExportFile( } else { aegisDB = decodedJson['db']; } + final Map groupIDToName = {}; + try { + for (var item in aegisDB?['groups']) { + groupIDToName[item['uuid']] = item['name']; + } + } catch (e) { + Logger("AegisImport").warning("Failed to parse groups", e); + } + final parsedCodes = []; for (var item in aegisDB?['entries']) { + bool isFavorite = item['favorite'] ?? false; + List tags = []; var kind = item['type']; var account = item['name']; var issuer = item['issuer']; @@ -137,20 +149,27 @@ Future _processAegisExportFile( var digits = item['info']['digits']; var counter = item['info']['counter']; - + for (var group in item['groups']) { + if (groupIDToName.containsKey(group)) { + tags.add(groupIDToName[group]!); + } + } // Build the OTP URL String otpUrl; - if (kind.toLowerCase() == 'totp') { + if (kind.toLowerCase() == 'totp' || kind.toLowerCase() == 'steam') { otpUrl = 'otpauth://$kind/$issuer:$account?secret=$secret&issuer=$issuer&algorithm=$algorithm&digits=$digits&period=$timer'; } else if (kind.toLowerCase() == 'hotp') { otpUrl = 'otpauth://$kind/$issuer:$account?secret=$secret&issuer=$issuer&algorithm=$algorithm&digits=$digits&counter=$counter'; } else { - throw Exception('Invalid OTP type'); + throw Exception('Invalid OTP type: $kind'); } - parsedCodes.add(Code.fromOTPAuthUrl(otpUrl)); + + Code code = Code.fromOTPAuthUrl(otpUrl); + code = code.copyWith(display: CodeDisplay(pinned: isFavorite, tags: tags)); + parsedCodes.add(code); } for (final code in parsedCodes) { diff --git a/auth/lib/ui/settings/data/import/bitwarden_import.dart b/auth/lib/ui/settings/data/import/bitwarden_import.dart index 6878fa9f05..8149a8099a 100644 --- a/auth/lib/ui/settings/data/import/bitwarden_import.dart +++ b/auth/lib/ui/settings/data/import/bitwarden_import.dart @@ -4,6 +4,7 @@ import 'dart:io'; import 'package:ente_auth/l10n/l10n.dart'; import 'package:ente_auth/models/code.dart'; +import 'package:ente_auth/models/code_display.dart'; import 'package:ente_auth/services/authenticator_service.dart'; import 'package:ente_auth/store/code_store.dart'; import 'package:ente_auth/ui/components/buttons/button_widget.dart'; @@ -14,6 +15,7 @@ import 'package:ente_auth/utils/dialog_util.dart'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import 'package:logging/logging.dart'; Future showBitwardenImportInstruction(BuildContext context) async { final l10n = context.l10n; @@ -60,12 +62,13 @@ Future _pickBitwardenJsonFile(BuildContext context) async { if (count != null) { await importSuccessDialog(context, count); } - } catch (e) { + } catch (e, s) { + Logger("BitwardenImport").severe('Failed to import', e, s); await progressDialog.hide(); await showErrorDialog( context, context.l10n.sorry, - context.l10n.importFailureDesc, + "${context.l10n.importFailureDesc}\n Error: ${e.toString()}", ); } } @@ -78,19 +81,36 @@ Future _processBitwardenExportFile( final jsonString = await file.readAsString(); final data = jsonDecode(jsonString); List jsonArray = data['items']; + final Map folderIdToName = {}; + try { + for (var item in data['folders']) { + folderIdToName[item['id']] = item['name']; + } + } catch (e) { + debugPrint("Failed to get folder details $e"); + } final parsedCodes = []; for (var item in jsonArray) { if (item['login'] != null && item['login']['totp'] != null) { var totp = item['login']['totp']; + String? folderID = item['folderId']; Code code; - if (totp.contains("otpauth://")) { code = Code.fromOTPAuthUrl(totp); + } else if (totp.contains("steam://")) { + var secret = totp.split("steam://")[1]; + code = Code.fromAccountAndSecret( + Type.steam, + item['login']['username'], + item['name'], + secret, + null, + Code.steamDigits, + ); } else { var issuer = item['name']; var account = item['login']['username']; - code = Code.fromAccountAndSecret( Type.totp, account, @@ -100,6 +120,11 @@ Future _processBitwardenExportFile( Code.defaultDigits, ); } + if (folderID != null && folderIdToName.containsKey(folderID)) { + code = code.copyWith( + display: CodeDisplay(tags: [folderIdToName[folderID]!]), + ); + } parsedCodes.add(code); } diff --git a/auth/lib/ui/settings/data/import/lastpass_import.dart b/auth/lib/ui/settings/data/import/lastpass_import.dart index 8c36f02536..550f2af7e0 100644 --- a/auth/lib/ui/settings/data/import/lastpass_import.dart +++ b/auth/lib/ui/settings/data/import/lastpass_import.dart @@ -14,6 +14,7 @@ import 'package:ente_auth/utils/dialog_util.dart'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import 'package:logging/logging.dart'; Future showLastpassImportInstruction(BuildContext context) async { final l10n = context.l10n; @@ -60,12 +61,13 @@ Future _pickLastpassJsonFile(BuildContext context) async { if (count != null) { await importSuccessDialog(context, count); } - } catch (e) { + } catch (e, s) { + Logger('LastPassImport').severe('exception while processing import', e, s); await progressDialog.hide(); await showErrorDialog( context, context.l10n.sorry, - context.l10n.importFailureDesc, + "${context.l10n.importFailureDesc}\n Error: ${e.toString()}", ); } } diff --git a/auth/lib/ui/settings/data/import/raivo_plain_text_import.dart b/auth/lib/ui/settings/data/import/raivo_plain_text_import.dart index 3590a38b37..5adaa73cf5 100644 --- a/auth/lib/ui/settings/data/import/raivo_plain_text_import.dart +++ b/auth/lib/ui/settings/data/import/raivo_plain_text_import.dart @@ -14,6 +14,7 @@ import 'package:ente_auth/utils/dialog_util.dart'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import 'package:logging/logging.dart'; Future showRaivoImportInstruction(BuildContext context) async { final l10n = context.l10n; @@ -60,12 +61,13 @@ Future _pickRaivoJsonFile(BuildContext context) async { if (count != null) { await importSuccessDialog(context, count); } - } catch (e) { + } catch (e, s) { + Logger("RaivoImport").severe('Failed to import', e, s); await progressDialog.hide(); await showErrorDialog( context, context.l10n.sorry, - context.l10n.importFailureDesc, + "${context.l10n.importFailureDesc}\n Error: ${e.toString()}", ); } } diff --git a/auth/lib/ui/settings/data/import/two_fas_import.dart b/auth/lib/ui/settings/data/import/two_fas_import.dart index 710d898d44..db33a10f35 100644 --- a/auth/lib/ui/settings/data/import/two_fas_import.dart +++ b/auth/lib/ui/settings/data/import/two_fas_import.dart @@ -72,7 +72,7 @@ Future _pick2FasFile(BuildContext context) async { await showErrorDialog( context, context.l10n.sorry, - context.l10n.importFailureDesc, + "${context.l10n.importFailureDesc}\n Error: ${e.toString()}", ); } } diff --git a/auth/lib/utils/totp_util.dart b/auth/lib/utils/totp_util.dart index 61c7f20e92..f886215081 100644 --- a/auth/lib/utils/totp_util.dart +++ b/auth/lib/utils/totp_util.dart @@ -4,7 +4,7 @@ import 'package:otp/otp.dart' as otp; import 'package:steam_totp/steam_totp.dart'; String getOTP(Code code) { - if (code.type == Type.steam) { + if (code.type == Type.steam || code.issuer.toLowerCase() == 'steam') { return _getSteamCode(code); } if (code.type == Type.hotp) { @@ -39,7 +39,7 @@ String _getSteamCode(Code code, [bool isNext = false]) { } String getNextTotp(Code code) { - if (code.type == Type.steam) { + if (code.type == Type.steam || code.issuer.toLowerCase() == 'steam') { return _getSteamCode(code, true); } return otp.OTP.generateTOTPCodeString( diff --git a/auth/pubspec.yaml b/auth/pubspec.yaml index 3a127cee31..3d70dbd2aa 100644 --- a/auth/pubspec.yaml +++ b/auth/pubspec.yaml @@ -1,6 +1,6 @@ name: ente_auth description: ente two-factor authenticator -version: 3.0.4+304 +version: 3.0.7+307 publish_to: none environment: diff --git a/cli/internal/crypto/crypto.go b/cli/internal/crypto/crypto.go index 11868c0ba6..e01ac52d1f 100644 --- a/cli/internal/crypto/crypto.go +++ b/cli/internal/crypto/crypto.go @@ -113,3 +113,23 @@ func DecryptChaChaBase64(data string, key []byte, nonce string) (string, []byte, } return base64.StdEncoding.EncodeToString(decryptedData), decryptedData, nil } + +func DecryptChaChaBase64Auth(data string, key []byte, nonce string) (string, []byte, error) { + // Decode data from base64 + dataBytes, err := base64.StdEncoding.DecodeString(data) + if err != nil { + // safe to log the encrypted data + return "", nil, fmt.Errorf("invalid base64 data %s: %v", data, err) + } + // Decode nonce from base64 + nonceBytes, err := base64.StdEncoding.DecodeString(nonce) + if err != nil { + return "", nil, fmt.Errorf("invalid nonce: %v", err) + } + // Decrypt data + decryptedData, err := decryptChaCha20poly1305V2(dataBytes, key, nonceBytes) + if err != nil { + return "", nil, fmt.Errorf("failed to decrypt data: %v", err) + } + return base64.StdEncoding.EncodeToString(decryptedData), decryptedData, nil +} diff --git a/cli/internal/crypto/crypto_libsodium.go b/cli/internal/crypto/crypto_libsodium.go index a7c193c995..81768c830b 100644 --- a/cli/internal/crypto/crypto_libsodium.go +++ b/cli/internal/crypto/crypto_libsodium.go @@ -88,6 +88,23 @@ func decryptChaCha20poly1305(data []byte, key []byte, nonce []byte) ([]byte, err return decoded, nil } +// decryptChaCha20poly1305V2 is used only to decrypt Ente Auth data. Ente Auth use new version of LibSodium. +// In that version, the final tag value is 0x0 instead of TagFinal. +func decryptChaCha20poly1305V2(data []byte, key []byte, nonce []byte) ([]byte, error) { + decryptor, err := NewDecryptor(key, nonce) + if err != nil { + return nil, err + } + decoded, tag, err := decryptor.Pull(data) + if tag != TagFinal && tag != TagMessage { + return nil, errors.New("invalid tag") + } + if err != nil { + return nil, err + } + return decoded, nil +} + //func SecretBoxOpenLibSodium(c []byte, n []byte, k []byte) ([]byte, error) { // var cp sodium.Bytes = c // res, err := cp.SecretBoxOpen(sodium.SecretBoxNonce{Bytes: n}, sodium.SecretBoxKey{Bytes: k}) diff --git a/cli/main.go b/cli/main.go index d62cdcffad..05ea3a6e27 100644 --- a/cli/main.go +++ b/cli/main.go @@ -15,7 +15,7 @@ import ( "strings" ) -var AppVersion = "0.1.13" +var AppVersion = "0.1.14" func main() { cliDBPath, err := GetCLIConfigPath() diff --git a/cli/pkg/authenticator/decrypt.go b/cli/pkg/authenticator/decrypt.go index 6ae6056c69..f841de2717 100644 --- a/cli/pkg/authenticator/decrypt.go +++ b/cli/pkg/authenticator/decrypt.go @@ -55,7 +55,7 @@ func DecryptExport(inputPath string, outputPath string) error { return fmt.Errorf("error deriving key: %v", err) } - _, decryptedData, err := eCrypto.DecryptChaChaBase64(export.EncryptedData, key, export.EncryptionNonce) + _, decryptedData, err := eCrypto.DecryptChaChaBase64Auth(export.EncryptedData, key, export.EncryptionNonce) if err != nil { fmt.Printf("\nerror decrypting data %v", err) fmt.Println("\nPlease check your password and try again") diff --git a/desktop/.github/workflows/desktop-release.yml b/desktop/.github/workflows/desktop-release.yml index 70eedf3ea6..67385b1932 100644 --- a/desktop/.github/workflows/desktop-release.yml +++ b/desktop/.github/workflows/desktop-release.yml @@ -5,12 +5,19 @@ name: "Release" # For more details, see `docs/release.md` in ente-io/ente. on: - # Trigger manually or `gh workflow run desktop-release.yml`. + # Trigger manually or `gh workflow run desktop-release.yml --source=foo`. workflow_dispatch: + inputs: + source: + description: "Branch (ente-io/ente) to build" + type: string + schedule: + # Run everyday at ~8:00 AM IST (except Sundays). + # See: [Note: Run workflow every 24 hours] + # + - cron: "45 2 * * 1-6" push: # Run when a tag matching the pattern "v*"" is pushed. - # - # See: [Note: Testing release workflows that are triggered by tags]. tags: - "v*" @@ -30,9 +37,13 @@ jobs: - name: Checkout code uses: actions/checkout@v4 with: - # Checkout the desktop/rc branch from the source repository. + # If triggered by a tag, checkout photosd-$tag from the source + # repository. Otherwise checkout $source (default: "main"). repository: ente-io/ente - ref: desktop/rc + ref: + "${{ startsWith(github.ref, 'refs/tags/v') && + format('photosd-{0}', github.ref_name) || ( inputs.source + || 'main' ) }}" submodules: recursive - name: Setup node @@ -64,10 +75,8 @@ jobs: # (No need to define this secret in the repo settings) github_token: ${{ secrets.GITHUB_TOKEN }} - # If the commit is tagged with a version (e.g. "v1.0.0"), - # create a (draft) release after building. Otherwise upload - # assets to the existing draft named after the version. - release: ${{ startsWith(github.ref, 'refs/tags/v') }} + # Passes `--publish always` to electron-builder + release: true mac_certs: ${{ secrets.MAC_CERTS }} mac_certs_password: ${{ secrets.MAC_CERTS_PASSWORD }} @@ -77,4 +86,13 @@ jobs: APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + # Default is "draft", but since our nightly builds update + # existing pre-releases, set this to "prerelease". + EP_PRE_RELEASE: true + # By default, electron-builder does not update releases that + # were more than 2 hours ago. Override this to allow us to + # continually update our nightly pre-releases. + EP_GH_IGNORE_TIME: true + # Workaround recommended in + # https://github.com/electron-userland/electron-builder/issues/3179 USE_HARD_LINKS: false diff --git a/desktop/CHANGELOG.md b/desktop/CHANGELOG.md index 5fbbefaaa8..1d535dc1a2 100644 --- a/desktop/CHANGELOG.md +++ b/desktop/CHANGELOG.md @@ -1,6 +1,11 @@ # CHANGELOG -## v1.7.0 (Unreleased) +## v1.7.1 (Unreleased) + +- Remember the window size across app restarts. +- Revert changes to the Linux icon. + +## v1.7.0 v1.7 is a major rewrite to improve the security of our app. In particular, the UI and the native parts of the app now run isolated from each other and diff --git a/desktop/build/window-icon.png b/desktop/build/window-icon.png new file mode 100644 index 0000000000..5b0458033d Binary files /dev/null and b/desktop/build/window-icon.png differ diff --git a/desktop/docs/release.md b/desktop/docs/release.md index 1cda1c11b1..7f5ebbe427 100644 --- a/desktop/docs/release.md +++ b/desktop/docs/release.md @@ -1,65 +1,94 @@ ## Releases -Conceptually, the release is straightforward: We trigger a GitHub workflow that -creates a draft release with artifacts built. When ready, we publish that -release. The download links on our website, and existing apps already check the -latest GitHub release and update accordingly. +Conceptually, the release is straightforward: -The complication comes by the fact that electron-builder's auto updaterr (the +1. We trigger a GitHub workflow that creates a (pre-)release with the build. + +2. When ready, we make that release the latest. + +3. The download links on our website, and existing apps already check the + latest GitHub release and update automatically. + +The complication comes by the fact that electron-builder's auto updater (the mechanism that we use for auto updates) doesn't work with monorepos. So we need -to keep a separate (non-mono) repository just for doing releases. +to keep a separate repository just for holding the releases. - Source code lives here, in [ente-io/ente](https://github.com/ente-io/ente). - Releases are done from [ente-io/photos-desktop](https://github.com/ente-io/photos-desktop). -## Workflow - Release Candidates +## Workflow - Release candidates -Leading up to the release, we can make one or more draft releases that are not -intended to be published, but serve as test release candidates. +Nightly RC builds of `main` are published by a scheduled workflow automatically. +If needed, these builds can also be manually updated, and the branch of the +source repository to build (default "main") also specified: -The workflow for making such "rc" builds is: +```sh +gh workflow run desktop-release.yml --source= +``` -1. Update `package.json` in the source repo to use version `1.x.x-rc`. Create a - new draft release in the release repo with title `1.x.x-rc`. In the tag - input enter `v1.x.x-rc` and select the option to "create a new tag on - publish". - -2. Push code to the `desktop/rc` branch in the source repo. - -3. Trigger the GitHub action in the release repo - - ```sh - gh workflow run desktop-release.yml - ``` - -We can do steps 2 and 3 multiple times: each time it'll just update the -artifacts attached to the same draft. +Each such workflow run will update the artifacts attached to the same +(pre-existing) pre-release. ## Workflow - Release -1. Update source repo to set version `1.x.x` in `package.json` and finialize - the CHANGELOG. +1. Update source repo to set version `1.x.x` in `package.json` and finalize the + CHANGELOG. -2. Push code to the `desktop/rc` branch in the source repo. +2. Merge PR then tag the merge commit on `main` in the source repo: -3. In the release repo + ```sh + git tag photosd-v1.x.x + git push origin photosd-v1.x.x + ``` + +3. In the release repo: ```sh ./.github/trigger-release.sh v1.x.x ``` -4. If the build is successful, tag `desktop/rc` in the source repo. +This'll trigger the workflow and create a new pre-release. We can edit this to +add the release notes, convert it to a release. Once it is marked as latest, the +release goes live. + +We are done at this point, and can now create a new pre-release to host +subsequent nightly builds. + +1. Update `package.json` in the source repo to use version `1.x.x-rc`, and + merge these changes into `main`. + +2. In the release repo: ```sh - # Assuming we're on desktop/rc that just got build - - git tag photosd-v1.x.x - git push origin photosd-v1.x.x + git tag 1.x.x-rc + git push origin 1.x.x-rc ``` -## Post build +3. Once the workflow finishes and the pre-release is created, edit its + description to "Nightly builds". + +4. Delete the pre-release for the previous (already released) version. + +## Workflow - Extra pre-releases + +To create extra one off pre-releases in addition to the nightly `1.x.x-rc` ones, + +1. In your branch in the source repository, set the version in `package.json` + to something different, say `1.x.x-my-test`. + +2. Create a new pre-release in the release repo with title `1.x.x-test`. In the + tag input enter `v1.x.x-test` and select the option to "create a new tag on + publish". + +3. Trigger the workflow in the release repo: + + ```sh + gh workflow run desktop-release.yml --source=my-branch + ``` + +## Details The GitHub Action runs on Windows, Linux and macOS. It produces the artifacts defined in the `build` value in `package.json`. @@ -87,8 +116,3 @@ everything is automated: now their maintainers automatically bump the SHA, version number and the (derived from the version) URL in the formula when their tools notice a new release on our GitHub. - -We can also publish the draft releases by checking the "pre-release" option. -Such releases don't cause any of the channels (our website, or the desktop app -auto updater, or brew) to be notified, instead these are useful for giving links -to pre-release builds to customers. diff --git a/desktop/electron-builder.yml b/desktop/electron-builder.yml index c2c000ce9f..347aabe631 100644 --- a/desktop/electron-builder.yml +++ b/desktop/electron-builder.yml @@ -23,6 +23,7 @@ linux: - target: pacman arch: [x64, arm64] category: Photography + icon: ./build/icon.icns mac: target: target: default diff --git a/desktop/eslint.config.mjs b/desktop/eslint.config.mjs new file mode 100644 index 0000000000..46e94d010d --- /dev/null +++ b/desktop/eslint.config.mjs @@ -0,0 +1,43 @@ +// @ts-check + +import js from "@eslint/js"; +import ts from "typescript-eslint"; + +export default ts.config( + js.configs.recommended, + ...ts.configs.strictTypeChecked, + ...ts.configs.stylisticTypeChecked, + { + // typescript-eslint needs this enabling type checked rules. + languageOptions: { + parserOptions: { + project: true, + tsconfigRootDir: import.meta.dirname, + }, + }, + }, + { + // The list of (minimatch) globs to ignore. This needs to be the only + // key in this configuration object. + ignores: ["eslint.config.mjs", "app/", "out/", "dist/"], + }, + { + // Rule customizations. + rules: { + // Allow numbers to be used in template literals. + "@typescript-eslint/restrict-template-expressions": [ + "error", + { + allowNumber: true, + }, + ], + // Allow void expressions as the entire body of an arrow function. + "@typescript-eslint/no-confusing-void-expression": [ + "error", + { + ignoreArrowShorthand: true, + }, + ], + }, + }, +); diff --git a/desktop/package.json b/desktop/package.json index 236dd55927..80404e5c11 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -1,6 +1,6 @@ { "name": "ente", - "version": "1.7.0-rc", + "version": "1.7.1-rc", "private": true, "description": "Desktop client for Ente Photos", "repository": "github:ente-io/photos-desktop", @@ -17,8 +17,8 @@ "dev-main": "tsc && electron .", "dev-renderer": "cd ../web && yarn install && yarn dev:photos", "postinstall": "electron-builder install-app-deps", - "lint": "yarn prettier --check --log-level warn . && eslint --ext .ts src && yarn tsc", - "lint-fix": "yarn prettier --write --log-level warn . && eslint --fix --ext .ts src && yarn tsc" + "lint": "yarn prettier --check --log-level warn . && yarn eslint && yarn tsc", + "lint-fix": "yarn prettier --write --log-level warn . && yarn eslint && yarn tsc" }, "resolutions": { "jackspeak": "2.1.1" @@ -30,29 +30,30 @@ "compare-versions": "^6.1", "electron-log": "^5.1", "electron-store": "^8.2", - "electron-updater": "^6.1", + "electron-updater": "^6.2", "ffmpeg-static": "^5.2", "html-entities": "^2.5", "jpeg-js": "^0.4", "next-electron-server": "^1", "node-stream-zip": "^1.15", - "onnxruntime-node": "^1.17" + "onnxruntime-node": "^1.18" }, "devDependencies": { + "@eslint/js": "^9.4.0", "@tsconfig/node20": "^20.1.4", "@types/auto-launch": "^5.0", + "@types/eslint__js": "^8.42.3", "@types/ffmpeg-static": "^3.0", - "@typescript-eslint/eslint-plugin": "^7", - "@typescript-eslint/parser": "^7", "concurrently": "^8", "electron": "^30", - "electron-builder": "25.0.0-alpha.6", - "eslint": "^8", + "electron-builder": "25.0.0-alpha.8", + "eslint": "^9.4.0", "prettier": "^3", "prettier-plugin-organize-imports": "^3", "prettier-plugin-packagejson": "^2", "shx": "^0.3", - "typescript": "^5" + "typescript": "^5", + "typescript-eslint": "8.0.0-alpha.10" }, "packageManager": "yarn@1.22.21", "productName": "ente" diff --git a/desktop/src/main.ts b/desktop/src/main.ts index 463774dc2b..afbf4eccd3 100644 --- a/desktop/src/main.ts +++ b/desktop/src/main.ts @@ -143,12 +143,22 @@ const registerPrivilegedSchemes = () => { * This window will show the HTML served from {@link rendererURL}. */ const createMainWindow = () => { + const icon = nativeImage.createFromPath( + path.join(isDev ? "build" : process.resourcesPath, "window-icon.png"), + ); + const bounds = windowBounds(); + // Create the main window. This'll show our web content. const window = new BrowserWindow({ webPreferences: { preload: path.join(__dirname, "preload.js"), sandbox: true, }, + icon, + // Set the window's position and size (if we have one saved). + ...(bounds ?? {}), + // Enforce a minimum size + ...minimumWindowSize(), // The color to show in the window until the web content gets loaded. // See: https://www.electronjs.org/docs/latest/api/browser-window#setting-the-backgroundcolor-property backgroundColor: "black", @@ -162,8 +172,10 @@ const createMainWindow = () => { // On macOS, also hide the dock icon on macOS. if (process.platform == "darwin") app.dock.hide(); } else { - // Show our window (maximizing it) otherwise. - window.maximize(); + // Show our window otherwise. + // + // If we did not give it an explicit size, maximize it + bounds ? window.show() : window.maximize(); } // Open the DevTools automatically when running in dev mode @@ -209,11 +221,64 @@ const createMainWindow = () => { return window; }; +/** + * The position and size to use when showing the main window. + * + * The return value is `undefined` if the app's window was maximized the last + * time around, and so if we should restore it to the maximized state. + * + * Otherwise it returns the position and size of the window the last time the + * app quit. + * + * If there is no such saved value (or if it is the first time the user is + * running the app), return a default size. + */ +const windowBounds = () => { + if (userPreferences.get("isWindowMaximized")) return undefined; + + const bounds = userPreferences.get("windowBounds"); + if (bounds) return bounds; + + // Default size. Picked arbitrarily as something that should look good on + // first launch. We don't provide a position to let Electron center the app. + return { width: 1170, height: 710 }; +}; + +/** + * If for some reason {@link windowBounds} is outside the screen's bounds (e.g. + * if the user's screen resolution has changed), then the previously saved + * bounds might not be appropriate. + * + * Luckily, if we try to set an x/y position that is outside the screen's + * bounds, then Electron automatically clamps x + width and y + height to lie + * within the screen's available space, and we do not need to tackle such out of + * bounds cases specifically. + * + * However there is no minimum window size the Electron enforces by default. As + * a safety valve, provide an (arbitrary) minimum size so that the user can + * resize it back to sanity if something I cannot currently anticipate happens. + */ +const minimumWindowSize = () => ({ minWidth: 200, minHeight: 200 }); + +/** + * Sibling of {@link windowBounds}, see that function's documentation for more + * details. + */ +const saveWindowBounds = (window: BrowserWindow) => { + if (window.isMaximized()) { + userPreferences.set("isWindowMaximized", true); + userPreferences.delete("windowBounds"); + } else { + userPreferences.delete("isWindowMaximized"); + userPreferences.set("windowBounds", window.getBounds()); + } +}; + /** * Automatically set the save path for user initiated downloads to the system's * "downloads" directory instead of asking the user to select a save location. */ -export const setDownloadPath = (webContents: WebContents) => { +const setDownloadPath = (webContents: WebContents) => { webContents.session.on("will-download", (_, item) => { item.setSavePath( uniqueSavePath(app.getPath("downloads"), item.getFilename()), @@ -241,7 +306,7 @@ const uniqueSavePath = (dirPath: string, fileName: string) => { * * @param webContents The renderer to configure. */ -export const allowExternalLinks = (webContents: WebContents) => +const allowExternalLinks = (webContents: WebContents) => // By default, if the user were open a link, say // https://github.com/ente-io/ente/discussions, then it would open a _new_ // BrowserWindow within our app. @@ -273,7 +338,7 @@ export const allowExternalLinks = (webContents: WebContents) => * "Access-Control-Allow-Origin: *" or do a echo-back of `Origin`, we add a * workaround here instead, intercepting the ACAO header and allowing `*`. */ -export const allowAllCORSOrigins = (webContents: WebContents) => +const allowAllCORSOrigins = (webContents: WebContents) => webContents.session.webRequest.onHeadersReceived( ({ responseHeaders }, callback) => { const headers: NonNullable = {}; @@ -322,6 +387,13 @@ const setupTrayItem = (mainWindow: BrowserWindow) => { * once most people have upgraded to newer versions. */ const deleteLegacyDiskCacheDirIfExists = async () => { + const removeIfExists = async (dirPath: string) => { + if (existsSync(dirPath)) { + log.info(`Removing legacy disk cache from ${dirPath}`); + await fs.rm(dirPath, { recursive: true }); + } + }; + // [Note: Getting the cache path] // // The existing code was passing "cache" as a parameter to getPath. @@ -338,9 +410,18 @@ const deleteLegacyDiskCacheDirIfExists = async () => { // // @ts-expect-error "cache" works but is not part of the public API. const cacheDir = path.join(app.getPath("cache"), "ente"); - if (existsSync(cacheDir)) { - log.info(`Removing legacy disk cache from ${cacheDir}`); - await fs.rm(cacheDir, { recursive: true }); + if (process.platform == "win32") { + // On Windows the cache dir is the same as the app data (!). So deleting + // the ente subfolder of the cache dir is equivalent to deleting the + // user data dir. + // + // Obviously, that's not good. So instead of Windows we explicitly + // delete the named cache directories. + await removeIfExists(path.join(cacheDir, "thumbs")); + await removeIfExists(path.join(cacheDir, "files")); + await removeIfExists(path.join(cacheDir, "face-crops")); + } else { + await removeIfExists(cacheDir); } }; @@ -428,7 +509,10 @@ const main = () => { // app, e.g. by clicking on its dock icon. app.on("activate", () => mainWindow?.show()); - app.on("before-quit", allowWindowClose); + app.on("before-quit", () => { + if (mainWindow) saveWindowBounds(mainWindow); + allowWindowClose(); + }); }; main(); diff --git a/desktop/src/main/services/logout.ts b/desktop/src/main/services/logout.ts index e6cb7666ca..37e73309a4 100644 --- a/desktop/src/main/services/logout.ts +++ b/desktop/src/main/services/logout.ts @@ -12,19 +12,22 @@ import { watchReset } from "./watch"; * See: [Note: Do not throw during logout]. */ export const logout = (watcher: FSWatcher) => { + const ignoreError = (label: string, e: unknown) => + log.error(`Ignoring error during logout (${label})`, e); + try { watchReset(watcher); } catch (e) { - log.error("Ignoring error during logout (FS watch)", e); + ignoreError("FS watch", e); } try { clearConvertToMP4Results(); } catch (e) { - log.error("Ignoring error during logout (convert-to-mp4)", e); + ignoreError("convert-to-mp4", e); } try { clearStores(); } catch (e) { - log.error("Ignoring error during logout (native stores)", e); + ignoreError("native stores", e); } }; diff --git a/desktop/src/main/stores/user-preferences.ts b/desktop/src/main/stores/user-preferences.ts index f3b1929892..b337b342ac 100644 --- a/desktop/src/main/stores/user-preferences.ts +++ b/desktop/src/main/stores/user-preferences.ts @@ -1,21 +1,47 @@ import Store, { Schema } from "electron-store"; interface UserPreferences { + /** + * If true, then the user has set a preference to also hide the dock icon on + * macOS whenever the app is hidden. The tray icon is always visible and can + * then be used to reopen the app when needed. + */ hideDockIcon?: boolean; skipAppVersion?: string; muteUpdateNotificationVersion?: string; + /** + * The last position and size of our app's window. + * + * This value is saved when the app is about to quit, and is used to restore + * the window to the previous state when it restarts. It is only saved if + * the app is not maximized (when the app was maximized when it was being + * quit then {@link isWindowMaximized} will be set instead). + */ + windowBounds?: { + x: number; + y: number; + width: number; + height: number; + }; + /** + * `true` if the app's main window is maximized the last time it was closed. + */ + isWindowMaximized?: boolean; } const userPreferencesSchema: Schema = { - hideDockIcon: { - type: "boolean", - }, - skipAppVersion: { - type: "string", - }, - muteUpdateNotificationVersion: { - type: "string", + hideDockIcon: { type: "boolean" }, + skipAppVersion: { type: "string" }, + muteUpdateNotificationVersion: { type: "string" }, + windowBounds: { + properties: { + x: { type: "number" }, + y: { type: "number" }, + width: { type: "number" }, + height: { type: "number" }, + }, }, + isWindowMaximized: { type: "boolean" }, }; export const userPreferences = new Store({ diff --git a/desktop/src/main/types/any-shell-escape.d.ts b/desktop/src/main/types/any-shell-escape.d.ts index b8777d2aed..4172cdb1ef 100644 --- a/desktop/src/main/types/any-shell-escape.d.ts +++ b/desktop/src/main/types/any-shell-escape.d.ts @@ -19,7 +19,6 @@ * curl -v -H "Location;" -H "User-Agent: FooBar's so-called ""Browser""" "http://www.daveeddy.com/?name=dave&age=24" Which is suitable for being executed by the shell. */ -/* eslint-disable no-unused-vars */ declare module "any-shell-escape" { declare const shellescape: (args: readonly string | string[]) => string; export default shellescape; diff --git a/desktop/src/preload.ts b/desktop/src/preload.ts index 85475031d3..c6df891dc5 100644 --- a/desktop/src/preload.ts +++ b/desktop/src/preload.ts @@ -1,4 +1,3 @@ -/* eslint-disable no-unused-vars */ /** * @file The preload script * diff --git a/desktop/src/thirdparty/clip-bpe-ts/mod.ts b/desktop/src/thirdparty/clip-bpe-ts/mod.ts index 4d00eef0e4..74126433be 100644 --- a/desktop/src/thirdparty/clip-bpe-ts/mod.ts +++ b/desktop/src/thirdparty/clip-bpe-ts/mod.ts @@ -373,7 +373,6 @@ export default class { return token + ""; } - // eslint-disable-next-line no-constant-condition while (1) { let bigram: [string, string] | null = null; let minRank = Infinity; diff --git a/desktop/tsconfig.json b/desktop/tsconfig.json index 7806cd93a7..cf0f8b8856 100644 --- a/desktop/tsconfig.json +++ b/desktop/tsconfig.json @@ -32,6 +32,6 @@ "noUncheckedIndexedAccess": true, "exactOptionalPropertyTypes": true }, - /* Transpile all `.ts` files in `src/` */ - "include": ["src/**/*.ts"] + /* Include all `.ts` files in `src/` */ + "include": ["src"] } diff --git a/desktop/yarn.lock b/desktop/yarn.lock index 2aa060efc0..78f98106ef 100644 --- a/desktop/yarn.lock +++ b/desktop/yarn.lock @@ -65,10 +65,10 @@ fs-extra "^9.0.1" promise-retry "^2.0.1" -"@electron/osx-sign@1.0.5": - version "1.0.5" - resolved "https://registry.yarnpkg.com/@electron/osx-sign/-/osx-sign-1.0.5.tgz#0af7149f2fce44d1a8215660fd25a9fb610454d8" - integrity sha512-k9ZzUQtamSoweGQDV2jILiRIHUu7lYlJ3c6IEmjv1hC17rclE+eb9U+f6UFlOOETo0JzY1HNlXy4YOlCvl+Lww== +"@electron/osx-sign@1.3.0": + version "1.3.0" + resolved "https://registry.yarnpkg.com/@electron/osx-sign/-/osx-sign-1.3.0.tgz#bd6fb60c519b76ca8a000e5801f5685690e8adad" + integrity sha512-TEXhxlYSDRr9JWK5nWdOv5MtuUdaZ412uxIIEQ0hLt80o0HYWtQJBlW5QmrQDMtebzATaOjKG9UfCzLyA90zWQ== dependencies: compare-version "^0.1.2" debug "^4.3.4" @@ -122,49 +122,54 @@ resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.10.0.tgz#548f6de556857c8bb73bbee70c35dc82a2e74d63" integrity sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA== -"@eslint/eslintrc@^2.1.4": - version "2.1.4" - resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.1.4.tgz#388a269f0f25c1b6adc317b5a2c55714894c70ad" - integrity sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ== +"@eslint/config-array@^0.15.1": + version "0.15.1" + resolved "https://registry.yarnpkg.com/@eslint/config-array/-/config-array-0.15.1.tgz#1fa78b422d98f4e7979f2211a1fde137e26c7d61" + integrity sha512-K4gzNq+yymn/EVsXYmf+SBcBro8MTf+aXJZUphM96CdzUEr+ClGDvAbpmaEK+cGVigVXIgs9gNmvHAlrzzY5JQ== + dependencies: + "@eslint/object-schema" "^2.1.3" + debug "^4.3.1" + minimatch "^3.0.5" + +"@eslint/eslintrc@^3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-3.1.0.tgz#dbd3482bfd91efa663cbe7aa1f506839868207b6" + integrity sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ== dependencies: ajv "^6.12.4" debug "^4.3.2" - espree "^9.6.0" - globals "^13.19.0" + espree "^10.0.1" + globals "^14.0.0" ignore "^5.2.0" import-fresh "^3.2.1" js-yaml "^4.1.0" minimatch "^3.1.2" strip-json-comments "^3.1.1" -"@eslint/js@8.57.0": - version "8.57.0" - resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.57.0.tgz#a5417ae8427873f1dd08b70b3574b453e67b5f7f" - integrity sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g== +"@eslint/js@9.4.0", "@eslint/js@^9.4.0": + version "9.4.0" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-9.4.0.tgz#96a2edd37ec0551ce5f9540705be23951c008a0c" + integrity sha512-fdI7VJjP3Rvc70lC4xkFXHB0fiPeojiL1PxVG6t1ZvXQrarj893PweuBTujxDUFk0Fxj4R7PIIAZ/aiiyZPZcg== + +"@eslint/object-schema@^2.1.3": + version "2.1.3" + resolved "https://registry.yarnpkg.com/@eslint/object-schema/-/object-schema-2.1.3.tgz#e65ae80ee2927b4fd8c5c26b15ecacc2b2a6cc2a" + integrity sha512-HAbhAYKfsAC2EkTqve00ibWIZlaU74Z1EHwAjYr4PXF0YU2VEA1zSIKSSpKszRLRWwHzzRZXvK632u+uXzvsvw== "@gar/promisify@^1.1.3": version "1.1.3" resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.3.tgz#555193ab2e3bb3b6adc3d551c9c030d9e860daf6" integrity sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw== -"@humanwhocodes/config-array@^0.11.14": - version "0.11.14" - resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.14.tgz#d78e481a039f7566ecc9660b4ea7fe6b1fec442b" - integrity sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg== - dependencies: - "@humanwhocodes/object-schema" "^2.0.2" - debug "^4.3.1" - minimatch "^3.0.5" - "@humanwhocodes/module-importer@^1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c" integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== -"@humanwhocodes/object-schema@^2.0.2": - version "2.0.3" - resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz#4a2868d75d6d6963e423bcf90b7fd1be343409d3" - integrity sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA== +"@humanwhocodes/retry@^0.3.0": + version "0.3.0" + resolved "https://registry.yarnpkg.com/@humanwhocodes/retry/-/retry-0.3.0.tgz#6d86b8cb322660f03d3f0aa94b99bdd8e172d570" + integrity sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew== "@isaacs/fs-minipass@^4.0.0": version "4.0.1" @@ -281,6 +286,26 @@ dependencies: "@types/ms" "*" +"@types/eslint@*": + version "8.56.10" + resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-8.56.10.tgz#eb2370a73bf04a901eeba8f22595c7ee0f7eb58d" + integrity sha512-Shavhk87gCtY2fhXDctcfS3e6FdxWkCx1iUZ9eEUbh7rTqlZT0/IzOkCOVt0fCjcFuZ9FPYfuezTBImfHCDBGQ== + dependencies: + "@types/estree" "*" + "@types/json-schema" "*" + +"@types/eslint__js@^8.42.3": + version "8.42.3" + resolved "https://registry.yarnpkg.com/@types/eslint__js/-/eslint__js-8.42.3.tgz#d1fa13e5c1be63a10b4e3afe992779f81c1179a0" + integrity sha512-alfG737uhmPdnvkrLdZLcEKJ/B8s9Y4hrZ+YAdzUeoArBlSUERA2E87ROfOaS4jd/C45fzOoZzidLc1IPwLqOw== + dependencies: + "@types/eslint" "*" + +"@types/estree@*": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4" + integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw== + "@types/ffmpeg-static@^3.0": version "3.0.3" resolved "https://registry.yarnpkg.com/@types/ffmpeg-static/-/ffmpeg-static-3.0.3.tgz#605358ac6304507a75c2fd5fd861534837b19e2f" @@ -298,7 +323,7 @@ resolved "https://registry.yarnpkg.com/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz#b979ebad3919799c979b17c72621c0bc0a31c6c4" integrity sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA== -"@types/json-schema@^7.0.15": +"@types/json-schema@*", "@types/json-schema@^7.0.15": version "7.0.15" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== @@ -316,9 +341,9 @@ integrity sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g== "@types/node@*", "@types/node@^20.9.0": - version "20.12.12" - resolved "https://registry.yarnpkg.com/@types/node/-/node-20.12.12.tgz#7cbecdf902085cec634fdb362172dfe12b8f2050" - integrity sha512-eWLDGF/FOSPtAvEqeRAQ4C8LSA7M1I7i0ky1I8U7kD1J5ITyW3AsRhQrKVoWf5pFKZ2kILsEGJhsI9r93PYnOw== + version "20.13.0" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.13.0.tgz#011a76bc1e71ae9a026dddcfd7039084f752c4b6" + integrity sha512-FM6AOb3khNkNIXPnHFDYaHerSv8uN22C91z098AnGccVu+Pcdhi+pNUFDi0iLmPIsVE0JBD0KVS7mzUYt4nRzQ== dependencies: undici-types "~5.26.4" @@ -359,16 +384,16 @@ dependencies: "@types/node" "*" -"@typescript-eslint/eslint-plugin@^7": - version "7.8.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.8.0.tgz#c78e309fe967cb4de05b85cdc876fb95f8e01b6f" - integrity sha512-gFTT+ezJmkwutUPmB0skOj3GZJtlEGnlssems4AjkVweUPGj7jRwwqg0Hhg7++kPGJqKtTYx+R05Ftww372aIg== +"@typescript-eslint/eslint-plugin@8.0.0-alpha.10": + version "8.0.0-alpha.10" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.0.0-alpha.10.tgz#a102e40da7b72a2981cb2da43064d9b3c865ca58" + integrity sha512-jsNKqn41nIS8jz5Li5xsueGEBBmRYLaflUKlclEkj8cWrO1tMK1/7xITeiVz7ZlNFZF2nop2NlXrbLtRpLEzhg== dependencies: "@eslint-community/regexpp" "^4.10.0" - "@typescript-eslint/scope-manager" "7.8.0" - "@typescript-eslint/type-utils" "7.8.0" - "@typescript-eslint/utils" "7.8.0" - "@typescript-eslint/visitor-keys" "7.8.0" + "@typescript-eslint/scope-manager" "8.0.0-alpha.10" + "@typescript-eslint/type-utils" "8.0.0-alpha.10" + "@typescript-eslint/utils" "8.0.0-alpha.10" + "@typescript-eslint/visitor-keys" "8.0.0-alpha.10" debug "^4.3.4" graphemer "^1.4.0" ignore "^5.3.1" @@ -376,47 +401,47 @@ semver "^7.6.0" ts-api-utils "^1.3.0" -"@typescript-eslint/parser@^7": - version "7.8.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-7.8.0.tgz#1e1db30c8ab832caffee5f37e677dbcb9357ddc8" - integrity sha512-KgKQly1pv0l4ltcftP59uQZCi4HUYswCLbTqVZEJu7uLX8CTLyswqMLqLN+2QFz4jCptqWVV4SB7vdxcH2+0kQ== +"@typescript-eslint/parser@8.0.0-alpha.10": + version "8.0.0-alpha.10" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.0.0-alpha.10.tgz#fbefd39da010d65407b985f2b5c6e0a79bc8a6f4" + integrity sha512-4EerPviLfBKgExHARehJgWrCtX2a7+PXBc0LBPlH93ypSgj0LU1ejMgjrB0gcfd6bJ7LN/UGNAAy3B7/Y785sA== dependencies: - "@typescript-eslint/scope-manager" "7.8.0" - "@typescript-eslint/types" "7.8.0" - "@typescript-eslint/typescript-estree" "7.8.0" - "@typescript-eslint/visitor-keys" "7.8.0" + "@typescript-eslint/scope-manager" "8.0.0-alpha.10" + "@typescript-eslint/types" "8.0.0-alpha.10" + "@typescript-eslint/typescript-estree" "8.0.0-alpha.10" + "@typescript-eslint/visitor-keys" "8.0.0-alpha.10" debug "^4.3.4" -"@typescript-eslint/scope-manager@7.8.0": - version "7.8.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-7.8.0.tgz#bb19096d11ec6b87fb6640d921df19b813e02047" - integrity sha512-viEmZ1LmwsGcnr85gIq+FCYI7nO90DVbE37/ll51hjv9aG+YZMb4WDE2fyWpUR4O/UrhGRpYXK/XajcGTk2B8g== +"@typescript-eslint/scope-manager@8.0.0-alpha.10": + version "8.0.0-alpha.10" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.0.0-alpha.10.tgz#25506ce51ab64e99f2bc0b7d3f0f82656e14a794" + integrity sha512-SUU0yhqehjuWilWRJWfhcxf6eMKVrZ3bpV2w6NF6GmBHR3FJo6oWZYLVXP04s6//INxpC2ynvKSglo4LRzWVTw== dependencies: - "@typescript-eslint/types" "7.8.0" - "@typescript-eslint/visitor-keys" "7.8.0" + "@typescript-eslint/types" "8.0.0-alpha.10" + "@typescript-eslint/visitor-keys" "8.0.0-alpha.10" -"@typescript-eslint/type-utils@7.8.0": - version "7.8.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-7.8.0.tgz#9de166f182a6e4d1c5da76e94880e91831e3e26f" - integrity sha512-H70R3AefQDQpz9mGv13Uhi121FNMh+WEaRqcXTX09YEDky21km4dV1ZXJIp8QjXc4ZaVkXVdohvWDzbnbHDS+A== +"@typescript-eslint/type-utils@8.0.0-alpha.10": + version "8.0.0-alpha.10" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.0.0-alpha.10.tgz#d27f0fdd81450380887b3a07297440ba3588a70e" + integrity sha512-6aTcbnDZWKgKr3gquECJSFyvXWLSKtUHrk2ZXDP4DEzmzTDjrkY7tIQpqv4SczPQJ+3/aky3ArPhtnQYJbAMzg== dependencies: - "@typescript-eslint/typescript-estree" "7.8.0" - "@typescript-eslint/utils" "7.8.0" + "@typescript-eslint/typescript-estree" "8.0.0-alpha.10" + "@typescript-eslint/utils" "8.0.0-alpha.10" debug "^4.3.4" ts-api-utils "^1.3.0" -"@typescript-eslint/types@7.8.0": - version "7.8.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-7.8.0.tgz#1fd2577b3ad883b769546e2d1ef379f929a7091d" - integrity sha512-wf0peJ+ZGlcH+2ZS23aJbOv+ztjeeP8uQ9GgwMJGVLx/Nj9CJt17GWgWWoSmoRVKAX2X+7fzEnAjxdvK2gqCLw== +"@typescript-eslint/types@8.0.0-alpha.10": + version "8.0.0-alpha.10" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.0.0-alpha.10.tgz#89be400c6a1751fe86f5917ed8087ec100e002da" + integrity sha512-prbN+b/I4yH6H43WmyenMz8K5e34Hs73BQuWXR4wwij3Cg2xNGLPcpjr2cKWKlH4dZQPTz6R6oBeC+LfaoKi8g== -"@typescript-eslint/typescript-estree@7.8.0": - version "7.8.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-7.8.0.tgz#b028a9226860b66e623c1ee55cc2464b95d2987c" - integrity sha512-5pfUCOwK5yjPaJQNy44prjCwtr981dO8Qo9J9PwYXZ0MosgAbfEMB008dJ5sNo3+/BN6ytBPuSvXUg9SAqB0dg== +"@typescript-eslint/typescript-estree@8.0.0-alpha.10": + version "8.0.0-alpha.10" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.0.0-alpha.10.tgz#e850056d2a5029688269a60206dec3bbd7beb953" + integrity sha512-8wBUIhu6IRa440hv5/0ZEnb5JLp/UsfzIXYKRwICUOMTVj2ss1n+w3m1CtT5ghVWy5Z05qkscsbhlKFmZguU8w== dependencies: - "@typescript-eslint/types" "7.8.0" - "@typescript-eslint/visitor-keys" "7.8.0" + "@typescript-eslint/types" "8.0.0-alpha.10" + "@typescript-eslint/visitor-keys" "8.0.0-alpha.10" debug "^4.3.4" globby "^11.1.0" is-glob "^4.0.3" @@ -424,32 +449,27 @@ semver "^7.6.0" ts-api-utils "^1.3.0" -"@typescript-eslint/utils@7.8.0": - version "7.8.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-7.8.0.tgz#57a79f9c0c0740ead2f622e444cfaeeb9fd047cd" - integrity sha512-L0yFqOCflVqXxiZyXrDr80lnahQfSOfc9ELAAZ75sqicqp2i36kEZZGuUymHNFoYOqxRT05up760b4iGsl02nQ== +"@typescript-eslint/utils@8.0.0-alpha.10": + version "8.0.0-alpha.10" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.0.0-alpha.10.tgz#b77f743227353bfa493e95409c0e079044c9258e" + integrity sha512-WZyNf49CuvaW/whz/B8XjYwXE/wm/EQAXq+Vqgp6BrJb8SC3bMCwGuUxReNDN1o+dNdOC96ofVSvqa8NUQ65Jg== dependencies: "@eslint-community/eslint-utils" "^4.4.0" "@types/json-schema" "^7.0.15" "@types/semver" "^7.5.8" - "@typescript-eslint/scope-manager" "7.8.0" - "@typescript-eslint/types" "7.8.0" - "@typescript-eslint/typescript-estree" "7.8.0" + "@typescript-eslint/scope-manager" "8.0.0-alpha.10" + "@typescript-eslint/types" "8.0.0-alpha.10" + "@typescript-eslint/typescript-estree" "8.0.0-alpha.10" semver "^7.6.0" -"@typescript-eslint/visitor-keys@7.8.0": - version "7.8.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-7.8.0.tgz#7285aab991da8bee411a42edbd5db760d22fdd91" - integrity sha512-q4/gibTNBQNA0lGyYQCmWRS5D15n8rXh4QjK3KV+MBPlTYHpfBUT3D3PaPR/HeNiI9W6R7FvlkcGhNyAoP+caA== +"@typescript-eslint/visitor-keys@8.0.0-alpha.10": + version "8.0.0-alpha.10" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.0.0-alpha.10.tgz#d0a9250c69cc2f73c7f423c36183d222a329e260" + integrity sha512-UohTNnT7S29uQgXsGZY489nWmoBBSJucNdRvog62R1QX9pQQb2pKVV1kHepUxoY2vd+M4tb9SQwZQ3gPNgqQ6w== dependencies: - "@typescript-eslint/types" "7.8.0" + "@typescript-eslint/types" "8.0.0-alpha.10" eslint-visitor-keys "^3.4.3" -"@ungap/structured-clone@^1.2.0": - version "1.2.0" - resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406" - integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== - "@xmldom/xmldom@^0.8.8": version "0.8.10" resolved "https://registry.yarnpkg.com/@xmldom/xmldom/-/xmldom-0.8.10.tgz#a1337ca426aa61cef9fe15b5b28e340a72f6fa99" @@ -465,7 +485,7 @@ acorn-jsx@^5.3.2: resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== -acorn@^8.9.0: +acorn@^8.11.3: version "8.11.3" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.3.tgz#71e0b14e13a4ec160724b38fb7b0f233b1b81d7a" integrity sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg== @@ -554,14 +574,14 @@ app-builder-bin@4.0.0: resolved "https://registry.yarnpkg.com/app-builder-bin/-/app-builder-bin-4.0.0.tgz#1df8e654bd1395e4a319d82545c98667d7eed2f0" integrity sha512-xwdG0FJPQMe0M0UA4Tz0zEB8rBJTRA5a476ZawAqiBkMv16GRK5xpXThOjMaEOFnZ6zabejjG4J3da0SXG63KA== -app-builder-lib@25.0.0-alpha.6: - version "25.0.0-alpha.6" - resolved "https://registry.yarnpkg.com/app-builder-lib/-/app-builder-lib-25.0.0-alpha.6.tgz#3edb49843b249a1dd52b32a80f9787677bc5a32b" - integrity sha512-kXveR7MFTJXBwb2xB2geKWeWP+YGcJ3IzWRgTEV96zwyo4IxzE5xRXcndSQQglmlzw/VoM5Mx322E9ErYbMCVg== +app-builder-lib@25.0.0-alpha.8: + version "25.0.0-alpha.8" + resolved "https://registry.yarnpkg.com/app-builder-lib/-/app-builder-lib-25.0.0-alpha.8.tgz#e8065005b0b5d43f22153ac72101f71bf4e1022b" + integrity sha512-d/pcaTcDv3gfdl9AGGP/DKvc+A+TdJmG15f+vqPeEGKOoqLE0ukReaEevXAtH3wOOs5CqgX6QgNPdszeeqFn3Q== dependencies: "@develar/schema-utils" "~2.6.5" "@electron/notarize" "2.3.0" - "@electron/osx-sign" "1.0.5" + "@electron/osx-sign" "1.3.0" "@electron/rebuild" "3.6.0" "@electron/universal" "2.0.1" "@malept/flatpak-bundler" "^0.4.0" @@ -573,7 +593,7 @@ app-builder-lib@25.0.0-alpha.6: chromium-pickle-js "^0.2.0" debug "^4.3.4" ejs "^3.1.8" - electron-publish "25.0.0-alpha.6" + electron-publish "25.0.0-alpha.7" form-data "^4.0.0" fs-extra "^10.1.0" hosted-git-info "^4.1.0" @@ -582,7 +602,7 @@ app-builder-lib@25.0.0-alpha.6: js-yaml "^4.1.0" lazy-val "^1.0.5" minimatch "^5.1.1" - read-config-file "6.3.2" + read-config-file "6.4.0" sanitize-filename "^1.6.3" semver "^7.3.8" tar "^6.1.12" @@ -718,7 +738,14 @@ brace-expansion@^2.0.1: dependencies: balanced-match "^1.0.0" -braces@^3.0.2, braces@~3.0.2: +braces@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" + integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== + dependencies: + fill-range "^7.1.1" + +braces@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== @@ -743,10 +770,10 @@ buffer@^5.1.0, buffer@^5.5.0: base64-js "^1.3.1" ieee754 "^1.1.13" -builder-util-runtime@9.2.3: - version "9.2.3" - resolved "https://registry.yarnpkg.com/builder-util-runtime/-/builder-util-runtime-9.2.3.tgz#0a82c7aca8eadef46d67b353c638f052c206b83c" - integrity sha512-FGhkqXdFFZ5dNC4C+yuQB9ak311rpGAw+/ASz8ZdxwODCv1GGMWgLDeofRkdi0F3VCHQEWy/aXcJQozx2nOPiw== +builder-util-runtime@9.2.4: + version "9.2.4" + resolved "https://registry.yarnpkg.com/builder-util-runtime/-/builder-util-runtime-9.2.4.tgz#13cd1763da621e53458739a1e63f7fcba673c42a" + integrity sha512-upp+biKpN/XZMLim7aguUyW8s0FUpDvOtK6sbanMFDAMBzpHDqdhgVYm6zc9HJ6nWo7u2Lxk60i2M6Jd3aiNrA== dependencies: debug "^4.3.4" sax "^1.2.4" @@ -1007,6 +1034,14 @@ conf@^10.2.0: pkg-up "^3.1.0" semver "^7.3.5" +config-file-ts@0.2.8-rc1: + version "0.2.8-rc1" + resolved "https://registry.yarnpkg.com/config-file-ts/-/config-file-ts-0.2.8-rc1.tgz#fb7fc6ccb2e313f69dbeb78f1db0b00038049de0" + integrity sha512-GtNECbVI82bT4RiDIzBSVuTKoSHufnU7Ce7/42bkWZJZFLjmDF2WBpVsvRkhKCfKBnTBb3qZrBwPpFBU/Myvhg== + dependencies: + glob "^10.3.12" + typescript "^5.4.3" + config-file-ts@^0.2.4: version "0.2.6" resolved "https://registry.yarnpkg.com/config-file-ts/-/config-file-ts-0.2.6.tgz#b424ff74612fb37f626d6528f08f92ddf5d22027" @@ -1055,13 +1090,20 @@ debounce-fn@^4.0.0: dependencies: mimic-fn "^3.0.0" -debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4: +debug@4, debug@^4.3.1, debug@^4.3.2, debug@^4.3.3: version "4.3.4" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== dependencies: ms "2.1.2" +debug@^4.1.0, debug@^4.1.1, debug@^4.3.4: + version "4.3.5" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.5.tgz#e83444eceb9fedd4a1da56d671ae2446a01a6e1e" + integrity sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg== + dependencies: + ms "2.1.2" + decompress-response@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-6.0.0.tgz#ca387612ddb7e104bd16d85aab00d5ecf09c66fc" @@ -1149,12 +1191,12 @@ dir-glob@^3.0.1: dependencies: path-type "^4.0.0" -dmg-builder@25.0.0-alpha.6: - version "25.0.0-alpha.6" - resolved "https://registry.yarnpkg.com/dmg-builder/-/dmg-builder-25.0.0-alpha.6.tgz#1a13008de0543c3080595534ab294cde2a5e57e8" - integrity sha512-GStVExwsuumGN6rPGJksA5bLN5n5QEQd5iQrGKyBSxuwR1+LWidFkM+anroXnANIyTwbppk2S7+808vHjT/Eyw== +dmg-builder@25.0.0-alpha.8: + version "25.0.0-alpha.8" + resolved "https://registry.yarnpkg.com/dmg-builder/-/dmg-builder-25.0.0-alpha.8.tgz#fe887023ffc9ce72dfd2472303a76ec008a156d2" + integrity sha512-1/Sfl1sQugHkHEobFafyx1HcmgkFj8pV7HFEK0NQ8bH5K2qsGvknjAeHjtYhV2sBoSNGod4P0SfbScS6p6h4eg== dependencies: - app-builder-lib "25.0.0-alpha.6" + app-builder-lib "25.0.0-alpha.8" builder-util "25.0.0-alpha.6" builder-util-runtime "9.2.5-alpha.2" fs-extra "^10.1.0" @@ -1177,13 +1219,6 @@ dmg-license@^1.0.11: smart-buffer "^4.0.2" verror "^1.10.0" -doctrine@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" - integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== - dependencies: - esutils "^2.0.2" - dot-prop@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-6.0.1.tgz#fc26b3cf142b9e59b74dbd39ed66ce620c681083" @@ -1191,11 +1226,23 @@ dot-prop@^6.0.1: dependencies: is-obj "^2.0.0" +dotenv-expand@^11.0.6: + version "11.0.6" + resolved "https://registry.yarnpkg.com/dotenv-expand/-/dotenv-expand-11.0.6.tgz#f2c840fd924d7c77a94eff98f153331d876882d3" + integrity sha512-8NHi73otpWsZGBSZwwknTXS5pqMOrk9+Ssrna8xCaxkzEpU9OTf9R5ArQGVw03//Zmk9MOwLPng9WwndvpAJ5g== + dependencies: + dotenv "^16.4.4" + dotenv-expand@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/dotenv-expand/-/dotenv-expand-5.1.0.tgz#3fbaf020bfd794884072ea26b1e9791d45a629f0" integrity sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA== +dotenv@^16.4.4, dotenv@^16.4.5: + version "16.4.5" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.5.tgz#cdd3b3b604cb327e286b4762e13502f717cb099f" + integrity sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg== + dotenv@^9.0.2: version "9.0.2" resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-9.0.2.tgz#dacc20160935a37dea6364aa1bef819fb9b6ab05" @@ -1208,16 +1255,16 @@ ejs@^3.1.8: dependencies: jake "^10.8.5" -electron-builder@25.0.0-alpha.6: - version "25.0.0-alpha.6" - resolved "https://registry.yarnpkg.com/electron-builder/-/electron-builder-25.0.0-alpha.6.tgz#a72f96f7029539ac28f92ce5c83f872ba3b6e7c1" - integrity sha512-qXzzdID2W9hhx3TXddwXv1C5HsqjF6bKnftUtywAB/gtDwu+neifPZvnXDNHI4ZamRrZpJJH59esfkqkc2KNSQ== +electron-builder@25.0.0-alpha.8: + version "25.0.0-alpha.8" + resolved "https://registry.yarnpkg.com/electron-builder/-/electron-builder-25.0.0-alpha.8.tgz#7238623cf7a753d0da31f16daea8767d0ef6d572" + integrity sha512-nfrtTljEZackbhJE1BcK+RFXrPvrkrBo0TgR0gH2GxNhPiRTwj/S24K3zHbYj6vBaDVtnqlS6Mqm8tMUrRU4tA== dependencies: - app-builder-lib "25.0.0-alpha.6" + app-builder-lib "25.0.0-alpha.8" builder-util "25.0.0-alpha.6" builder-util-runtime "9.2.5-alpha.2" chalk "^4.1.2" - dmg-builder "25.0.0-alpha.6" + dmg-builder "25.0.0-alpha.8" fs-extra "^10.1.0" is-ci "^3.0.0" lazy-val "^1.0.5" @@ -1226,14 +1273,14 @@ electron-builder@25.0.0-alpha.6: yargs "^17.6.2" electron-log@^5.1: - version "5.1.2" - resolved "https://registry.yarnpkg.com/electron-log/-/electron-log-5.1.2.tgz#fb40ad7f4ae694dd0e4c02c662d1a65c03e1243e" - integrity sha512-Cpg4hAZ27yM9wzE77c4TvgzxzavZ+dVltCczParXN+Vb3jocojCSAuSMCVOI9fhFuuOR+iuu3tZLX1cu0y0kgQ== + version "5.1.5" + resolved "https://registry.yarnpkg.com/electron-log/-/electron-log-5.1.5.tgz#70d5051fc5ab7669b2592f53f72034867269c96e" + integrity sha512-vuq10faUAxRbILgQx7yHoMObKZDEfj7hMSZrJPsVrDNeCpV/HN11dU7QuY4UDUe055pzBxhSCB3m0+6D3Aktjw== -electron-publish@25.0.0-alpha.6: - version "25.0.0-alpha.6" - resolved "https://registry.yarnpkg.com/electron-publish/-/electron-publish-25.0.0-alpha.6.tgz#8af3cb6e2435c00b8c71de43c330483808df5924" - integrity sha512-Hin+6j+jiXBc5g6Wlv9JB5Xu7MADBHxZAndt4WE7luCw7b3+OJdQeDvD/uYiCLpiG8cc2NLxu4MyBSVu86MrJA== +electron-publish@25.0.0-alpha.7: + version "25.0.0-alpha.7" + resolved "https://registry.yarnpkg.com/electron-publish/-/electron-publish-25.0.0-alpha.7.tgz#3c1944c8890b22d5f674772bf16c5382da248861" + integrity sha512-d9R6Jnds3PjzBM4Wt3nRn9ramkbM3kBzt9a6WHQL4/09ByyZGZ1Cu9GS9atRCkH3tBJlOIotUYVhVO36lk3sAA== dependencies: "@types/fs-extra" "^9.0.11" builder-util "25.0.0-alpha.6" @@ -1251,12 +1298,12 @@ electron-store@^8.2: conf "^10.2.0" type-fest "^2.17.0" -electron-updater@^6.1: - version "6.1.8" - resolved "https://registry.yarnpkg.com/electron-updater/-/electron-updater-6.1.8.tgz#17637bca165322f4e526b13c99165f43e6f697d8" - integrity sha512-hhOTfaFAd6wRHAfUaBhnAOYc+ymSGCWJLtFkw4xJqOvtpHmIdNHnXDV9m1MHC+A6q08Abx4Ykgyz/R5DGKNAMQ== +electron-updater@^6.2: + version "6.2.1" + resolved "https://registry.yarnpkg.com/electron-updater/-/electron-updater-6.2.1.tgz#1c9adb9ba2a21a5dc50a8c434c45360d5e9fe6c9" + integrity sha512-83eKIPW14qwZqUUM6wdsIRwVKZyjmHxQ4/8G+1C6iS5PdDt7b1umYQyj1/qPpH510GmHEQe4q0kCPe3qmb3a0Q== dependencies: - builder-util-runtime "9.2.3" + builder-util-runtime "9.2.4" fs-extra "^10.1.0" js-yaml "^4.1.0" lazy-val "^1.0.5" @@ -1266,9 +1313,9 @@ electron-updater@^6.1: tiny-typed-emitter "^2.1.0" electron@^30: - version "30.0.6" - resolved "https://registry.yarnpkg.com/electron/-/electron-30.0.6.tgz#9ddea5f68396ecca88ad7c2c466a30fc9c16144b" - integrity sha512-PkhEPFdpYcTzjAO3gMHZ+map7g2+xCrMDedo/L1i0ir2BRXvAB93IkTJX497U6Srb/09r2cFt+k20VPNVCdw3Q== + version "30.0.9" + resolved "https://registry.yarnpkg.com/electron/-/electron-30.0.9.tgz#b11400e4642a4b635e79244ba365f1d401ee60b5" + integrity sha512-ArxgdGHVu3o5uaP+Tqj8cJDvU03R6vrGrOqiMs7JXLnvQHMqXJIIxmFKQAIdJW8VoT3ac3hD21tA7cPO10RLow== dependencies: "@electron/get" "^2.0.0" "@types/node" "^20.9.0" @@ -1330,54 +1377,55 @@ escape-string-regexp@^4.0.0: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== -eslint-scope@^7.2.2: - version "7.2.2" - resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.2.2.tgz#deb4f92563390f32006894af62a22dba1c46423f" - integrity sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg== +eslint-scope@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-8.0.1.tgz#a9601e4b81a0b9171657c343fb13111688963cfc" + integrity sha512-pL8XjgP4ZOmmwfFE8mEhSxA7ZY4C+LWyqjQ3o4yWkkmD0qcMT9kkW3zWHOczhWcjTSgqycYAgwSlXvZltv65og== dependencies: esrecurse "^4.3.0" estraverse "^5.2.0" -eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4.3: +eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.3: version "3.4.3" resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800" integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== -eslint@^8: - version "8.57.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.57.0.tgz#c786a6fd0e0b68941aaf624596fb987089195668" - integrity sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ== +eslint-visitor-keys@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz#e3adc021aa038a2a8e0b2f8b0ce8f66b9483b1fb" + integrity sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw== + +eslint@^9.4.0: + version "9.4.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-9.4.0.tgz#79150c3610ae606eb131f1d648d5f43b3d45f3cd" + integrity sha512-sjc7Y8cUD1IlwYcTS9qPSvGjAC8Ne9LctpxKKu3x/1IC9bnOg98Zy6GxEJUfr1NojMgVPlyANXYns8oE2c1TAA== dependencies: "@eslint-community/eslint-utils" "^4.2.0" "@eslint-community/regexpp" "^4.6.1" - "@eslint/eslintrc" "^2.1.4" - "@eslint/js" "8.57.0" - "@humanwhocodes/config-array" "^0.11.14" + "@eslint/config-array" "^0.15.1" + "@eslint/eslintrc" "^3.1.0" + "@eslint/js" "9.4.0" "@humanwhocodes/module-importer" "^1.0.1" + "@humanwhocodes/retry" "^0.3.0" "@nodelib/fs.walk" "^1.2.8" - "@ungap/structured-clone" "^1.2.0" ajv "^6.12.4" chalk "^4.0.0" cross-spawn "^7.0.2" debug "^4.3.2" - doctrine "^3.0.0" escape-string-regexp "^4.0.0" - eslint-scope "^7.2.2" - eslint-visitor-keys "^3.4.3" - espree "^9.6.1" + eslint-scope "^8.0.1" + eslint-visitor-keys "^4.0.0" + espree "^10.0.1" esquery "^1.4.2" esutils "^2.0.2" fast-deep-equal "^3.1.3" - file-entry-cache "^6.0.1" + file-entry-cache "^8.0.0" find-up "^5.0.0" glob-parent "^6.0.2" - globals "^13.19.0" - graphemer "^1.4.0" ignore "^5.2.0" imurmurhash "^0.1.4" is-glob "^4.0.0" is-path-inside "^3.0.3" - js-yaml "^4.1.0" json-stable-stringify-without-jsonify "^1.0.1" levn "^0.4.1" lodash.merge "^4.6.2" @@ -1387,14 +1435,14 @@ eslint@^8: strip-ansi "^6.0.1" text-table "^0.2.0" -espree@^9.6.0, espree@^9.6.1: - version "9.6.1" - resolved "https://registry.yarnpkg.com/espree/-/espree-9.6.1.tgz#a2a17b8e434690a5432f2f8018ce71d331a48c6f" - integrity sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ== +espree@^10.0.1: + version "10.0.1" + resolved "https://registry.yarnpkg.com/espree/-/espree-10.0.1.tgz#600e60404157412751ba4a6f3a2ee1a42433139f" + integrity sha512-MWkrWZbJsL2UwnjxTX3gG8FneachS/Mwg7tdGXce011sJd5b0JG54vat5KHnfSBODZ3Wvzd2WnjxyzsRoVv+ww== dependencies: - acorn "^8.9.0" + acorn "^8.11.3" acorn-jsx "^5.3.2" - eslint-visitor-keys "^3.4.1" + eslint-visitor-keys "^4.0.0" esquery@^1.4.2: version "1.5.0" @@ -1491,12 +1539,12 @@ ffmpeg-static@^5.2: https-proxy-agent "^5.0.0" progress "^2.0.3" -file-entry-cache@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" - integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg== +file-entry-cache@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-8.0.0.tgz#7787bddcf1131bffb92636c69457bbc0edd6d81f" + integrity sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ== dependencies: - flat-cache "^3.0.4" + flat-cache "^4.0.0" filelist@^1.0.4: version "1.0.4" @@ -1505,10 +1553,10 @@ filelist@^1.0.4: dependencies: minimatch "^5.0.1" -fill-range@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" - integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== +fill-range@^7.0.1, fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== dependencies: to-regex-range "^5.0.1" @@ -1527,14 +1575,13 @@ find-up@^5.0.0: locate-path "^6.0.0" path-exists "^4.0.0" -flat-cache@^3.0.4: - version "3.2.0" - resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.2.0.tgz#2c0c2d5040c99b1632771a9d105725c0115363ee" - integrity sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw== +flat-cache@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-4.0.1.tgz#0ece39fcb14ee012f4b0410bd33dd9c1f011127c" + integrity sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw== dependencies: flatted "^3.2.9" - keyv "^4.5.3" - rimraf "^3.0.2" + keyv "^4.5.4" flatted@^3.2.9: version "3.3.1" @@ -1678,7 +1725,7 @@ glob-parent@^6.0.2: dependencies: is-glob "^4.0.3" -glob@^10.3.10, glob@^10.3.7: +glob@^10.3.10: version "10.3.12" resolved "https://registry.yarnpkg.com/glob/-/glob-10.3.12.tgz#3a65c363c2e9998d220338e88a5f6ac97302960b" integrity sha512-TCNv8vJ+xz4QiqTpfOJA7HvYv+tNIRHKfUWw/q+v2jdgN4ebz+KY9tGx5J4rHP0o84mNP+ApH66HRX8us3Khqg== @@ -1689,6 +1736,17 @@ glob@^10.3.10, glob@^10.3.7: minipass "^7.0.4" path-scurry "^1.10.2" +glob@^10.3.12, glob@^10.3.7: + version "10.4.1" + resolved "https://registry.yarnpkg.com/glob/-/glob-10.4.1.tgz#0cfb01ab6a6b438177bfe6a58e2576f6efe909c2" + integrity sha512-2jelhlq3E4ho74ZyVLN03oKdAZVUa6UDZzFLVH1H7dnoax+y9qyaq8zBkfDIggjniU19z0wU18y16jMB2eyVIw== + dependencies: + foreground-child "^3.1.0" + jackspeak "^3.1.2" + minimatch "^9.0.4" + minipass "^7.1.2" + path-scurry "^1.11.1" + glob@^7.0.0, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6: version "7.2.3" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" @@ -1724,12 +1782,10 @@ global-agent@^3.0.0: semver "^7.3.2" serialize-error "^7.0.1" -globals@^13.19.0: - version "13.24.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-13.24.0.tgz#8432a19d78ce0c1e833949c36adb345400bb1171" - integrity sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ== - dependencies: - type-fest "^0.20.2" +globals@^14.0.0: + version "14.0.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-14.0.0.tgz#898d7413c29babcf6bafe56fcadded858ada724e" + integrity sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ== globalthis@^1.0.1: version "1.0.4" @@ -2048,7 +2104,7 @@ isexe@^2.0.0: resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== -jackspeak@2.1.1, jackspeak@^2.3.6: +jackspeak@2.1.1, jackspeak@^2.3.6, jackspeak@^3.1.2: version "2.1.1" resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-2.1.1.tgz#2a42db4cfbb7e55433c28b6f75d8b796af9669cd" integrity sha512-juf9stUEwUaILepraGOWIJTLwg48bUnBmRqd2ln2Os1sW987zeoj/hzhbvRB95oMuS2ZTpjULmdwHNX4rzZIZw== @@ -2114,7 +2170,7 @@ json-stringify-safe@^5.0.1: resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" integrity sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA== -json5@^2.2.0: +json5@^2.2.0, json5@^2.2.3: version "2.2.3" resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== @@ -2135,7 +2191,7 @@ jsonfile@^6.0.1: optionalDependencies: graceful-fs "^4.1.6" -keyv@^4.0.0, keyv@^4.5.3: +keyv@^4.0.0, keyv@^4.5.4: version "4.5.4" resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw== @@ -2255,11 +2311,11 @@ merge2@^1.3.0, merge2@^1.4.1: integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== micromatch@^4.0.4: - version "4.0.5" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" - integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== + version "4.0.7" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.7.tgz#33e8190d9fe474a9895525f5618eee136d46c2e5" + integrity sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q== dependencies: - braces "^3.0.2" + braces "^3.0.3" picomatch "^2.3.1" mime-db@1.52.0: @@ -2376,10 +2432,10 @@ minipass@^5.0.0: resolved "https://registry.yarnpkg.com/minipass/-/minipass-5.0.0.tgz#3e9788ffb90b694a5d0ec94479a45b5d8738133d" integrity sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ== -"minipass@^5.0.0 || ^6.0.2 || ^7.0.0", minipass@^7.0.4: - version "7.0.4" - resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.0.4.tgz#dbce03740f50a4786ba994c1fb908844d27b038c" - integrity sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ== +"minipass@^5.0.0 || ^6.0.2 || ^7.0.0", minipass@^7.0.4, minipass@^7.1.0, minipass@^7.1.2: + version "7.1.2" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707" + integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw== minizlib@^2.1.1, minizlib@^2.1.2: version "2.1.2" @@ -2526,17 +2582,17 @@ onetime@^5.1.0, onetime@^5.1.2: dependencies: mimic-fn "^2.1.0" -onnxruntime-common@1.17.3: - version "1.17.3" - resolved "https://registry.yarnpkg.com/onnxruntime-common/-/onnxruntime-common-1.17.3.tgz#aadc456477873a540ee3d611ae9cd4f3de7c43e5" - integrity sha512-IkbaDelNVX8cBfHFgsNADRIq2TlXMFWW+nG55mwWvQT4i0NZb32Jf35Pf6h9yjrnK78RjcnlNYaI37w394ovMw== +onnxruntime-common@1.18.0: + version "1.18.0" + resolved "https://registry.yarnpkg.com/onnxruntime-common/-/onnxruntime-common-1.18.0.tgz#b904dc6ff134e7f21a3eab702fac17538f59e116" + integrity sha512-lufrSzX6QdKrktAELG5x5VkBpapbCeS3dQwrXbN0eD9rHvU0yAWl7Ztju9FvgAKWvwd/teEKJNj3OwM6eTZh3Q== -onnxruntime-node@^1.17: - version "1.17.3" - resolved "https://registry.yarnpkg.com/onnxruntime-node/-/onnxruntime-node-1.17.3.tgz#53b8b7ef68bf3834bba9d7be592e4c2d718d2018" - integrity sha512-NtbN1pfApTSEjVq46LrJ396aPP2Gjhy+oYZi5Bu1leDXAEvVap/BQ8CZELiLs7z0UnXy3xjJW23HiB4P3//FIw== +onnxruntime-node@^1.18: + version "1.18.0" + resolved "https://registry.yarnpkg.com/onnxruntime-node/-/onnxruntime-node-1.18.0.tgz#ad3947365ca038ec3a16fa4c48972708ccd294e9" + integrity sha512-iTnFcxKpmywCatx8ov4GTbECe3tJk2Bp1OA2mWRJde78q+7tpPYBhKMnwhlaoKy9oKQcy4UoEuuhoy2PSD13ww== dependencies: - onnxruntime-common "1.17.3" + onnxruntime-common "1.18.0" tar "^7.0.1" optionator@^0.9.3: @@ -2648,10 +2704,10 @@ path-parse@^1.0.7: resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== -path-scurry@^1.10.2: - version "1.10.2" - resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.10.2.tgz#8f6357eb1239d5fa1da8b9f70e9c080675458ba7" - integrity sha512-7xTavNy5RQXnsjANvVvMkEjvloOinkAjv/Z6Ildz9v2RinZ4SBKTWFOVRbaF8p0vpHnyjV/UwNDdKuUv6M5qcA== +path-scurry@^1.10.2, path-scurry@^1.11.1: + version "1.11.1" + resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.11.1.tgz#7960a668888594a0720b12a911d1a742ab9f11d2" + integrity sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA== dependencies: lru-cache "^10.2.0" minipass "^5.0.0 || ^6.0.2 || ^7.0.0" @@ -2706,9 +2762,9 @@ prettier-plugin-packagejson@^2: synckit "0.9.0" prettier@^3: - version "3.2.5" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.2.5.tgz#e52bc3090586e824964a8813b09aba6233b28368" - integrity sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A== + version "3.3.0" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.3.0.tgz#d173ea0524a691d4c0b1181752f2b46724328cdf" + integrity sha512-J9odKxERhCQ10OC2yb93583f6UnYutOeiV5i0zEDS7UGTdUt0u+y8erxl3lBKvwo/JHyyoEdXjwp4dke9oyZ/g== progress@^2.0.3: version "2.0.3" @@ -2770,6 +2826,18 @@ read-config-file@6.3.2: json5 "^2.2.0" lazy-val "^1.0.4" +read-config-file@6.4.0: + version "6.4.0" + resolved "https://registry.yarnpkg.com/read-config-file/-/read-config-file-6.4.0.tgz#970542833216cccff6b1d83320495003dcf85a45" + integrity sha512-uB5QOBeF84PT61GlV11OTV4jUGHAO3iDEOP6v9ygxhG6Bs9PLg7WsjNT6mtIX2G+x8lJTr4ZWNeG6LDTKkNf2Q== + dependencies: + config-file-ts "0.2.8-rc1" + dotenv "^16.4.5" + dotenv-expand "^11.0.6" + js-yaml "^4.1.0" + json5 "^2.2.3" + lazy-val "^1.0.5" + readable-stream@^3.0.2, readable-stream@^3.4.0, readable-stream@^3.6.0: version "3.6.2" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" @@ -2860,9 +2928,9 @@ rimraf@^3.0.2: glob "^7.1.3" rimraf@^5.0.5: - version "5.0.5" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-5.0.5.tgz#9be65d2d6e683447d2e9013da2bf451139a61ccf" - integrity sha512-CqDakW+hMe/Bz202FPEymy68P+G50RfMQK+Qo5YUqc9SPipvbGjCGKd0RSKEelbsfQuw3g5NZDSrlZZAJurH1A== + version "5.0.7" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-5.0.7.tgz#27bddf202e7d89cb2e0381656380d1734a854a74" + integrity sha512-nV6YcJo5wbLW77m+8KjH8aB/7/rxQy9SZ0HY5shnwULfS+9nmTtVXAJET5NdZmCzA4fPI/Hm1wo/Po/4mopOdg== dependencies: glob "^10.3.7" @@ -2924,12 +2992,12 @@ semver@^6.2.0: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== -semver@^7.3.2: +semver@^7.3.2, semver@^7.6.0: version "7.6.2" resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.2.tgz#1e3b34759f896e8f14d6134732ce798aeb0c6e13" integrity sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w== -semver@^7.3.5, semver@^7.3.8, semver@^7.5.3, semver@^7.6.0: +semver@^7.3.5, semver@^7.3.8, semver@^7.5.3: version "7.6.0" resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.0.tgz#1a46a4db4bffcccd97b743b5005c8325f23d4e2d" integrity sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg== @@ -3169,13 +3237,13 @@ tar@^6.0.5, tar@^6.1.11, tar@^6.1.12, tar@^6.1.2: yallist "^4.0.0" tar@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/tar/-/tar-7.0.1.tgz#8f6ccebcd91b69e9767a6fc4892799e8b0e606d5" - integrity sha512-IjMhdQMZFpKsHEQT3woZVxBtCQY+0wk3CVxdRkGXEgyGa0dNS/ehPvOMr2nmfC7x5Zj2N+l6yZUpmICjLGS35w== + version "7.2.0" + resolved "https://registry.yarnpkg.com/tar/-/tar-7.2.0.tgz#f03ae6ecd2e2bab880f2ef33450f502e761d7548" + integrity sha512-hctwP0Nb4AB60bj8WQgRYaMOuJYRAPMGiQUAotms5igN8ppfQM+IvjQ5HcKu1MaZh2Wy2KWVTe563Yj8dfc14w== dependencies: "@isaacs/fs-minipass" "^4.0.0" chownr "^3.0.0" - minipass "^5.0.0" + minipass "^7.1.0" minizlib "^3.0.1" mkdirp "^3.0.1" yallist "^5.0.0" @@ -3251,11 +3319,6 @@ type-fest@^0.13.1: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.13.1.tgz#0172cb5bce80b0bd542ea348db50c7e21834d934" integrity sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg== -type-fest@^0.20.2: - version "0.20.2" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" - integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== - type-fest@^2.17.0: version "2.19.0" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.19.0.tgz#88068015bb33036a598b952e55e9311a60fd3a9b" @@ -3266,7 +3329,16 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA== -typescript@^5, typescript@^5.3.3: +typescript-eslint@8.0.0-alpha.10: + version "8.0.0-alpha.10" + resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.0.0-alpha.10.tgz#2172d41ab30c8447927c3823c5a549b9c09be89f" + integrity sha512-iMbN7boDtUmcSDor/J022+H4G018W3r3RSUUr7yoghMTmFuKVIkI89xJHDg82DBGYkA0xOoDNPBr7XfRFbEXKQ== + dependencies: + "@typescript-eslint/eslint-plugin" "8.0.0-alpha.10" + "@typescript-eslint/parser" "8.0.0-alpha.10" + "@typescript-eslint/utils" "8.0.0-alpha.10" + +typescript@^5, typescript@^5.3.3, typescript@^5.4.3: version "5.4.5" resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.4.5.tgz#42ccef2c571fdbd0f6718b1d1f5e6e5ef006f611" integrity sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ== diff --git a/docs/docs/auth/migration-guides/steam/index.md b/docs/docs/auth/migration-guides/steam/index.md index acb1f77aa5..1784c61a2b 100644 --- a/docs/docs/auth/migration-guides/steam/index.md +++ b/docs/docs/auth/migration-guides/steam/index.md @@ -5,8 +5,6 @@ description: Guide for importing from Steam Authenticator to Ente Auth # Migrating from Steam Authenticator -A guide written by an ente.io lover - > [!WARNING] > > Steam Authenticator code is only supported after auth-v3.0.3, check the app's diff --git a/docs/docs/photos/faq/security-and-privacy.md b/docs/docs/photos/faq/security-and-privacy.md index 5aba33e8f1..2970c9aff6 100644 --- a/docs/docs/photos/faq/security-and-privacy.md +++ b/docs/docs/photos/faq/security-and-privacy.md @@ -87,3 +87,13 @@ Yes, Ente Photos has undergone a thorough security audit conducted by Cure53, in collaboration with Symbolic Software. Cure53 is a prominent German cybersecurity firm, while Symbolic Software specializes in applied cryptography. Please find the full report here: https://ente.io/blog/cryptography-audit/ + +## How can I delete my account? + +You can delete your account at any time by using the "Delete account" option in +the settings. For security reasons, we request you to delete your account on +your own instead of contacting support to ask them to delete your account. + +Note that both Ente photos and Ente auth data will be deleted when you delete +your account (irrespective of which app you delete it from) since both photos +and auth use the same underlying account. diff --git a/docs/docs/self-hosting/faq/sharing.md b/docs/docs/self-hosting/faq/sharing.md index 4e3652ff7f..c33216a90a 100644 --- a/docs/docs/self-hosting/faq/sharing.md +++ b/docs/docs/self-hosting/faq/sharing.md @@ -57,3 +57,39 @@ apps: (For more details, see [local.yaml](https://github.com/ente-io/ente/blob/main/server/configurations/local.yaml) in the server's source code). + +## Dockerfile example + +Here is an example of a Dockerfile by @Dylanger on our community Discord. This +runs a standalone self-hosted version of the public albums app in production +mode. + +```Dockerfile +FROM node:20-alpine as builder + +WORKDIR /app +COPY . . + +ARG NEXT_PUBLIC_ENTE_ENDPOINT=https://your.ente.tld.api +ENV NEXT_PUBLIC_ENTE_ALBUMS_ENDPOINT=https://your.albums.tld.api + +RUN yarn install && yarn build + +FROM node:20-alpine + +WORKDIR /app +COPY --from=builder /app/apps/photos/out . + +RUN npm install -g serve + +ENV PORT=3000 +EXPOSE ${PORT} + +CMD serve -s . -l tcp://0.0.0.0:${PORT} +``` + +Note that this only runs the public albums app, but the same principle can be +used to run both the normal Ente photos app and the public albums app. There is +a slightly more involved example showing how to do this also provided by in a +community contributed guide about +[configuring external S3](/self-hosting/guides/external-s3). diff --git a/docs/docs/self-hosting/guides/custom-server/index.md b/docs/docs/self-hosting/guides/custom-server/index.md index 110e3dbb88..81ba95a78a 100644 --- a/docs/docs/self-hosting/guides/custom-server/index.md +++ b/docs/docs/self-hosting/guides/custom-server/index.md @@ -37,7 +37,7 @@ endpoint: (Another [example](https://github.com/ente-io/ente/blob/main/cli/config.yaml.example)) -## Web appps and Photos desktop app +## Web apps and Photos desktop app You will need to build the app from source and use the `NEXT_PUBLIC_ENTE_ENDPOINT` environment variable to tell it which server to diff --git a/docs/docs/self-hosting/index.md b/docs/docs/self-hosting/index.md index 14f9ba4dd7..03e31226dd 100644 --- a/docs/docs/self-hosting/index.md +++ b/docs/docs/self-hosting/index.md @@ -41,7 +41,7 @@ NEXT_PUBLIC_ENTE_ENDPOINT=http://localhost:8080 yarn dev That's about it. If you open http://localhost:3000, you will be able to create an account on a Ente Photos web app running on your machine, and this web app will be connecting to the server running on your local machine at -localhost:8080. +`localhost:8080`. For the mobile apps, you don't even need to build, and can install normal Ente apps and configure them to use your diff --git a/infra/staff/.env.local b/infra/staff/.env.local new file mode 100644 index 0000000000..3151dc2a84 --- /dev/null +++ b/infra/staff/.env.local @@ -0,0 +1 @@ +VITE_ENTE_ENDPOINT = http://localhost:8080 diff --git a/desktop/.eslintrc.js b/infra/staff/.eslintrc.cjs similarity index 63% rename from desktop/.eslintrc.js rename to infra/staff/.eslintrc.cjs index 44d03ef0c1..d1bbd1afa5 100644 --- a/desktop/.eslintrc.js +++ b/infra/staff/.eslintrc.cjs @@ -3,20 +3,17 @@ module.exports = { root: true, extends: [ "eslint:recommended", - "plugin:@typescript-eslint/eslint-recommended", "plugin:@typescript-eslint/strict-type-checked", "plugin:@typescript-eslint/stylistic-type-checked", + "plugin:react/recommended", + "plugin:react-hooks/recommended", + "plugin:react/jsx-runtime", ], - plugins: ["@typescript-eslint"], + plugins: ["@typescript-eslint", "react-refresh"], + parserOptions: { project: true }, parser: "@typescript-eslint/parser", - parserOptions: { - project: true, - }, - ignorePatterns: [".eslintrc.js", "app", "out", "dist"], - env: { - es2022: true, - node: true, - }, + ignorePatterns: [".eslintrc.cjs", "vite.config.ts", "dist"], + settings: { react: { version: "18.2" } }, rules: { /* Allow numbers to be used in template literals */ "@typescript-eslint/restrict-template-expressions": [ @@ -32,5 +29,9 @@ module.exports = { ignoreArrowShorthand: true, }, ], + "react-refresh/only-export-components": [ + "warn", + { allowConstantExport: true }, + ], }, }; diff --git a/infra/staff/.gitignore b/infra/staff/.gitignore new file mode 100644 index 0000000000..2c6a2dd2e8 --- /dev/null +++ b/infra/staff/.gitignore @@ -0,0 +1,17 @@ +# Node +node_modules/ + +# macOS +.DS_Store + +# Editors +.vscode/ + +# Local env files +.env*.local + +# tsc +*.tsbuildinfo + +# Vite +dist diff --git a/infra/staff/.prettierrc.json b/infra/staff/.prettierrc.json new file mode 100644 index 0000000000..7cf8c86c77 --- /dev/null +++ b/infra/staff/.prettierrc.json @@ -0,0 +1,8 @@ +{ + "tabWidth": 4, + "proseWrap": "always", + "plugins": [ + "prettier-plugin-organize-imports", + "prettier-plugin-packagejson" + ] +} diff --git a/infra/staff/README.md b/infra/staff/README.md new file mode 100644 index 0000000000..d6d567e1b3 --- /dev/null +++ b/infra/staff/README.md @@ -0,0 +1,19 @@ +## Staff dashboard + +Web app for staff members to help with support and other administration. + +### Development + +Use `yarn dev` to run a local dev server with hot reload. + +> [!TIP] +> +> See [web/docs/new.md](../../web/docs/new.md) for help in setting up your +> editor to do the formatting and linting. You can also run the formatter and +> linter manually using `yarn lint`, and `yarn lint-fix` to fix them. These +> commands automatically run on every PR. + +### Deployment + +The app gets redeployed whenever a PR is merged into main. See +[web/docs/deploy.md](../../web/docs/deploy.md) for more details. diff --git a/web/apps/staff/index.html b/infra/staff/index.html similarity index 100% rename from web/apps/staff/index.html rename to infra/staff/index.html diff --git a/infra/staff/package.json b/infra/staff/package.json new file mode 100644 index 0000000000..0f60a1e7cd --- /dev/null +++ b/infra/staff/package.json @@ -0,0 +1,35 @@ +{ + "name": "staff", + "version": "0.0.0", + "private": true, + "type": "module", + "scripts": { + "build": "tsc && vite build", + "dev": "vite", + "lint": "yarn prettier --check --log-level warn . && yarn eslint && yarn tsc", + "lint-fix": "yarn prettier --write --log-level warn . && yarn eslint --fix && yarn tsc", + "preview": "vite preview" + }, + "dependencies": { + "react": "^18", + "react-dom": "^18", + "zod": "^3" + }, + "devDependencies": { + "@types/react": "^18", + "@types/react-dom": "^18", + "@typescript-eslint/eslint-plugin": "^7", + "@typescript-eslint/parser": "^7", + "@vitejs/plugin-react": "^4.2", + "eslint": "^8", + "eslint-plugin-react": "^7.34", + "eslint-plugin-react-hooks": "^4.6", + "eslint-plugin-react-refresh": "^0.4.7", + "prettier": "^3", + "prettier-plugin-organize-imports": "^3.2", + "prettier-plugin-packagejson": "^2.5", + "typescript": "^5", + "vite": "^5.2" + }, + "packageManager": "yarn@1.22.21" +} diff --git a/web/apps/staff/src/App.tsx b/infra/staff/src/App.tsx similarity index 100% rename from web/apps/staff/src/App.tsx rename to infra/staff/src/App.tsx diff --git a/web/apps/staff/src/components/Container.tsx b/infra/staff/src/components/Container.tsx similarity index 100% rename from web/apps/staff/src/components/Container.tsx rename to infra/staff/src/components/Container.tsx diff --git a/web/apps/staff/src/main.tsx b/infra/staff/src/main.tsx similarity index 100% rename from web/apps/staff/src/main.tsx rename to infra/staff/src/main.tsx diff --git a/web/apps/staff/src/services/support-service.ts b/infra/staff/src/services/support-service.ts similarity index 70% rename from web/apps/staff/src/services/support-service.ts rename to infra/staff/src/services/support-service.ts index 0b27260e3f..3e22234e76 100644 --- a/web/apps/staff/src/services/support-service.ts +++ b/infra/staff/src/services/support-service.ts @@ -1,10 +1,10 @@ -import { object, type InferType } from "yup"; +import { z } from "zod"; const apiOrigin = import.meta.env.VITE_ENTE_ENDPOINT ?? "https://api.ente.io"; -const userDetailsSchema = object({}); +const UserDetails = z.object({}).passthrough(); -export type UserDetails = InferType; +export type UserDetails = z.infer; /** Fetch details of the user associated with the given {@link authToken}. */ export const getUserDetails = async ( @@ -17,5 +17,5 @@ export const getUserDetails = async ( }, }); if (!res.ok) throw new Error(`Failed to fetch ${url}: HTTP ${res.status}`); - return await userDetailsSchema.validate(await res.json()); + return UserDetails.parse(await res.json()); }; diff --git a/web/apps/staff/src/styles/globals.css b/infra/staff/src/styles/globals.css similarity index 100% rename from web/apps/staff/src/styles/globals.css rename to infra/staff/src/styles/globals.css diff --git a/web/apps/staff/src/utils/strings.ts b/infra/staff/src/utils/strings.ts similarity index 100% rename from web/apps/staff/src/utils/strings.ts rename to infra/staff/src/utils/strings.ts diff --git a/web/apps/staff/src/vite-env.d.ts b/infra/staff/src/vite-env.d.ts similarity index 100% rename from web/apps/staff/src/vite-env.d.ts rename to infra/staff/src/vite-env.d.ts diff --git a/infra/staff/tsconfig.json b/infra/staff/tsconfig.json new file mode 100644 index 0000000000..e4ff26ca2d --- /dev/null +++ b/infra/staff/tsconfig.json @@ -0,0 +1,48 @@ +{ + /* TSConfig file used for typechecking the files in src/. + * + * The base configuration was generated using `yarn create vite`. This was + * already almost the same as the `tsconfig-typecheck.json` we use + * elsewhere, with one or two differences. + * + * For more details about the flags vite cares about, see + * https://vitejs.dev/guide/features.html#typescript-compiler-options + */ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "esnext", + "skipLibCheck": true, + + /* Bundler mode. */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + + /* Linting. */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + + /* + * On top of the generated configuration, we've mostly added additional + * strictness checks. + */ + + /* Require the `type` modifier when importing types. */ + "verbatimModuleSyntax": true, + + /* Stricter than strict. */ + "noImplicitReturns": true, + /* e.g. makes array indexing returns undefined. */ + "noUncheckedIndexedAccess": true, + "exactOptionalPropertyTypes": true + }, + "include": ["src"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/infra/staff/tsconfig.node.json b/infra/staff/tsconfig.node.json new file mode 100644 index 0000000000..71c4923013 --- /dev/null +++ b/infra/staff/tsconfig.node.json @@ -0,0 +1,15 @@ +{ + /* TSConfig file used for typechecking vite's config file itself. + * + * These are vite defaults, generated using `yarn create vite`. + */ + "compilerOptions": { + "composite": true, + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true, + "strict": true + }, + "include": ["vite.config.ts"] +} diff --git a/web/apps/staff/vite.config.ts b/infra/staff/vite.config.ts similarity index 100% rename from web/apps/staff/vite.config.ts rename to infra/staff/vite.config.ts diff --git a/infra/staff/yarn.lock b/infra/staff/yarn.lock new file mode 100644 index 0000000000..8471e93856 --- /dev/null +++ b/infra/staff/yarn.lock @@ -0,0 +1,2654 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@ampproject/remapping@^2.2.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.3.0.tgz#ed441b6fa600072520ce18b43d2c8cc8caecc7f4" + integrity sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw== + dependencies: + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.24" + +"@babel/code-frame@^7.24.6": + version "7.24.6" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.24.6.tgz#ab88da19344445c3d8889af2216606d3329f3ef2" + integrity sha512-ZJhac6FkEd1yhG2AHOmfcXG4ceoLltoCVJjN5XsWN9BifBQr+cHJbWi0h68HZuSORq+3WtJ2z0hwF2NG1b5kcA== + dependencies: + "@babel/highlight" "^7.24.6" + picocolors "^1.0.0" + +"@babel/compat-data@^7.24.6": + version "7.24.6" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.24.6.tgz#b3600217688cabb26e25f8e467019e66d71b7ae2" + integrity sha512-aC2DGhBq5eEdyXWqrDInSqQjO0k8xtPRf5YylULqx8MCd6jBtzqfta/3ETMRpuKIc5hyswfO80ObyA1MvkCcUQ== + +"@babel/core@^7.24.5": + version "7.24.6" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.24.6.tgz#8650e0e4b03589ebe886c4e4a60398db0a7ec787" + integrity sha512-qAHSfAdVyFmIvl0VHELib8xar7ONuSHrE2hLnsaWkYNTI68dmi1x8GYDhJjMI/e7XWal9QBlZkwbOnkcw7Z8gQ== + dependencies: + "@ampproject/remapping" "^2.2.0" + "@babel/code-frame" "^7.24.6" + "@babel/generator" "^7.24.6" + "@babel/helper-compilation-targets" "^7.24.6" + "@babel/helper-module-transforms" "^7.24.6" + "@babel/helpers" "^7.24.6" + "@babel/parser" "^7.24.6" + "@babel/template" "^7.24.6" + "@babel/traverse" "^7.24.6" + "@babel/types" "^7.24.6" + convert-source-map "^2.0.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.2.3" + semver "^6.3.1" + +"@babel/generator@^7.24.6": + version "7.24.6" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.24.6.tgz#dfac82a228582a9d30c959fe50ad28951d4737a7" + integrity sha512-S7m4eNa6YAPJRHmKsLHIDJhNAGNKoWNiWefz1MBbpnt8g9lvMDl1hir4P9bo/57bQEmuwEhnRU/AMWsD0G/Fbg== + dependencies: + "@babel/types" "^7.24.6" + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.25" + jsesc "^2.5.1" + +"@babel/helper-compilation-targets@^7.24.6": + version "7.24.6" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.24.6.tgz#4a51d681f7680043d38e212715e2a7b1ad29cb51" + integrity sha512-VZQ57UsDGlX/5fFA7GkVPplZhHsVc+vuErWgdOiysI9Ksnw0Pbbd6pnPiR/mmJyKHgyIW0c7KT32gmhiF+cirg== + dependencies: + "@babel/compat-data" "^7.24.6" + "@babel/helper-validator-option" "^7.24.6" + browserslist "^4.22.2" + lru-cache "^5.1.1" + semver "^6.3.1" + +"@babel/helper-environment-visitor@^7.24.6": + version "7.24.6" + resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.24.6.tgz#ac7ad5517821641550f6698dd5468f8cef78620d" + integrity sha512-Y50Cg3k0LKLMjxdPjIl40SdJgMB85iXn27Vk/qbHZCFx/o5XO3PSnpi675h1KEmmDb6OFArfd5SCQEQ5Q4H88g== + +"@babel/helper-function-name@^7.24.6": + version "7.24.6" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.24.6.tgz#cebdd063386fdb95d511d84b117e51fc68fec0c8" + integrity sha512-xpeLqeeRkbxhnYimfr2PC+iA0Q7ljX/d1eZ9/inYbmfG2jpl8Lu3DyXvpOAnrS5kxkfOWJjioIMQsaMBXFI05w== + dependencies: + "@babel/template" "^7.24.6" + "@babel/types" "^7.24.6" + +"@babel/helper-hoist-variables@^7.24.6": + version "7.24.6" + resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.24.6.tgz#8a7ece8c26756826b6ffcdd0e3cf65de275af7f9" + integrity sha512-SF/EMrC3OD7dSta1bLJIlrsVxwtd0UpjRJqLno6125epQMJ/kyFmpTT4pbvPbdQHzCHg+biQ7Syo8lnDtbR+uA== + dependencies: + "@babel/types" "^7.24.6" + +"@babel/helper-module-imports@^7.24.6": + version "7.24.6" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.24.6.tgz#65e54ffceed6a268dc4ce11f0433b82cfff57852" + integrity sha512-a26dmxFJBF62rRO9mmpgrfTLsAuyHk4e1hKTUkD/fcMfynt8gvEKwQPQDVxWhca8dHoDck+55DFt42zV0QMw5g== + dependencies: + "@babel/types" "^7.24.6" + +"@babel/helper-module-transforms@^7.24.6": + version "7.24.6" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.24.6.tgz#22346ed9df44ce84dee850d7433c5b73fab1fe4e" + integrity sha512-Y/YMPm83mV2HJTbX1Qh2sjgjqcacvOlhbzdCCsSlblOKjSYmQqEbO6rUniWQyRo9ncyfjT8hnUjlG06RXDEmcA== + dependencies: + "@babel/helper-environment-visitor" "^7.24.6" + "@babel/helper-module-imports" "^7.24.6" + "@babel/helper-simple-access" "^7.24.6" + "@babel/helper-split-export-declaration" "^7.24.6" + "@babel/helper-validator-identifier" "^7.24.6" + +"@babel/helper-plugin-utils@^7.24.6": + version "7.24.6" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.6.tgz#fa02a32410a15a6e8f8185bcbf608f10528d2a24" + integrity sha512-MZG/JcWfxybKwsA9N9PmtF2lOSFSEMVCpIRrbxccZFLJPrJciJdG/UhSh5W96GEteJI2ARqm5UAHxISwRDLSNg== + +"@babel/helper-simple-access@^7.24.6": + version "7.24.6" + resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.24.6.tgz#1d6e04d468bba4fc963b4906f6dac6286cfedff1" + integrity sha512-nZzcMMD4ZhmB35MOOzQuiGO5RzL6tJbsT37Zx8M5L/i9KSrukGXWTjLe1knIbb/RmxoJE9GON9soq0c0VEMM5g== + dependencies: + "@babel/types" "^7.24.6" + +"@babel/helper-split-export-declaration@^7.24.6": + version "7.24.6" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.6.tgz#e830068f7ba8861c53b7421c284da30ae656d7a3" + integrity sha512-CvLSkwXGWnYlF9+J3iZUvwgAxKiYzK3BWuo+mLzD/MDGOZDj7Gq8+hqaOkMxmJwmlv0iu86uH5fdADd9Hxkymw== + dependencies: + "@babel/types" "^7.24.6" + +"@babel/helper-string-parser@^7.24.6": + version "7.24.6" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.24.6.tgz#28583c28b15f2a3339cfafafeaad42f9a0e828df" + integrity sha512-WdJjwMEkmBicq5T9fm/cHND3+UlFa2Yj8ALLgmoSQAJZysYbBjw+azChSGPN4DSPLXOcooGRvDwZWMcF/mLO2Q== + +"@babel/helper-validator-identifier@^7.24.6": + version "7.24.6" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.6.tgz#08bb6612b11bdec78f3feed3db196da682454a5e" + integrity sha512-4yA7s865JHaqUdRbnaxarZREuPTHrjpDT+pXoAZ1yhyo6uFnIEpS8VMu16siFOHDpZNKYv5BObhsB//ycbICyw== + +"@babel/helper-validator-option@^7.24.6": + version "7.24.6" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.24.6.tgz#59d8e81c40b7d9109ab7e74457393442177f460a" + integrity sha512-Jktc8KkF3zIkePb48QO+IapbXlSapOW9S+ogZZkcO6bABgYAxtZcjZ/O005111YLf+j4M84uEgwYoidDkXbCkQ== + +"@babel/helpers@^7.24.6": + version "7.24.6" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.24.6.tgz#cd124245299e494bd4e00edda0e4ea3545c2c176" + integrity sha512-V2PI+NqnyFu1i0GyTd/O/cTpxzQCYioSkUIRmgo7gFEHKKCg5w46+r/A6WeUR1+P3TeQ49dspGPNd/E3n9AnnA== + dependencies: + "@babel/template" "^7.24.6" + "@babel/types" "^7.24.6" + +"@babel/highlight@^7.24.6": + version "7.24.6" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.24.6.tgz#6d610c1ebd2c6e061cade0153bf69b0590b7b3df" + integrity sha512-2YnuOp4HAk2BsBrJJvYCbItHx0zWscI1C3zgWkz+wDyD9I7GIVrfnLyrR4Y1VR+7p+chAEcrgRQYZAGIKMV7vQ== + dependencies: + "@babel/helper-validator-identifier" "^7.24.6" + chalk "^2.4.2" + js-tokens "^4.0.0" + picocolors "^1.0.0" + +"@babel/parser@^7.1.0", "@babel/parser@^7.20.7", "@babel/parser@^7.24.6": + version "7.24.6" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.24.6.tgz#5e030f440c3c6c78d195528c3b688b101a365328" + integrity sha512-eNZXdfU35nJC2h24RznROuOpO94h6x8sg9ju0tT9biNtLZ2vuP8SduLqqV+/8+cebSLV9SJEAN5Z3zQbJG/M+Q== + +"@babel/plugin-transform-react-jsx-self@^7.24.5": + version "7.24.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.24.6.tgz#4fa4870d594d6840d724d2006d0f98b19be6f502" + integrity sha512-FfZfHXtQ5jYPQsCRyLpOv2GeLIIJhs8aydpNh39vRDjhD411XcfWDni5i7OjP/Rs8GAtTn7sWFFELJSHqkIxYg== + dependencies: + "@babel/helper-plugin-utils" "^7.24.6" + +"@babel/plugin-transform-react-jsx-source@^7.24.1": + version "7.24.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.24.6.tgz#4e1503f24ca5fccb1fc7f20c57426899d5ce5c1f" + integrity sha512-BQTBCXmFRreU3oTUXcGKuPOfXAGb1liNY4AvvFKsOBAJ89RKcTsIrSsnMYkj59fNa66OFKnSa4AJZfy5Y4B9WA== + dependencies: + "@babel/helper-plugin-utils" "^7.24.6" + +"@babel/template@^7.24.6": + version "7.24.6" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.24.6.tgz#048c347b2787a6072b24c723664c8d02b67a44f9" + integrity sha512-3vgazJlLwNXi9jhrR1ef8qiB65L1RK90+lEQwv4OxveHnqC3BfmnHdgySwRLzf6akhlOYenT+b7AfWq+a//AHw== + dependencies: + "@babel/code-frame" "^7.24.6" + "@babel/parser" "^7.24.6" + "@babel/types" "^7.24.6" + +"@babel/traverse@^7.24.6": + version "7.24.6" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.24.6.tgz#0941ec50cdeaeacad0911eb67ae227a4f8424edc" + integrity sha512-OsNjaJwT9Zn8ozxcfoBc+RaHdj3gFmCmYoQLUII1o6ZrUwku0BMg80FoOTPx+Gi6XhcQxAYE4xyjPTo4SxEQqw== + dependencies: + "@babel/code-frame" "^7.24.6" + "@babel/generator" "^7.24.6" + "@babel/helper-environment-visitor" "^7.24.6" + "@babel/helper-function-name" "^7.24.6" + "@babel/helper-hoist-variables" "^7.24.6" + "@babel/helper-split-export-declaration" "^7.24.6" + "@babel/parser" "^7.24.6" + "@babel/types" "^7.24.6" + debug "^4.3.1" + globals "^11.1.0" + +"@babel/types@^7.0.0", "@babel/types@^7.20.7", "@babel/types@^7.24.6": + version "7.24.6" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.24.6.tgz#ba4e1f59870c10dc2fa95a274ac4feec23b21912" + integrity sha512-WaMsgi6Q8zMgMth93GvWPXkhAIEobfsIkLTacoVZoK1J0CevIPGYY2Vo5YvJGqyHqXM6P4ppOYGsIRU8MM9pFQ== + dependencies: + "@babel/helper-string-parser" "^7.24.6" + "@babel/helper-validator-identifier" "^7.24.6" + to-fast-properties "^2.0.0" + +"@esbuild/aix-ppc64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz#a70f4ac11c6a1dfc18b8bbb13284155d933b9537" + integrity sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g== + +"@esbuild/android-arm64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz#db1c9202a5bc92ea04c7b6840f1bbe09ebf9e6b9" + integrity sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg== + +"@esbuild/android-arm@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.20.2.tgz#3b488c49aee9d491c2c8f98a909b785870d6e995" + integrity sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w== + +"@esbuild/android-x64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.20.2.tgz#3b1628029e5576249d2b2d766696e50768449f98" + integrity sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg== + +"@esbuild/darwin-arm64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz#6e8517a045ddd86ae30c6608c8475ebc0c4000bb" + integrity sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA== + +"@esbuild/darwin-x64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz#90ed098e1f9dd8a9381695b207e1cff45540a0d0" + integrity sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA== + +"@esbuild/freebsd-arm64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz#d71502d1ee89a1130327e890364666c760a2a911" + integrity sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw== + +"@esbuild/freebsd-x64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz#aa5ea58d9c1dd9af688b8b6f63ef0d3d60cea53c" + integrity sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw== + +"@esbuild/linux-arm64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz#055b63725df678379b0f6db9d0fa85463755b2e5" + integrity sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A== + +"@esbuild/linux-arm@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz#76b3b98cb1f87936fbc37f073efabad49dcd889c" + integrity sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg== + +"@esbuild/linux-ia32@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz#c0e5e787c285264e5dfc7a79f04b8b4eefdad7fa" + integrity sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig== + +"@esbuild/linux-loong64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz#a6184e62bd7cdc63e0c0448b83801001653219c5" + integrity sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ== + +"@esbuild/linux-mips64el@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz#d08e39ce86f45ef8fc88549d29c62b8acf5649aa" + integrity sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA== + +"@esbuild/linux-ppc64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz#8d252f0b7756ffd6d1cbde5ea67ff8fd20437f20" + integrity sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg== + +"@esbuild/linux-riscv64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz#19f6dcdb14409dae607f66ca1181dd4e9db81300" + integrity sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg== + +"@esbuild/linux-s390x@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz#3c830c90f1a5d7dd1473d5595ea4ebb920988685" + integrity sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ== + +"@esbuild/linux-x64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz#86eca35203afc0d9de0694c64ec0ab0a378f6fff" + integrity sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw== + +"@esbuild/netbsd-x64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz#e771c8eb0e0f6e1877ffd4220036b98aed5915e6" + integrity sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ== + +"@esbuild/openbsd-x64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz#9a795ae4b4e37e674f0f4d716f3e226dd7c39baf" + integrity sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ== + +"@esbuild/sunos-x64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz#7df23b61a497b8ac189def6e25a95673caedb03f" + integrity sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w== + +"@esbuild/win32-arm64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz#f1ae5abf9ca052ae11c1bc806fb4c0f519bacf90" + integrity sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ== + +"@esbuild/win32-ia32@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz#241fe62c34d8e8461cd708277813e1d0ba55ce23" + integrity sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ== + +"@esbuild/win32-x64@0.20.2": + version "0.20.2" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz#9c907b21e30a52db959ba4f80bb01a0cc403d5cc" + integrity sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ== + +"@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0": + version "4.4.0" + resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59" + integrity sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA== + dependencies: + eslint-visitor-keys "^3.3.0" + +"@eslint-community/regexpp@^4.10.0", "@eslint-community/regexpp@^4.6.1": + version "4.10.0" + resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.10.0.tgz#548f6de556857c8bb73bbee70c35dc82a2e74d63" + integrity sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA== + +"@eslint/eslintrc@^2.1.4": + version "2.1.4" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.1.4.tgz#388a269f0f25c1b6adc317b5a2c55714894c70ad" + integrity sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ== + dependencies: + ajv "^6.12.4" + debug "^4.3.2" + espree "^9.6.0" + globals "^13.19.0" + ignore "^5.2.0" + import-fresh "^3.2.1" + js-yaml "^4.1.0" + minimatch "^3.1.2" + strip-json-comments "^3.1.1" + +"@eslint/js@8.57.0": + version "8.57.0" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.57.0.tgz#a5417ae8427873f1dd08b70b3574b453e67b5f7f" + integrity sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g== + +"@humanwhocodes/config-array@^0.11.14": + version "0.11.14" + resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.14.tgz#d78e481a039f7566ecc9660b4ea7fe6b1fec442b" + integrity sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg== + dependencies: + "@humanwhocodes/object-schema" "^2.0.2" + debug "^4.3.1" + minimatch "^3.0.5" + +"@humanwhocodes/module-importer@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c" + integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== + +"@humanwhocodes/object-schema@^2.0.2": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz#4a2868d75d6d6963e423bcf90b7fd1be343409d3" + integrity sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA== + +"@jridgewell/gen-mapping@^0.3.5": + version "0.3.5" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz#dcce6aff74bdf6dad1a95802b69b04a2fcb1fb36" + integrity sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg== + dependencies: + "@jridgewell/set-array" "^1.2.1" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping" "^0.3.24" + +"@jridgewell/resolve-uri@^3.1.0": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" + integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== + +"@jridgewell/set-array@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.2.1.tgz#558fb6472ed16a4c850b889530e6b36438c49280" + integrity sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A== + +"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14": + version "1.4.15" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" + integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== + +"@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25": + version "0.3.25" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz#15f190e98895f3fc23276ee14bc76b675c2e50f0" + integrity sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ== + dependencies: + "@jridgewell/resolve-uri" "^3.1.0" + "@jridgewell/sourcemap-codec" "^1.4.14" + +"@nodelib/fs.scandir@2.1.5": + version "2.1.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" + integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== + dependencies: + "@nodelib/fs.stat" "2.0.5" + run-parallel "^1.1.9" + +"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" + integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== + +"@nodelib/fs.walk@^1.2.3", "@nodelib/fs.walk@^1.2.8": + version "1.2.8" + resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" + integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== + dependencies: + "@nodelib/fs.scandir" "2.1.5" + fastq "^1.6.0" + +"@pkgr/core@^0.1.0": + version "0.1.1" + resolved "https://registry.yarnpkg.com/@pkgr/core/-/core-0.1.1.tgz#1ec17e2edbec25c8306d424ecfbf13c7de1aaa31" + integrity sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA== + +"@rollup/rollup-android-arm-eabi@4.18.0": + version "4.18.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.18.0.tgz#bbd0e616b2078cd2d68afc9824d1fadb2f2ffd27" + integrity sha512-Tya6xypR10giZV1XzxmH5wr25VcZSncG0pZIjfePT0OVBvqNEurzValetGNarVrGiq66EBVAFn15iYX4w6FKgQ== + +"@rollup/rollup-android-arm64@4.18.0": + version "4.18.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.18.0.tgz#97255ef6384c5f73f4800c0de91f5f6518e21203" + integrity sha512-avCea0RAP03lTsDhEyfy+hpfr85KfyTctMADqHVhLAF3MlIkq83CP8UfAHUssgXTYd+6er6PaAhx/QGv4L1EiA== + +"@rollup/rollup-darwin-arm64@4.18.0": + version "4.18.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.18.0.tgz#b6dd74e117510dfe94541646067b0545b42ff096" + integrity sha512-IWfdwU7KDSm07Ty0PuA/W2JYoZ4iTj3TUQjkVsO/6U+4I1jN5lcR71ZEvRh52sDOERdnNhhHU57UITXz5jC1/w== + +"@rollup/rollup-darwin-x64@4.18.0": + version "4.18.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.18.0.tgz#e07d76de1cec987673e7f3d48ccb8e106d42c05c" + integrity sha512-n2LMsUz7Ynu7DoQrSQkBf8iNrjOGyPLrdSg802vk6XT3FtsgX6JbE8IHRvposskFm9SNxzkLYGSq9QdpLYpRNA== + +"@rollup/rollup-linux-arm-gnueabihf@4.18.0": + version "4.18.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.18.0.tgz#9f1a6d218b560c9d75185af4b8bb42f9f24736b8" + integrity sha512-C/zbRYRXFjWvz9Z4haRxcTdnkPt1BtCkz+7RtBSuNmKzMzp3ZxdM28Mpccn6pt28/UWUCTXa+b0Mx1k3g6NOMA== + +"@rollup/rollup-linux-arm-musleabihf@4.18.0": + version "4.18.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.18.0.tgz#53618b92e6ffb642c7b620e6e528446511330549" + integrity sha512-l3m9ewPgjQSXrUMHg93vt0hYCGnrMOcUpTz6FLtbwljo2HluS4zTXFy2571YQbisTnfTKPZ01u/ukJdQTLGh9A== + +"@rollup/rollup-linux-arm64-gnu@4.18.0": + version "4.18.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.18.0.tgz#99a7ba5e719d4f053761a698f7b52291cefba577" + integrity sha512-rJ5D47d8WD7J+7STKdCUAgmQk49xuFrRi9pZkWoRD1UeSMakbcepWXPF8ycChBoAqs1pb2wzvbY6Q33WmN2ftw== + +"@rollup/rollup-linux-arm64-musl@4.18.0": + version "4.18.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.18.0.tgz#f53db99a45d9bc00ce94db8a35efa7c3c144a58c" + integrity sha512-be6Yx37b24ZwxQ+wOQXXLZqpq4jTckJhtGlWGZs68TgdKXJgw54lUUoFYrg6Zs/kjzAQwEwYbp8JxZVzZLRepQ== + +"@rollup/rollup-linux-powerpc64le-gnu@4.18.0": + version "4.18.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.18.0.tgz#cbb0837408fe081ce3435cf3730e090febafc9bf" + integrity sha512-hNVMQK+qrA9Todu9+wqrXOHxFiD5YmdEi3paj6vP02Kx1hjd2LLYR2eaN7DsEshg09+9uzWi2W18MJDlG0cxJA== + +"@rollup/rollup-linux-riscv64-gnu@4.18.0": + version "4.18.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.18.0.tgz#8ed09c1d1262ada4c38d791a28ae0fea28b80cc9" + integrity sha512-ROCM7i+m1NfdrsmvwSzoxp9HFtmKGHEqu5NNDiZWQtXLA8S5HBCkVvKAxJ8U+CVctHwV2Gb5VUaK7UAkzhDjlg== + +"@rollup/rollup-linux-s390x-gnu@4.18.0": + version "4.18.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.18.0.tgz#938138d3c8e0c96f022252a28441dcfb17afd7ec" + integrity sha512-0UyyRHyDN42QL+NbqevXIIUnKA47A+45WyasO+y2bGJ1mhQrfrtXUpTxCOrfxCR4esV3/RLYyucGVPiUsO8xjg== + +"@rollup/rollup-linux-x64-gnu@4.18.0": + version "4.18.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.18.0.tgz#1a7481137a54740bee1ded4ae5752450f155d942" + integrity sha512-xuglR2rBVHA5UsI8h8UbX4VJ470PtGCf5Vpswh7p2ukaqBGFTnsfzxUBetoWBWymHMxbIG0Cmx7Y9qDZzr648w== + +"@rollup/rollup-linux-x64-musl@4.18.0": + version "4.18.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.18.0.tgz#f1186afc601ac4f4fc25fac4ca15ecbee3a1874d" + integrity sha512-LKaqQL9osY/ir2geuLVvRRs+utWUNilzdE90TpyoX0eNqPzWjRm14oMEE+YLve4k/NAqCdPkGYDaDF5Sw+xBfg== + +"@rollup/rollup-win32-arm64-msvc@4.18.0": + version "4.18.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.18.0.tgz#ed6603e93636a96203c6915be4117245c1bd2daf" + integrity sha512-7J6TkZQFGo9qBKH0pk2cEVSRhJbL6MtfWxth7Y5YmZs57Pi+4x6c2dStAUvaQkHQLnEQv1jzBUW43GvZW8OFqA== + +"@rollup/rollup-win32-ia32-msvc@4.18.0": + version "4.18.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.18.0.tgz#14e0b404b1c25ebe6157a15edb9c46959ba74c54" + integrity sha512-Txjh+IxBPbkUB9+SXZMpv+b/vnTEtFyfWZgJ6iyCmt2tdx0OF5WhFowLmnh8ENGNpfUlUZkdI//4IEmhwPieNg== + +"@rollup/rollup-win32-x64-msvc@4.18.0": + version "4.18.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.18.0.tgz#5d694d345ce36b6ecf657349e03eb87297e68da4" + integrity sha512-UOo5FdvOL0+eIVTgS4tIdbW+TtnBLWg1YBCcU2KWM7nuNwRz9bksDX1bekJJCpu25N1DVWaCwnT39dVQxzqS8g== + +"@types/babel__core@^7.20.5": + version "7.20.5" + resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.20.5.tgz#3df15f27ba85319caa07ba08d0721889bb39c017" + integrity sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA== + dependencies: + "@babel/parser" "^7.20.7" + "@babel/types" "^7.20.7" + "@types/babel__generator" "*" + "@types/babel__template" "*" + "@types/babel__traverse" "*" + +"@types/babel__generator@*": + version "7.6.8" + resolved "https://registry.yarnpkg.com/@types/babel__generator/-/babel__generator-7.6.8.tgz#f836c61f48b1346e7d2b0d93c6dacc5b9535d3ab" + integrity sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw== + dependencies: + "@babel/types" "^7.0.0" + +"@types/babel__template@*": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@types/babel__template/-/babel__template-7.4.4.tgz#5672513701c1b2199bc6dad636a9d7491586766f" + integrity sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A== + dependencies: + "@babel/parser" "^7.1.0" + "@babel/types" "^7.0.0" + +"@types/babel__traverse@*": + version "7.20.6" + resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.20.6.tgz#8dc9f0ae0f202c08d8d4dab648912c8d6038e3f7" + integrity sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg== + dependencies: + "@babel/types" "^7.20.7" + +"@types/estree@1.0.5": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4" + integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw== + +"@types/prop-types@*": + version "15.7.12" + resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.12.tgz#12bb1e2be27293c1406acb6af1c3f3a1481d98c6" + integrity sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q== + +"@types/react-dom@^18": + version "18.3.0" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.3.0.tgz#0cbc818755d87066ab6ca74fbedb2547d74a82b0" + integrity sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg== + dependencies: + "@types/react" "*" + +"@types/react@*", "@types/react@^18": + version "18.3.3" + resolved "https://registry.yarnpkg.com/@types/react/-/react-18.3.3.tgz#9679020895318b0915d7a3ab004d92d33375c45f" + integrity sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw== + dependencies: + "@types/prop-types" "*" + csstype "^3.0.2" + +"@typescript-eslint/eslint-plugin@^7": + version "7.11.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.11.0.tgz#f90f0914657ead08e1c75f66939c926edeab42dd" + integrity sha512-P+qEahbgeHW4JQ/87FuItjBj8O3MYv5gELDzr8QaQ7fsll1gSMTYb6j87MYyxwf3DtD7uGFB9ShwgmCJB5KmaQ== + dependencies: + "@eslint-community/regexpp" "^4.10.0" + "@typescript-eslint/scope-manager" "7.11.0" + "@typescript-eslint/type-utils" "7.11.0" + "@typescript-eslint/utils" "7.11.0" + "@typescript-eslint/visitor-keys" "7.11.0" + graphemer "^1.4.0" + ignore "^5.3.1" + natural-compare "^1.4.0" + ts-api-utils "^1.3.0" + +"@typescript-eslint/parser@^7": + version "7.11.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-7.11.0.tgz#525ad8bee54a8f015f134edd241d91b84ab64839" + integrity sha512-yimw99teuaXVWsBcPO1Ais02kwJ1jmNA1KxE7ng0aT7ndr1pT1wqj0OJnsYVGKKlc4QJai86l/025L6z8CljOg== + dependencies: + "@typescript-eslint/scope-manager" "7.11.0" + "@typescript-eslint/types" "7.11.0" + "@typescript-eslint/typescript-estree" "7.11.0" + "@typescript-eslint/visitor-keys" "7.11.0" + debug "^4.3.4" + +"@typescript-eslint/scope-manager@7.11.0": + version "7.11.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-7.11.0.tgz#cf5619b01de62a226a59add15a02bde457335d1d" + integrity sha512-27tGdVEiutD4POirLZX4YzT180vevUURJl4wJGmm6TrQoiYwuxTIY98PBp6L2oN+JQxzE0URvYlzJaBHIekXAw== + dependencies: + "@typescript-eslint/types" "7.11.0" + "@typescript-eslint/visitor-keys" "7.11.0" + +"@typescript-eslint/type-utils@7.11.0": + version "7.11.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-7.11.0.tgz#ac216697d649084fedf4a910347b9642bd0ff099" + integrity sha512-WmppUEgYy+y1NTseNMJ6mCFxt03/7jTOy08bcg7bxJJdsM4nuhnchyBbE8vryveaJUf62noH7LodPSo5Z0WUCg== + dependencies: + "@typescript-eslint/typescript-estree" "7.11.0" + "@typescript-eslint/utils" "7.11.0" + debug "^4.3.4" + ts-api-utils "^1.3.0" + +"@typescript-eslint/types@7.11.0": + version "7.11.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-7.11.0.tgz#5e9702a5e8b424b7fc690e338d359939257d6722" + integrity sha512-MPEsDRZTyCiXkD4vd3zywDCifi7tatc4K37KqTprCvaXptP7Xlpdw0NR2hRJTetG5TxbWDB79Ys4kLmHliEo/w== + +"@typescript-eslint/typescript-estree@7.11.0": + version "7.11.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-7.11.0.tgz#7cbc569bc7336c3a494ceaf8204fdee5d5dbb7fa" + integrity sha512-cxkhZ2C/iyi3/6U9EPc5y+a6csqHItndvN/CzbNXTNrsC3/ASoYQZEt9uMaEp+xFNjasqQyszp5TumAVKKvJeQ== + dependencies: + "@typescript-eslint/types" "7.11.0" + "@typescript-eslint/visitor-keys" "7.11.0" + debug "^4.3.4" + globby "^11.1.0" + is-glob "^4.0.3" + minimatch "^9.0.4" + semver "^7.6.0" + ts-api-utils "^1.3.0" + +"@typescript-eslint/utils@7.11.0": + version "7.11.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-7.11.0.tgz#524f047f2209959424c3ef689b0d83b3bc09919c" + integrity sha512-xlAWwPleNRHwF37AhrZurOxA1wyXowW4PqVXZVUNCLjB48CqdPJoJWkrpH2nij9Q3Lb7rtWindtoXwxjxlKKCA== + dependencies: + "@eslint-community/eslint-utils" "^4.4.0" + "@typescript-eslint/scope-manager" "7.11.0" + "@typescript-eslint/types" "7.11.0" + "@typescript-eslint/typescript-estree" "7.11.0" + +"@typescript-eslint/visitor-keys@7.11.0": + version "7.11.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-7.11.0.tgz#2c50cd292e67645eec05ac0830757071b4a4d597" + integrity sha512-7syYk4MzjxTEk0g/w3iqtgxnFQspDJfn6QKD36xMuuhTzjcxY7F8EmBLnALjVyaOF1/bVocu3bS/2/F7rXrveQ== + dependencies: + "@typescript-eslint/types" "7.11.0" + eslint-visitor-keys "^3.4.3" + +"@ungap/structured-clone@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406" + integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== + +"@vitejs/plugin-react@^4.2": + version "4.3.0" + resolved "https://registry.yarnpkg.com/@vitejs/plugin-react/-/plugin-react-4.3.0.tgz#f20ec2369a92d8abaaefa60da8b7157819d20481" + integrity sha512-KcEbMsn4Dpk+LIbHMj7gDPRKaTMStxxWRkRmxsg/jVdFdJCZWt1SchZcf0M4t8lIKdwwMsEyzhrcOXRrDPtOBw== + dependencies: + "@babel/core" "^7.24.5" + "@babel/plugin-transform-react-jsx-self" "^7.24.5" + "@babel/plugin-transform-react-jsx-source" "^7.24.1" + "@types/babel__core" "^7.20.5" + react-refresh "^0.14.2" + +acorn-jsx@^5.3.2: + version "5.3.2" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" + integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== + +acorn@^8.9.0: + version "8.11.3" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.3.tgz#71e0b14e13a4ec160724b38fb7b0f233b1b81d7a" + integrity sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg== + +ajv@^6.12.4: + version "6.12.6" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" + integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + +ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== + dependencies: + color-convert "^1.9.0" + +ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== + +array-buffer-byte-length@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz#1e5583ec16763540a27ae52eed99ff899223568f" + integrity sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg== + dependencies: + call-bind "^1.0.5" + is-array-buffer "^3.0.4" + +array-includes@^3.1.6, array-includes@^3.1.8: + version "3.1.8" + resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.8.tgz#5e370cbe172fdd5dd6530c1d4aadda25281ba97d" + integrity sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.2" + es-object-atoms "^1.0.0" + get-intrinsic "^1.2.4" + is-string "^1.0.7" + +array-union@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" + integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== + +array.prototype.findlast@^1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz#3e4fbcb30a15a7f5bf64cf2faae22d139c2e4904" + integrity sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.2" + es-errors "^1.3.0" + es-object-atoms "^1.0.0" + es-shim-unscopables "^1.0.2" + +array.prototype.flat@^1.3.1: + version "1.3.2" + resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz#1476217df8cff17d72ee8f3ba06738db5b387d18" + integrity sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + es-shim-unscopables "^1.0.0" + +array.prototype.flatmap@^1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz#c9a7c6831db8e719d6ce639190146c24bbd3e527" + integrity sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + es-shim-unscopables "^1.0.0" + +array.prototype.toreversed@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/array.prototype.toreversed/-/array.prototype.toreversed-1.1.2.tgz#b989a6bf35c4c5051e1dc0325151bf8088954eba" + integrity sha512-wwDCoT4Ck4Cz7sLtgUmzR5UV3YF5mFHUlbChCzZBQZ+0m2cl/DH3tKgvphv1nKgFsJ48oCSg6p91q2Vm0I/ZMA== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + es-shim-unscopables "^1.0.0" + +array.prototype.tosorted@^1.1.3: + version "1.1.4" + resolved "https://registry.yarnpkg.com/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz#fe954678ff53034e717ea3352a03f0b0b86f7ffc" + integrity sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.3" + es-errors "^1.3.0" + es-shim-unscopables "^1.0.2" + +arraybuffer.prototype.slice@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz#097972f4255e41bc3425e37dc3f6421cf9aefde6" + integrity sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A== + dependencies: + array-buffer-byte-length "^1.0.1" + call-bind "^1.0.5" + define-properties "^1.2.1" + es-abstract "^1.22.3" + es-errors "^1.2.1" + get-intrinsic "^1.2.3" + is-array-buffer "^3.0.4" + is-shared-array-buffer "^1.0.2" + +available-typed-arrays@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz#a5cc375d6a03c2efc87a553f3e0b1522def14846" + integrity sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ== + dependencies: + possible-typed-array-names "^1.0.0" + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +brace-expansion@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" + integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== + dependencies: + balanced-match "^1.0.0" + +braces@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" + integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== + dependencies: + fill-range "^7.1.1" + +browserslist@^4.22.2: + version "4.23.0" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.23.0.tgz#8f3acc2bbe73af7213399430890f86c63a5674ab" + integrity sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ== + dependencies: + caniuse-lite "^1.0.30001587" + electron-to-chromium "^1.4.668" + node-releases "^2.0.14" + update-browserslist-db "^1.0.13" + +call-bind@^1.0.2, call-bind@^1.0.5, call-bind@^1.0.6, call-bind@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9" + integrity sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + set-function-length "^1.2.1" + +callsites@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" + integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== + +caniuse-lite@^1.0.30001587: + version "1.0.30001627" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001627.tgz#8071c42d468e06ed2fb2c545efe79a663fd326ab" + integrity sha512-4zgNiB8nTyV/tHhwZrFs88ryjls/lHiqFhrxCW4qSTeuRByBVnPYpDInchOIySWknznucaf31Z4KYqjfbrecVw== + +chalk@^2.4.2: + version "2.4.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + +chalk@^4.0.0: + version "4.1.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +color-convert@^1.9.0: + version "1.9.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== + +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== + +convert-source-map@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" + integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== + +cross-spawn@^7.0.2: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" + integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +csstype@^3.0.2: + version "3.1.3" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81" + integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw== + +data-view-buffer@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/data-view-buffer/-/data-view-buffer-1.0.1.tgz#8ea6326efec17a2e42620696e671d7d5a8bc66b2" + integrity sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA== + dependencies: + call-bind "^1.0.6" + es-errors "^1.3.0" + is-data-view "^1.0.1" + +data-view-byte-length@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz#90721ca95ff280677eb793749fce1011347669e2" + integrity sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ== + dependencies: + call-bind "^1.0.7" + es-errors "^1.3.0" + is-data-view "^1.0.1" + +data-view-byte-offset@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz#5e0bbfb4828ed2d1b9b400cd8a7d119bca0ff18a" + integrity sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA== + dependencies: + call-bind "^1.0.6" + es-errors "^1.3.0" + is-data-view "^1.0.1" + +debug@^4.1.0, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4: + version "4.3.5" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.5.tgz#e83444eceb9fedd4a1da56d671ae2446a01a6e1e" + integrity sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg== + dependencies: + ms "2.1.2" + +deep-is@^0.1.3: + version "0.1.4" + resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" + integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== + +define-data-property@^1.0.1, define-data-property@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" + integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + gopd "^1.0.1" + +define-properties@^1.2.0, define-properties@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.1.tgz#10781cc616eb951a80a034bafcaa7377f6af2b6c" + integrity sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg== + dependencies: + define-data-property "^1.0.1" + has-property-descriptors "^1.0.0" + object-keys "^1.1.1" + +detect-indent@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-7.0.1.tgz#cbb060a12842b9c4d333f1cac4aa4da1bb66bc25" + integrity sha512-Mc7QhQ8s+cLrnUfU/Ji94vG/r8M26m8f++vyres4ZoojaRDpZ1eSIh/EpzLNwlWuvzSZ3UbDFspjFvTDXe6e/g== + +detect-newline@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-4.0.1.tgz#fcefdb5713e1fb8cb2839b8b6ee22e6716ab8f23" + integrity sha512-qE3Veg1YXzGHQhlA6jzebZN2qVf6NX+A7m7qlhCGG30dJixrAQhYOsJjsnBjJkCSmuOPpCk30145fr8FV0bzog== + +dir-glob@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" + integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== + dependencies: + path-type "^4.0.0" + +doctrine@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d" + integrity sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw== + dependencies: + esutils "^2.0.2" + +doctrine@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" + integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== + dependencies: + esutils "^2.0.2" + +electron-to-chromium@^1.4.668: + version "1.4.788" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.788.tgz#a3545959d5cfa0a266d3e551386c040be34e7e06" + integrity sha512-ubp5+Ev/VV8KuRoWnfP2QF2Bg+O2ZFdb49DiiNbz2VmgkIqrnyYaqIOqj8A6K/3p1xV0QcU5hBQ1+BmB6ot1OA== + +es-abstract@^1.22.1, es-abstract@^1.22.3, es-abstract@^1.23.0, es-abstract@^1.23.1, es-abstract@^1.23.2, es-abstract@^1.23.3: + version "1.23.3" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.23.3.tgz#8f0c5a35cd215312573c5a27c87dfd6c881a0aa0" + integrity sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A== + dependencies: + array-buffer-byte-length "^1.0.1" + arraybuffer.prototype.slice "^1.0.3" + available-typed-arrays "^1.0.7" + call-bind "^1.0.7" + data-view-buffer "^1.0.1" + data-view-byte-length "^1.0.1" + data-view-byte-offset "^1.0.0" + es-define-property "^1.0.0" + es-errors "^1.3.0" + es-object-atoms "^1.0.0" + es-set-tostringtag "^2.0.3" + es-to-primitive "^1.2.1" + function.prototype.name "^1.1.6" + get-intrinsic "^1.2.4" + get-symbol-description "^1.0.2" + globalthis "^1.0.3" + gopd "^1.0.1" + has-property-descriptors "^1.0.2" + has-proto "^1.0.3" + has-symbols "^1.0.3" + hasown "^2.0.2" + internal-slot "^1.0.7" + is-array-buffer "^3.0.4" + is-callable "^1.2.7" + is-data-view "^1.0.1" + is-negative-zero "^2.0.3" + is-regex "^1.1.4" + is-shared-array-buffer "^1.0.3" + is-string "^1.0.7" + is-typed-array "^1.1.13" + is-weakref "^1.0.2" + object-inspect "^1.13.1" + object-keys "^1.1.1" + object.assign "^4.1.5" + regexp.prototype.flags "^1.5.2" + safe-array-concat "^1.1.2" + safe-regex-test "^1.0.3" + string.prototype.trim "^1.2.9" + string.prototype.trimend "^1.0.8" + string.prototype.trimstart "^1.0.8" + typed-array-buffer "^1.0.2" + typed-array-byte-length "^1.0.1" + typed-array-byte-offset "^1.0.2" + typed-array-length "^1.0.6" + unbox-primitive "^1.0.2" + which-typed-array "^1.1.15" + +es-define-property@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.0.tgz#c7faefbdff8b2696cf5f46921edfb77cc4ba3845" + integrity sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ== + dependencies: + get-intrinsic "^1.2.4" + +es-errors@^1.2.1, es-errors@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" + integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== + +es-iterator-helpers@^1.0.19: + version "1.0.19" + resolved "https://registry.yarnpkg.com/es-iterator-helpers/-/es-iterator-helpers-1.0.19.tgz#117003d0e5fec237b4b5c08aded722e0c6d50ca8" + integrity sha512-zoMwbCcH5hwUkKJkT8kDIBZSz9I6mVG//+lDCinLCGov4+r7NIy0ld8o03M0cJxl2spVf6ESYVS6/gpIfq1FFw== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.3" + es-errors "^1.3.0" + es-set-tostringtag "^2.0.3" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + globalthis "^1.0.3" + has-property-descriptors "^1.0.2" + has-proto "^1.0.3" + has-symbols "^1.0.3" + internal-slot "^1.0.7" + iterator.prototype "^1.1.2" + safe-array-concat "^1.1.2" + +es-object-atoms@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.0.0.tgz#ddb55cd47ac2e240701260bc2a8e31ecb643d941" + integrity sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw== + dependencies: + es-errors "^1.3.0" + +es-set-tostringtag@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz#8bb60f0a440c2e4281962428438d58545af39777" + integrity sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ== + dependencies: + get-intrinsic "^1.2.4" + has-tostringtag "^1.0.2" + hasown "^2.0.1" + +es-shim-unscopables@^1.0.0, es-shim-unscopables@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz#1f6942e71ecc7835ed1c8a83006d8771a63a3763" + integrity sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw== + dependencies: + hasown "^2.0.0" + +es-to-primitive@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" + integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA== + dependencies: + is-callable "^1.1.4" + is-date-object "^1.0.1" + is-symbol "^1.0.2" + +esbuild@^0.20.1: + version "0.20.2" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.20.2.tgz#9d6b2386561766ee6b5a55196c6d766d28c87ea1" + integrity sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g== + optionalDependencies: + "@esbuild/aix-ppc64" "0.20.2" + "@esbuild/android-arm" "0.20.2" + "@esbuild/android-arm64" "0.20.2" + "@esbuild/android-x64" "0.20.2" + "@esbuild/darwin-arm64" "0.20.2" + "@esbuild/darwin-x64" "0.20.2" + "@esbuild/freebsd-arm64" "0.20.2" + "@esbuild/freebsd-x64" "0.20.2" + "@esbuild/linux-arm" "0.20.2" + "@esbuild/linux-arm64" "0.20.2" + "@esbuild/linux-ia32" "0.20.2" + "@esbuild/linux-loong64" "0.20.2" + "@esbuild/linux-mips64el" "0.20.2" + "@esbuild/linux-ppc64" "0.20.2" + "@esbuild/linux-riscv64" "0.20.2" + "@esbuild/linux-s390x" "0.20.2" + "@esbuild/linux-x64" "0.20.2" + "@esbuild/netbsd-x64" "0.20.2" + "@esbuild/openbsd-x64" "0.20.2" + "@esbuild/sunos-x64" "0.20.2" + "@esbuild/win32-arm64" "0.20.2" + "@esbuild/win32-ia32" "0.20.2" + "@esbuild/win32-x64" "0.20.2" + +escalade@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.2.tgz#54076e9ab29ea5bf3d8f1ed62acffbb88272df27" + integrity sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA== + +escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== + +escape-string-regexp@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + +eslint-plugin-react-hooks@^4.6: + version "4.6.2" + resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz#c829eb06c0e6f484b3fbb85a97e57784f328c596" + integrity sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ== + +eslint-plugin-react-refresh@^0.4.7: + version "0.4.7" + resolved "https://registry.yarnpkg.com/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.7.tgz#1f597f9093b254f10ee0961c139a749acb19af7d" + integrity sha512-yrj+KInFmwuQS2UQcg1SF83ha1tuHC1jMQbRNyuWtlEzzKRDgAl7L4Yp4NlDUZTZNlWvHEzOtJhMi40R7JxcSw== + +eslint-plugin-react@^7.34: + version "7.34.2" + resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.34.2.tgz#2780a1a35a51aca379d86d29b9a72adc6bfe6b66" + integrity sha512-2HCmrU+/JNigDN6tg55cRDKCQWicYAPB38JGSFDQt95jDm8rrvSUo7YPkOIm5l6ts1j1zCvysNcasvfTMQzUOw== + dependencies: + array-includes "^3.1.8" + array.prototype.findlast "^1.2.5" + array.prototype.flatmap "^1.3.2" + array.prototype.toreversed "^1.1.2" + array.prototype.tosorted "^1.1.3" + doctrine "^2.1.0" + es-iterator-helpers "^1.0.19" + estraverse "^5.3.0" + jsx-ast-utils "^2.4.1 || ^3.0.0" + minimatch "^3.1.2" + object.entries "^1.1.8" + object.fromentries "^2.0.8" + object.hasown "^1.1.4" + object.values "^1.2.0" + prop-types "^15.8.1" + resolve "^2.0.0-next.5" + semver "^6.3.1" + string.prototype.matchall "^4.0.11" + +eslint-scope@^7.2.2: + version "7.2.2" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.2.2.tgz#deb4f92563390f32006894af62a22dba1c46423f" + integrity sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg== + dependencies: + esrecurse "^4.3.0" + estraverse "^5.2.0" + +eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4.3: + version "3.4.3" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800" + integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== + +eslint@^8: + version "8.57.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.57.0.tgz#c786a6fd0e0b68941aaf624596fb987089195668" + integrity sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ== + dependencies: + "@eslint-community/eslint-utils" "^4.2.0" + "@eslint-community/regexpp" "^4.6.1" + "@eslint/eslintrc" "^2.1.4" + "@eslint/js" "8.57.0" + "@humanwhocodes/config-array" "^0.11.14" + "@humanwhocodes/module-importer" "^1.0.1" + "@nodelib/fs.walk" "^1.2.8" + "@ungap/structured-clone" "^1.2.0" + ajv "^6.12.4" + chalk "^4.0.0" + cross-spawn "^7.0.2" + debug "^4.3.2" + doctrine "^3.0.0" + escape-string-regexp "^4.0.0" + eslint-scope "^7.2.2" + eslint-visitor-keys "^3.4.3" + espree "^9.6.1" + esquery "^1.4.2" + esutils "^2.0.2" + fast-deep-equal "^3.1.3" + file-entry-cache "^6.0.1" + find-up "^5.0.0" + glob-parent "^6.0.2" + globals "^13.19.0" + graphemer "^1.4.0" + ignore "^5.2.0" + imurmurhash "^0.1.4" + is-glob "^4.0.0" + is-path-inside "^3.0.3" + js-yaml "^4.1.0" + json-stable-stringify-without-jsonify "^1.0.1" + levn "^0.4.1" + lodash.merge "^4.6.2" + minimatch "^3.1.2" + natural-compare "^1.4.0" + optionator "^0.9.3" + strip-ansi "^6.0.1" + text-table "^0.2.0" + +espree@^9.6.0, espree@^9.6.1: + version "9.6.1" + resolved "https://registry.yarnpkg.com/espree/-/espree-9.6.1.tgz#a2a17b8e434690a5432f2f8018ce71d331a48c6f" + integrity sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ== + dependencies: + acorn "^8.9.0" + acorn-jsx "^5.3.2" + eslint-visitor-keys "^3.4.1" + +esquery@^1.4.2: + version "1.5.0" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.5.0.tgz#6ce17738de8577694edd7361c57182ac8cb0db0b" + integrity sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg== + dependencies: + estraverse "^5.1.0" + +esrecurse@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" + integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== + dependencies: + estraverse "^5.2.0" + +estraverse@^5.1.0, estraverse@^5.2.0, estraverse@^5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" + integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== + +esutils@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" + integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== + +fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-glob@^3.2.9, fast-glob@^3.3.0: + version "3.3.2" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129" + integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.2" + merge2 "^1.3.0" + micromatch "^4.0.4" + +fast-json-stable-stringify@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +fast-levenshtein@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" + integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== + +fastq@^1.6.0: + version "1.17.1" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.17.1.tgz#2a523f07a4e7b1e81a42b91b8bf2254107753b47" + integrity sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w== + dependencies: + reusify "^1.0.4" + +file-entry-cache@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" + integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg== + dependencies: + flat-cache "^3.0.4" + +fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== + dependencies: + to-regex-range "^5.0.1" + +find-up@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" + integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== + dependencies: + locate-path "^6.0.0" + path-exists "^4.0.0" + +flat-cache@^3.0.4: + version "3.2.0" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.2.0.tgz#2c0c2d5040c99b1632771a9d105725c0115363ee" + integrity sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw== + dependencies: + flatted "^3.2.9" + keyv "^4.5.3" + rimraf "^3.0.2" + +flatted@^3.2.9: + version "3.3.1" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.1.tgz#21db470729a6734d4997002f439cb308987f567a" + integrity sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw== + +for-each@^0.3.3: + version "0.3.3" + resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e" + integrity sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw== + dependencies: + is-callable "^1.1.3" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== + +fsevents@~2.3.2, fsevents@~2.3.3: + version "2.3.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== + +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + +function.prototype.name@^1.1.5, function.prototype.name@^1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.6.tgz#cdf315b7d90ee77a4c6ee216c3c3362da07533fd" + integrity sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + functions-have-names "^1.2.3" + +functions-have-names@^1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834" + integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== + +gensync@^1.0.0-beta.2: + version "1.0.0-beta.2" + resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" + integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== + +get-intrinsic@^1.1.3, get-intrinsic@^1.2.1, get-intrinsic@^1.2.3, get-intrinsic@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd" + integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ== + dependencies: + es-errors "^1.3.0" + function-bind "^1.1.2" + has-proto "^1.0.1" + has-symbols "^1.0.3" + hasown "^2.0.0" + +get-stdin@^9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-9.0.0.tgz#3983ff82e03d56f1b2ea0d3e60325f39d703a575" + integrity sha512-dVKBjfWisLAicarI2Sf+JuBE/DghV4UzNAVe9yhEJuzeREd3JhOTE9cUaJTeSa77fsbQUK3pcOpJfM59+VKZaA== + +get-symbol-description@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.2.tgz#533744d5aa20aca4e079c8e5daf7fd44202821f5" + integrity sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg== + dependencies: + call-bind "^1.0.5" + es-errors "^1.3.0" + get-intrinsic "^1.2.4" + +git-hooks-list@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/git-hooks-list/-/git-hooks-list-3.1.0.tgz#386dc531dcc17474cf094743ff30987a3d3e70fc" + integrity sha512-LF8VeHeR7v+wAbXqfgRlTSX/1BJR9Q1vEMR8JAz1cEg6GX07+zyj3sAdDvYjj/xnlIfVuGgj4qBei1K3hKH+PA== + +glob-parent@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + +glob-parent@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" + integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== + dependencies: + is-glob "^4.0.3" + +glob@^7.1.3: + version "7.2.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" + integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.1.1" + once "^1.3.0" + path-is-absolute "^1.0.0" + +globals@^11.1.0: + version "11.12.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" + integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== + +globals@^13.19.0: + version "13.24.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-13.24.0.tgz#8432a19d78ce0c1e833949c36adb345400bb1171" + integrity sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ== + dependencies: + type-fest "^0.20.2" + +globalthis@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.4.tgz#7430ed3a975d97bfb59bcce41f5cabbafa651236" + integrity sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ== + dependencies: + define-properties "^1.2.1" + gopd "^1.0.1" + +globby@^11.1.0: + version "11.1.0" + resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" + integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== + dependencies: + array-union "^2.1.0" + dir-glob "^3.0.1" + fast-glob "^3.2.9" + ignore "^5.2.0" + merge2 "^1.4.1" + slash "^3.0.0" + +globby@^13.1.2: + version "13.2.2" + resolved "https://registry.yarnpkg.com/globby/-/globby-13.2.2.tgz#63b90b1bf68619c2135475cbd4e71e66aa090592" + integrity sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w== + dependencies: + dir-glob "^3.0.1" + fast-glob "^3.3.0" + ignore "^5.2.4" + merge2 "^1.4.1" + slash "^4.0.0" + +gopd@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" + integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA== + dependencies: + get-intrinsic "^1.1.3" + +graphemer@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" + integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== + +has-bigints@^1.0.1, has-bigints@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa" + integrity sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ== + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw== + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +has-property-descriptors@^1.0.0, has-property-descriptors@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854" + integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== + dependencies: + es-define-property "^1.0.0" + +has-proto@^1.0.1, has-proto@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.3.tgz#b31ddfe9b0e6e9914536a6ab286426d0214f77fd" + integrity sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q== + +has-symbols@^1.0.2, has-symbols@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" + integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== + +has-tostringtag@^1.0.0, has-tostringtag@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc" + integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw== + dependencies: + has-symbols "^1.0.3" + +hasown@^2.0.0, hasown@^2.0.1, hasown@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" + integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== + dependencies: + function-bind "^1.1.2" + +ignore@^5.2.0, ignore@^5.2.4, ignore@^5.3.1: + version "5.3.1" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.1.tgz#5073e554cd42c5b33b394375f538b8593e34d4ef" + integrity sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw== + +import-fresh@^3.2.1: + version "3.3.0" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" + integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== + dependencies: + parent-module "^1.0.0" + resolve-from "^4.0.0" + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +internal-slot@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.7.tgz#c06dcca3ed874249881007b0a5523b172a190802" + integrity sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g== + dependencies: + es-errors "^1.3.0" + hasown "^2.0.0" + side-channel "^1.0.4" + +is-array-buffer@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.4.tgz#7a1f92b3d61edd2bc65d24f130530ea93d7fae98" + integrity sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.2.1" + +is-async-function@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-async-function/-/is-async-function-2.0.0.tgz#8e4418efd3e5d3a6ebb0164c05ef5afb69aa9646" + integrity sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA== + dependencies: + has-tostringtag "^1.0.0" + +is-bigint@^1.0.1: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.4.tgz#08147a1875bc2b32005d41ccd8291dffc6691df3" + integrity sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg== + dependencies: + has-bigints "^1.0.1" + +is-boolean-object@^1.1.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.1.2.tgz#5c6dc200246dd9321ae4b885a114bb1f75f63719" + integrity sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA== + dependencies: + call-bind "^1.0.2" + has-tostringtag "^1.0.0" + +is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.2.7: + version "1.2.7" + resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" + integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== + +is-core-module@^2.13.0: + version "2.13.1" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.13.1.tgz#ad0d7532c6fea9da1ebdc82742d74525c6273384" + integrity sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw== + dependencies: + hasown "^2.0.0" + +is-data-view@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-data-view/-/is-data-view-1.0.1.tgz#4b4d3a511b70f3dc26d42c03ca9ca515d847759f" + integrity sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w== + dependencies: + is-typed-array "^1.1.13" + +is-date-object@^1.0.1, is-date-object@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.5.tgz#0841d5536e724c25597bf6ea62e1bd38298df31f" + integrity sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ== + dependencies: + has-tostringtag "^1.0.0" + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== + +is-finalizationregistry@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-finalizationregistry/-/is-finalizationregistry-1.0.2.tgz#c8749b65f17c133313e661b1289b95ad3dbd62e6" + integrity sha512-0by5vtUJs8iFQb5TYUHHPudOR+qXYIMKtiUzvLIZITZUjknFmziyBJuLhVRc+Ds0dREFlskDNJKYIdIzu/9pfw== + dependencies: + call-bind "^1.0.2" + +is-generator-function@^1.0.10: + version "1.0.10" + resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.10.tgz#f1558baf1ac17e0deea7c0415c438351ff2b3c72" + integrity sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A== + dependencies: + has-tostringtag "^1.0.0" + +is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +is-map@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.3.tgz#ede96b7fe1e270b3c4465e3a465658764926d62e" + integrity sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw== + +is-negative-zero@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.3.tgz#ced903a027aca6381b777a5743069d7376a49747" + integrity sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw== + +is-number-object@^1.0.4: + version "1.0.7" + resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.7.tgz#59d50ada4c45251784e9904f5246c742f07a42fc" + integrity sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ== + dependencies: + has-tostringtag "^1.0.0" + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +is-path-inside@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" + integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== + +is-plain-obj@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-4.1.0.tgz#d65025edec3657ce032fd7db63c97883eaed71f0" + integrity sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg== + +is-regex@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958" + integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg== + dependencies: + call-bind "^1.0.2" + has-tostringtag "^1.0.0" + +is-set@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.3.tgz#8ab209ea424608141372ded6e0cb200ef1d9d01d" + integrity sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg== + +is-shared-array-buffer@^1.0.2, is-shared-array-buffer@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz#1237f1cba059cdb62431d378dcc37d9680181688" + integrity sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg== + dependencies: + call-bind "^1.0.7" + +is-string@^1.0.5, is-string@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.7.tgz#0dd12bf2006f255bb58f695110eff7491eebc0fd" + integrity sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg== + dependencies: + has-tostringtag "^1.0.0" + +is-symbol@^1.0.2, is-symbol@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.4.tgz#a6dac93b635b063ca6872236de88910a57af139c" + integrity sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg== + dependencies: + has-symbols "^1.0.2" + +is-typed-array@^1.1.13: + version "1.1.13" + resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.13.tgz#d6c5ca56df62334959322d7d7dd1cca50debe229" + integrity sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw== + dependencies: + which-typed-array "^1.1.14" + +is-weakmap@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/is-weakmap/-/is-weakmap-2.0.2.tgz#bf72615d649dfe5f699079c54b83e47d1ae19cfd" + integrity sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w== + +is-weakref@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.0.2.tgz#9529f383a9338205e89765e0392efc2f100f06f2" + integrity sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ== + dependencies: + call-bind "^1.0.2" + +is-weakset@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/is-weakset/-/is-weakset-2.0.3.tgz#e801519df8c0c43e12ff2834eead84ec9e624007" + integrity sha512-LvIm3/KWzS9oRFHugab7d+M/GcBXuXX5xZkzPmN+NxihdQlZUQ4dWuSV1xR/sq6upL1TJEDrfBgRepHFdBtSNQ== + dependencies: + call-bind "^1.0.7" + get-intrinsic "^1.2.4" + +isarray@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723" + integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw== + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== + +iterator.prototype@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/iterator.prototype/-/iterator.prototype-1.1.2.tgz#5e29c8924f01916cb9335f1ff80619dcff22b0c0" + integrity sha512-DR33HMMr8EzwuRL8Y9D3u2BMj8+RqSE850jfGu59kS7tbmPLzGkZmVSfyCFSDxuZiEY6Rzt3T2NA/qU+NwVj1w== + dependencies: + define-properties "^1.2.1" + get-intrinsic "^1.2.1" + has-symbols "^1.0.3" + reflect.getprototypeof "^1.0.4" + set-function-name "^2.0.1" + +"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + +js-yaml@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" + integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== + dependencies: + argparse "^2.0.1" + +jsesc@^2.5.1: + version "2.5.2" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" + integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== + +json-buffer@3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" + integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-stable-stringify-without-jsonify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" + integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== + +json5@^2.2.3: + version "2.2.3" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" + integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== + +"jsx-ast-utils@^2.4.1 || ^3.0.0": + version "3.3.5" + resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz#4766bd05a8e2a11af222becd19e15575e52a853a" + integrity sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ== + dependencies: + array-includes "^3.1.6" + array.prototype.flat "^1.3.1" + object.assign "^4.1.4" + object.values "^1.1.6" + +keyv@^4.5.3: + version "4.5.4" + resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" + integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw== + dependencies: + json-buffer "3.0.1" + +levn@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" + integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ== + dependencies: + prelude-ls "^1.2.1" + type-check "~0.4.0" + +locate-path@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" + integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== + dependencies: + p-locate "^5.0.0" + +lodash.merge@^4.6.2: + version "4.6.2" + resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" + integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== + +loose-envify@^1.1.0, loose-envify@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" + integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== + dependencies: + js-tokens "^3.0.0 || ^4.0.0" + +lru-cache@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" + integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== + dependencies: + yallist "^3.0.2" + +merge2@^1.3.0, merge2@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" + integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== + +micromatch@^4.0.4: + version "4.0.7" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.7.tgz#33e8190d9fe474a9895525f5618eee136d46c2e5" + integrity sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q== + dependencies: + braces "^3.0.3" + picomatch "^2.3.1" + +minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + dependencies: + brace-expansion "^1.1.7" + +minimatch@^9.0.4: + version "9.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.4.tgz#8e49c731d1749cbec05050ee5145147b32496a51" + integrity sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw== + dependencies: + brace-expansion "^2.0.1" + +ms@2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +nanoid@^3.3.7: + version "3.3.7" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8" + integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g== + +natural-compare@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" + integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== + +node-releases@^2.0.14: + version "2.0.14" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.14.tgz#2ffb053bceb8b2be8495ece1ab6ce600c4461b0b" + integrity sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw== + +object-assign@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== + +object-inspect@^1.13.1: + version "1.13.1" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.1.tgz#b96c6109324ccfef6b12216a956ca4dc2ff94bc2" + integrity sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ== + +object-keys@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" + integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== + +object.assign@^4.1.4, object.assign@^4.1.5: + version "4.1.5" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.5.tgz#3a833f9ab7fdb80fc9e8d2300c803d216d8fdbb0" + integrity sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ== + dependencies: + call-bind "^1.0.5" + define-properties "^1.2.1" + has-symbols "^1.0.3" + object-keys "^1.1.1" + +object.entries@^1.1.8: + version "1.1.8" + resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.8.tgz#bffe6f282e01f4d17807204a24f8edd823599c41" + integrity sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-object-atoms "^1.0.0" + +object.fromentries@^2.0.8: + version "2.0.8" + resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.8.tgz#f7195d8a9b97bd95cbc1999ea939ecd1a2b00c65" + integrity sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.2" + es-object-atoms "^1.0.0" + +object.hasown@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/object.hasown/-/object.hasown-1.1.4.tgz#e270ae377e4c120cdcb7656ce66884a6218283dc" + integrity sha512-FZ9LZt9/RHzGySlBARE3VF+gE26TxR38SdmqOqliuTnl9wrKulaQs+4dee1V+Io8VfxqzAfHu6YuRgUy8OHoTg== + dependencies: + define-properties "^1.2.1" + es-abstract "^1.23.2" + es-object-atoms "^1.0.0" + +object.values@^1.1.6, object.values@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.2.0.tgz#65405a9d92cee68ac2d303002e0b8470a4d9ab1b" + integrity sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-object-atoms "^1.0.0" + +once@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== + dependencies: + wrappy "1" + +optionator@^0.9.3: + version "0.9.4" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.4.tgz#7ea1c1a5d91d764fb282139c88fe11e182a3a734" + integrity sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g== + dependencies: + deep-is "^0.1.3" + fast-levenshtein "^2.0.6" + levn "^0.4.1" + prelude-ls "^1.2.1" + type-check "^0.4.0" + word-wrap "^1.2.5" + +p-limit@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== + dependencies: + yocto-queue "^0.1.0" + +p-locate@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" + integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== + dependencies: + p-limit "^3.0.2" + +parent-module@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" + integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== + dependencies: + callsites "^3.0.0" + +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== + +path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + +path-parse@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== + +path-type@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" + integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== + +picocolors@^1.0.0, picocolors@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.1.tgz#a8ad579b571952f0e5d25892de5445bcfe25aaa1" + integrity sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew== + +picomatch@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + +possible-typed-array-names@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz#89bb63c6fada2c3e90adc4a647beeeb39cc7bf8f" + integrity sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q== + +postcss@^8.4.38: + version "8.4.38" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.38.tgz#b387d533baf2054288e337066d81c6bee9db9e0e" + integrity sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A== + dependencies: + nanoid "^3.3.7" + picocolors "^1.0.0" + source-map-js "^1.2.0" + +prelude-ls@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" + integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== + +prettier-plugin-organize-imports@^3.2: + version "3.2.4" + resolved "https://registry.yarnpkg.com/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-3.2.4.tgz#77967f69d335e9c8e6e5d224074609309c62845e" + integrity sha512-6m8WBhIp0dfwu0SkgfOxJqh+HpdyfqSSLfKKRZSFbDuEQXDDndb8fTpRWkUrX/uBenkex3MgnVk0J3b3Y5byog== + +prettier-plugin-packagejson@^2.5: + version "2.5.0" + resolved "https://registry.yarnpkg.com/prettier-plugin-packagejson/-/prettier-plugin-packagejson-2.5.0.tgz#23d2cb8b1f7840702d35e3a5078e564ea0bc63e0" + integrity sha512-6XkH3rpin5QEQodBSVNg+rBo4r91g/1mCaRwS1YGdQJZ6jwqrg2UchBsIG9tpS1yK1kNBvOt84OILsX8uHzBGg== + dependencies: + sort-package-json "2.10.0" + synckit "0.9.0" + +prettier@^3: + version "3.3.0" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.3.0.tgz#d173ea0524a691d4c0b1181752f2b46724328cdf" + integrity sha512-J9odKxERhCQ10OC2yb93583f6UnYutOeiV5i0zEDS7UGTdUt0u+y8erxl3lBKvwo/JHyyoEdXjwp4dke9oyZ/g== + +prop-types@^15.8.1: + version "15.8.1" + resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" + integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== + dependencies: + loose-envify "^1.4.0" + object-assign "^4.1.1" + react-is "^16.13.1" + +punycode@^2.1.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" + integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== + +queue-microtask@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" + integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== + +react-dom@^18: + version "18.3.1" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.3.1.tgz#c2265d79511b57d479b3dd3fdfa51536494c5cb4" + integrity sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw== + dependencies: + loose-envify "^1.1.0" + scheduler "^0.23.2" + +react-is@^16.13.1: + version "16.13.1" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" + integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== + +react-refresh@^0.14.2: + version "0.14.2" + resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.14.2.tgz#3833da01ce32da470f1f936b9d477da5c7028bf9" + integrity sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA== + +react@^18: + version "18.3.1" + resolved "https://registry.yarnpkg.com/react/-/react-18.3.1.tgz#49ab892009c53933625bd16b2533fc754cab2891" + integrity sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ== + dependencies: + loose-envify "^1.1.0" + +reflect.getprototypeof@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz#3ab04c32a8390b770712b7a8633972702d278859" + integrity sha512-fmfw4XgoDke3kdI6h4xcUz1dG8uaiv5q9gcEwLS4Pnth2kxT+GZ7YehS1JTMGBQmtV7Y4GFGbs2re2NqhdozUg== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.1" + es-errors "^1.3.0" + get-intrinsic "^1.2.4" + globalthis "^1.0.3" + which-builtin-type "^1.1.3" + +regexp.prototype.flags@^1.5.2: + version "1.5.2" + resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz#138f644a3350f981a858c44f6bb1a61ff59be334" + integrity sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw== + dependencies: + call-bind "^1.0.6" + define-properties "^1.2.1" + es-errors "^1.3.0" + set-function-name "^2.0.1" + +resolve-from@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" + integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== + +resolve@^2.0.0-next.5: + version "2.0.0-next.5" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-2.0.0-next.5.tgz#6b0ec3107e671e52b68cd068ef327173b90dc03c" + integrity sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA== + dependencies: + is-core-module "^2.13.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + +reusify@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" + integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== + +rimraf@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" + integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== + dependencies: + glob "^7.1.3" + +rollup@^4.13.0: + version "4.18.0" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.18.0.tgz#497f60f0c5308e4602cf41136339fbf87d5f5dda" + integrity sha512-QmJz14PX3rzbJCN1SG4Xe/bAAX2a6NpCP8ab2vfu2GiUr8AQcr2nCV/oEO3yneFarB67zk8ShlIyWb2LGTb3Sg== + dependencies: + "@types/estree" "1.0.5" + optionalDependencies: + "@rollup/rollup-android-arm-eabi" "4.18.0" + "@rollup/rollup-android-arm64" "4.18.0" + "@rollup/rollup-darwin-arm64" "4.18.0" + "@rollup/rollup-darwin-x64" "4.18.0" + "@rollup/rollup-linux-arm-gnueabihf" "4.18.0" + "@rollup/rollup-linux-arm-musleabihf" "4.18.0" + "@rollup/rollup-linux-arm64-gnu" "4.18.0" + "@rollup/rollup-linux-arm64-musl" "4.18.0" + "@rollup/rollup-linux-powerpc64le-gnu" "4.18.0" + "@rollup/rollup-linux-riscv64-gnu" "4.18.0" + "@rollup/rollup-linux-s390x-gnu" "4.18.0" + "@rollup/rollup-linux-x64-gnu" "4.18.0" + "@rollup/rollup-linux-x64-musl" "4.18.0" + "@rollup/rollup-win32-arm64-msvc" "4.18.0" + "@rollup/rollup-win32-ia32-msvc" "4.18.0" + "@rollup/rollup-win32-x64-msvc" "4.18.0" + fsevents "~2.3.2" + +run-parallel@^1.1.9: + version "1.2.0" + resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" + integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== + dependencies: + queue-microtask "^1.2.2" + +safe-array-concat@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/safe-array-concat/-/safe-array-concat-1.1.2.tgz#81d77ee0c4e8b863635227c721278dd524c20edb" + integrity sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q== + dependencies: + call-bind "^1.0.7" + get-intrinsic "^1.2.4" + has-symbols "^1.0.3" + isarray "^2.0.5" + +safe-regex-test@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.0.3.tgz#a5b4c0f06e0ab50ea2c395c14d8371232924c377" + integrity sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw== + dependencies: + call-bind "^1.0.6" + es-errors "^1.3.0" + is-regex "^1.1.4" + +scheduler@^0.23.2: + version "0.23.2" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.2.tgz#414ba64a3b282892e944cf2108ecc078d115cdc3" + integrity sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ== + dependencies: + loose-envify "^1.1.0" + +semver@^6.3.1: + version "6.3.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" + integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== + +semver@^7.6.0: + version "7.6.2" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.2.tgz#1e3b34759f896e8f14d6134732ce798aeb0c6e13" + integrity sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w== + +set-function-length@^1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" + integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== + dependencies: + define-data-property "^1.1.4" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + gopd "^1.0.1" + has-property-descriptors "^1.0.2" + +set-function-name@^2.0.1, set-function-name@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/set-function-name/-/set-function-name-2.0.2.tgz#16a705c5a0dc2f5e638ca96d8a8cd4e1c2b90985" + integrity sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ== + dependencies: + define-data-property "^1.1.4" + es-errors "^1.3.0" + functions-have-names "^1.2.3" + has-property-descriptors "^1.0.2" + +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +side-channel@^1.0.4, side-channel@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2" + integrity sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA== + dependencies: + call-bind "^1.0.7" + es-errors "^1.3.0" + get-intrinsic "^1.2.4" + object-inspect "^1.13.1" + +slash@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" + integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== + +slash@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-4.0.0.tgz#2422372176c4c6c5addb5e2ada885af984b396a7" + integrity sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew== + +sort-object-keys@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/sort-object-keys/-/sort-object-keys-1.1.3.tgz#bff833fe85cab147b34742e45863453c1e190b45" + integrity sha512-855pvK+VkU7PaKYPc+Jjnmt4EzejQHyhhF33q31qG8x7maDzkeFhAAThdCYay11CISO+qAMwjOBP+fPZe0IPyg== + +sort-package-json@2.10.0: + version "2.10.0" + resolved "https://registry.yarnpkg.com/sort-package-json/-/sort-package-json-2.10.0.tgz#6be07424bf3b7db9fbb1bdd69e7945f301026d8a" + integrity sha512-MYecfvObMwJjjJskhxYfuOADkXp1ZMMnCFC8yhp+9HDsk7HhR336hd7eiBs96lTXfiqmUNI+WQCeCMRBhl251g== + dependencies: + detect-indent "^7.0.1" + detect-newline "^4.0.0" + get-stdin "^9.0.0" + git-hooks-list "^3.0.0" + globby "^13.1.2" + is-plain-obj "^4.1.0" + semver "^7.6.0" + sort-object-keys "^1.1.3" + +source-map-js@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.0.tgz#16b809c162517b5b8c3e7dcd315a2a5c2612b2af" + integrity sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg== + +string.prototype.matchall@^4.0.11: + version "4.0.11" + resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.11.tgz#1092a72c59268d2abaad76582dccc687c0297e0a" + integrity sha512-NUdh0aDavY2og7IbBPenWqR9exH+E26Sv8e0/eTe1tltDGZL+GtBkDAnnyBtmekfK6/Dq3MkcGtzXFEd1LQrtg== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.2" + es-errors "^1.3.0" + es-object-atoms "^1.0.0" + get-intrinsic "^1.2.4" + gopd "^1.0.1" + has-symbols "^1.0.3" + internal-slot "^1.0.7" + regexp.prototype.flags "^1.5.2" + set-function-name "^2.0.2" + side-channel "^1.0.6" + +string.prototype.trim@^1.2.9: + version "1.2.9" + resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz#b6fa326d72d2c78b6df02f7759c73f8f6274faa4" + integrity sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.0" + es-object-atoms "^1.0.0" + +string.prototype.trimend@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz#3651b8513719e8a9f48de7f2f77640b26652b229" + integrity sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-object-atoms "^1.0.0" + +string.prototype.trimstart@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz#7ee834dda8c7c17eff3118472bb35bfedaa34dde" + integrity sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-object-atoms "^1.0.0" + +strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-json-comments@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" + integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== + +supports-color@^5.3.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + +supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +supports-preserve-symlinks-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" + integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== + +synckit@0.9.0: + version "0.9.0" + resolved "https://registry.yarnpkg.com/synckit/-/synckit-0.9.0.tgz#5b33b458b3775e4466a5b377fba69c63572ae449" + integrity sha512-7RnqIMq572L8PeEzKeBINYEJDDxpcH8JEgLwUqBd3TkofhFRbkq4QLR0u+36avGAhCRbk2nnmjcW9SE531hPDg== + dependencies: + "@pkgr/core" "^0.1.0" + tslib "^2.6.2" + +text-table@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" + integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== + +to-fast-properties@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" + integrity sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog== + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +ts-api-utils@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.3.0.tgz#4b490e27129f1e8e686b45cc4ab63714dc60eea1" + integrity sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ== + +tslib@^2.6.2: + version "2.6.2" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" + integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== + +type-check@^0.4.0, type-check@~0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" + integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew== + dependencies: + prelude-ls "^1.2.1" + +type-fest@^0.20.2: + version "0.20.2" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" + integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== + +typed-array-buffer@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz#1867c5d83b20fcb5ccf32649e5e2fc7424474ff3" + integrity sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ== + dependencies: + call-bind "^1.0.7" + es-errors "^1.3.0" + is-typed-array "^1.1.13" + +typed-array-byte-length@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz#d92972d3cff99a3fa2e765a28fcdc0f1d89dec67" + integrity sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw== + dependencies: + call-bind "^1.0.7" + for-each "^0.3.3" + gopd "^1.0.1" + has-proto "^1.0.3" + is-typed-array "^1.1.13" + +typed-array-byte-offset@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz#f9ec1acb9259f395093e4567eb3c28a580d02063" + integrity sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA== + dependencies: + available-typed-arrays "^1.0.7" + call-bind "^1.0.7" + for-each "^0.3.3" + gopd "^1.0.1" + has-proto "^1.0.3" + is-typed-array "^1.1.13" + +typed-array-length@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/typed-array-length/-/typed-array-length-1.0.6.tgz#57155207c76e64a3457482dfdc1c9d1d3c4c73a3" + integrity sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g== + dependencies: + call-bind "^1.0.7" + for-each "^0.3.3" + gopd "^1.0.1" + has-proto "^1.0.3" + is-typed-array "^1.1.13" + possible-typed-array-names "^1.0.0" + +typescript@^5: + version "5.4.5" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.4.5.tgz#42ccef2c571fdbd0f6718b1d1f5e6e5ef006f611" + integrity sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ== + +unbox-primitive@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz#29032021057d5e6cdbd08c5129c226dff8ed6f9e" + integrity sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw== + dependencies: + call-bind "^1.0.2" + has-bigints "^1.0.2" + has-symbols "^1.0.3" + which-boxed-primitive "^1.0.2" + +update-browserslist-db@^1.0.13: + version "1.0.16" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.16.tgz#f6d489ed90fb2f07d67784eb3f53d7891f736356" + integrity sha512-KVbTxlBYlckhF5wgfyZXTWnMn7MMZjMu9XG8bPlliUOP9ThaF4QnhP8qrjrH7DRzHfSk0oQv1wToW+iA5GajEQ== + dependencies: + escalade "^3.1.2" + picocolors "^1.0.1" + +uri-js@^4.2.2: + version "4.4.1" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" + integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== + dependencies: + punycode "^2.1.0" + +vite@^5.2: + version "5.2.12" + resolved "https://registry.yarnpkg.com/vite/-/vite-5.2.12.tgz#3536c93c58ba18edea4915a2ac573e6537409d97" + integrity sha512-/gC8GxzxMK5ntBwb48pR32GGhENnjtY30G4A0jemunsBkiEZFw60s8InGpN8gkhHEkjnRK1aSAxeQgwvFhUHAA== + dependencies: + esbuild "^0.20.1" + postcss "^8.4.38" + rollup "^4.13.0" + optionalDependencies: + fsevents "~2.3.3" + +which-boxed-primitive@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6" + integrity sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg== + dependencies: + is-bigint "^1.0.1" + is-boolean-object "^1.1.0" + is-number-object "^1.0.4" + is-string "^1.0.5" + is-symbol "^1.0.3" + +which-builtin-type@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/which-builtin-type/-/which-builtin-type-1.1.3.tgz#b1b8443707cc58b6e9bf98d32110ff0c2cbd029b" + integrity sha512-YmjsSMDBYsM1CaFiayOVT06+KJeXf0o5M/CAd4o1lTadFAtacTUM49zoYxr/oroopFDfhvN6iEcBxUyc3gvKmw== + dependencies: + function.prototype.name "^1.1.5" + has-tostringtag "^1.0.0" + is-async-function "^2.0.0" + is-date-object "^1.0.5" + is-finalizationregistry "^1.0.2" + is-generator-function "^1.0.10" + is-regex "^1.1.4" + is-weakref "^1.0.2" + isarray "^2.0.5" + which-boxed-primitive "^1.0.2" + which-collection "^1.0.1" + which-typed-array "^1.1.9" + +which-collection@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/which-collection/-/which-collection-1.0.2.tgz#627ef76243920a107e7ce8e96191debe4b16c2a0" + integrity sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw== + dependencies: + is-map "^2.0.3" + is-set "^2.0.3" + is-weakmap "^2.0.2" + is-weakset "^2.0.3" + +which-typed-array@^1.1.14, which-typed-array@^1.1.15, which-typed-array@^1.1.9: + version "1.1.15" + resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.15.tgz#264859e9b11a649b388bfaaf4f767df1f779b38d" + integrity sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA== + dependencies: + available-typed-arrays "^1.0.7" + call-bind "^1.0.7" + for-each "^0.3.3" + gopd "^1.0.1" + has-tostringtag "^1.0.2" + +which@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +word-wrap@^1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" + integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== + +yallist@^3.0.2: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" + integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== + +yocto-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== + +zod@^3: + version "3.23.8" + resolved "https://registry.yarnpkg.com/zod/-/zod-3.23.8.tgz#e37b957b5d52079769fb8097099b592f0ef4067d" + integrity sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g== diff --git a/mobile/fastlane/metadata/playstore/fr/full_description.txt b/mobile/fastlane/metadata/playstore/fr/full_description.txt index 9a7f5975eb..07ff21f85f 100644 --- a/mobile/fastlane/metadata/playstore/fr/full_description.txt +++ b/mobile/fastlane/metadata/playstore/fr/full_description.txt @@ -1,30 +1,30 @@ -Entre est une application simple qui sauvegarde et organisĂ© vos photos et vidĂ©os. +Entre est une application simple qui sauvegarde et organise vos photos et vidĂ©os. -Si vous recherchez une alternative respectueuse de la vie privĂ©e pour prĂ©server vos souvenirs, vous ĂȘtes au bon endroit. Avec Ente, ils sont stockĂ©s chiffrĂ©s de bout-en-bout (e2ee). Cela signifie que vous-seul pouvez les voir. +Si vous recherchez une alternative respectueuse de votre vie privĂ©e pour prĂ©server vos souvenirs, vous ĂȘtes au bon endroit. Avec Ente, ils sont stockĂ©s chiffrĂ©s de bout-en-bout (e2ee). Cela signifie que vous-seul pouvez les voir. -Nous avons des applications sur Android, iOS, Web et Ordinateur, et vos photos seront synchronisĂ©es de maniĂšre transparente entre tous vos appareils chiffrĂ©e de bout en bout (e2ee). +Nous avons des applications pour Android, iOS, Web et Ordinateur, et vos photos seront synchronisĂ©es de maniĂšre transparente entre tous vos appareils avec une mĂ©thode de chiffrement de bout en bout (e2ee). Ente vous permet Ă©galement de partager vos albums avec vos proches. Vous pouvez soit les partager directement avec d'autres utilisateurs Ente, chiffrĂ©s de bout en bout ou avec des liens visibles publiquement. -Vos donnĂ©es chiffrĂ©es sont stockĂ©es Ă  travers de multiples endroits, dont un abri antiatomique Ă  Paris. Nous prenons la postĂ©ritĂ© au sĂ©rieux et facilitons la conservation de vos souvenirs. +Vos donnĂ©es chiffrĂ©es sont stockĂ©es dans de multiples endroits, dont un abri antiatomique Ă  Paris. Nous prenons la postĂ©ritĂ© au sĂ©rieux et facilitons la conservation de vos souvenirs. Nous sommes lĂ  pour faire l'application photo la plus sĂ»re de tous les temps, rejoignez-nous ! ✹ CARACTÉRISTIQUES -- Sauvegardes de qualitĂ© originales, car chaque pixel est important +- Sauvegardes en qualitĂ© originale, car chaque pixel est important - Abonnement familiaux, pour que vous puissiez partager l'espace de stockage avec votre famille - Dossiers partagĂ©s, si vous voulez que votre partenaire profite de vos clichĂ©s -- Liens ves les albums qui peuvent ĂȘtre protĂ©gĂ©s par un mot de passe et ĂȘtre configurĂ©s pour expirer +- Liens vers les albums, qui peuvent ĂȘtre protĂ©gĂ©s par un mot de passe et ĂȘtre configurĂ©s pour expirer - PossibilitĂ© de libĂ©rer de l'espace en supprimant les fichiers qui ont Ă©tĂ© sauvegardĂ©s en toute sĂ©curitĂ© - Éditeur d'images, pour ajouter des touches de finition -- Favoriser, cacher et revivre vos souvenirs, car ils sont prĂ©cieux +- Favoris, cacher et revivre vos souvenirs, car ils sont prĂ©cieux - Importation en un clic depuis Google, Apple, votre disque dur et plus encore - ThĂšme sombre, parce que vos photos y sont jolies - 2FA, 3FA, authentification biomĂ©trique - et beaucoup de choses encore ! đŸ’Č PRIX -Nous ne proposons pas d'abonnement gratuits pour toujours, car il est important pour nous de rester durables et de rĂ©sister Ă  l'Ă©preuve du temps. Au lieu de cela, nous vous proposons des abonnements abordables que vous pouvez partager librement avec votre famille. Vous pouvez trouver plus d'informations sur ente.io. +Nous ne proposons pas d'abonnements gratuits Ă  vie, car il est important pour nous de rester pĂ©renne et de rĂ©sister Ă  l'Ă©preuve du temps. Au lieu de cela, nous vous proposons des abonnements abordables que vous pouvez partager librement avec votre famille. Vous pouvez trouver plus d'informations sur ente.io. 🙋 ASSISTANCE Nous sommes fiers d'offrir un support humain. Si vous ĂȘtes un abonnĂ©, vous pouvez contacter team@ente.io et vous recevrez une rĂ©ponse de notre Ă©quipe dans les 24 heures. \ No newline at end of file diff --git a/mobile/lib/core/configuration.dart b/mobile/lib/core/configuration.dart index 8019e2a73c..92bb31e763 100644 --- a/mobile/lib/core/configuration.dart +++ b/mobile/lib/core/configuration.dart @@ -1,9 +1,9 @@ import "dart:async"; import 'dart:convert'; import "dart:io"; -import 'dart:typed_data'; import 'package:bip39/bip39.dart' as bip39; +import "package:flutter/services.dart"; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:logging/logging.dart'; import 'package:path_provider/path_provider.dart'; @@ -97,37 +97,58 @@ class Configuration { ); Future init() async { - _preferences = await SharedPreferences.getInstance(); - _secureStorage = const FlutterSecureStorage(); - _documentsDirectory = (await getApplicationDocumentsDirectory()).path; - _tempDocumentsDirPath = _documentsDirectory + "/temp/"; - final tempDocumentsDir = Directory(_tempDocumentsDirPath); - await _cleanUpStaleFiles(tempDocumentsDir); - tempDocumentsDir.createSync(recursive: true); - final tempDirectoryPath = (await getTemporaryDirectory()).path; - _thumbnailCacheDirectory = tempDirectoryPath + "/thumbnail-cache"; - Directory(_thumbnailCacheDirectory).createSync(recursive: true); - _sharedTempMediaDirectory = tempDirectoryPath + "/ente-shared-media"; - Directory(_sharedTempMediaDirectory).createSync(recursive: true); - _sharedDocumentsMediaDirectory = _documentsDirectory + "/ente-shared-media"; - Directory(_sharedDocumentsMediaDirectory).createSync(recursive: true); - if (!_preferences.containsKey(tokenKey)) { - await _secureStorage.deleteAll(iOptions: _secureStorageOptionsIOS); - } else { - _key = await _secureStorage.read( - key: keyKey, - iOptions: _secureStorageOptionsIOS, - ); - _secretKey = await _secureStorage.read( - key: secretKeyKey, - iOptions: _secureStorageOptionsIOS, - ); - if (_key == null) { - await logout(autoLogout: true); + try { + _preferences = await SharedPreferences.getInstance(); + _secureStorage = const FlutterSecureStorage(); + _documentsDirectory = (await getApplicationDocumentsDirectory()).path; + _tempDocumentsDirPath = _documentsDirectory + "/temp/"; + final tempDocumentsDir = Directory(_tempDocumentsDirPath); + await _cleanUpStaleFiles(tempDocumentsDir); + tempDocumentsDir.createSync(recursive: true); + final tempDirectoryPath = (await getTemporaryDirectory()).path; + _thumbnailCacheDirectory = tempDirectoryPath + "/thumbnail-cache"; + Directory(_thumbnailCacheDirectory).createSync(recursive: true); + _sharedTempMediaDirectory = tempDirectoryPath + "/ente-shared-media"; + Directory(_sharedTempMediaDirectory).createSync(recursive: true); + _sharedDocumentsMediaDirectory = + _documentsDirectory + "/ente-shared-media"; + Directory(_sharedDocumentsMediaDirectory).createSync(recursive: true); + if (!_preferences.containsKey(tokenKey)) { + await _secureStorage.deleteAll(iOptions: _secureStorageOptionsIOS); + } else { + _key = await _secureStorage.read( + key: keyKey, + iOptions: _secureStorageOptionsIOS, + ); + _secretKey = await _secureStorage.read( + key: secretKeyKey, + iOptions: _secureStorageOptionsIOS, + ); + if (_key == null) { + await logout(autoLogout: true); + } + await _migrateSecurityStorageToFirstUnlock(); + } + SuperLogging.setUserID(await _getOrCreateAnonymousUserID()).ignore(); + } catch (e, s) { + _logger.severe("Configuration init failed", e, s); + /* + Check if it's a known is related to reading secret from secure storage + on android https://github.com/mogol/flutter_secure_storage/issues/541 + */ + if (e is PlatformException) { + final PlatformException error = e; + final bool isBadPaddingError = + error.toString().contains('BadPaddingException') || + (error.message ?? '').contains('BadPaddingException'); + if (isBadPaddingError) { + await logout(autoLogout: true); + return; + } + } else { + rethrow; } - await _migrateSecurityStorageToFirstUnlock(); } - SuperLogging.setUserID(await _getOrCreateAnonymousUserID()).ignore(); } // _cleanUpStaleFiles deletes all files in the temp directory that are older @@ -167,14 +188,16 @@ class Configuration { } Future logout({bool autoLogout = false}) async { - if (SyncService.instance.isSyncInProgress()) { - SyncService.instance.stopSync(); - try { - await SyncService.instance - .existingSync() - .timeout(const Duration(seconds: 5)); - } catch (e) { - // ignore + if (!autoLogout) { + if (SyncService.instance.isSyncInProgress()) { + SyncService.instance.stopSync(); + try { + await SyncService.instance + .existingSync() + .timeout(const Duration(seconds: 5)); + } catch (e) { + // ignore + } } } await _preferences.clear(); diff --git a/mobile/lib/db/device_files_db.dart b/mobile/lib/db/device_files_db.dart index 25c88daca1..fbd1649e26 100644 --- a/mobile/lib/db/device_files_db.dart +++ b/mobile/lib/db/device_files_db.dart @@ -22,61 +22,55 @@ extension DeviceFiles on FilesDB { ConflictAlgorithm conflictAlgorithm = ConflictAlgorithm.ignore, }) async { debugPrint("Inserting missing PathIDToLocalIDMapping"); - final db = await database; - var batch = db.batch(); + final parameterSets = >[]; int batchCounter = 0; for (MapEntry e in mappingToAdd.entries) { final String pathID = e.key; for (String localID in e.value) { + parameterSets.add([localID, pathID]); + batchCounter++; + if (batchCounter == 400) { - await batch.commit(noResult: true); - batch = db.batch(); + await _insertBatch(parameterSets, conflictAlgorithm); + parameterSets.clear(); batchCounter = 0; } - batch.insert( - "device_files", - { - "id": localID, - "path_id": pathID, - }, - conflictAlgorithm: conflictAlgorithm, - ); - batchCounter++; } } - await batch.commit(noResult: true); + await _insertBatch(parameterSets, conflictAlgorithm); + parameterSets.clear(); + batchCounter = 0; } Future deletePathIDToLocalIDMapping( Map> mappingsToRemove, ) async { debugPrint("removing PathIDToLocalIDMapping"); - final db = await database; - var batch = db.batch(); + final parameterSets = >[]; int batchCounter = 0; for (MapEntry e in mappingsToRemove.entries) { final String pathID = e.key; + for (String localID in e.value) { + parameterSets.add([localID, pathID]); + batchCounter++; + if (batchCounter == 400) { - await batch.commit(noResult: true); - batch = db.batch(); + await _deleteBatch(parameterSets); + parameterSets.clear(); batchCounter = 0; } - batch.delete( - "device_files", - where: 'id = ? AND path_id = ?', - whereArgs: [localID, pathID], - ); - batchCounter++; } } - await batch.commit(noResult: true); + await _deleteBatch(parameterSets); + parameterSets.clear(); + batchCounter = 0; } Future> getDevicePathIDToImportedFileCount() async { try { - final db = await database; - final rows = await db.rawQuery( + final db = await sqliteAsyncDB; + final rows = await db.getAll( ''' SELECT count(*) as count, path_id FROM device_files @@ -96,8 +90,8 @@ extension DeviceFiles on FilesDB { Future>> getDevicePathIDToLocalIDMap() async { try { - final db = await database; - final rows = await db.rawQuery( + final db = await sqliteAsyncDB; + final rows = await db.getAll( ''' SELECT id, path_id FROM device_files; ''', ); final result = >{}; @@ -116,8 +110,8 @@ extension DeviceFiles on FilesDB { } Future> getDevicePathIDs() async { - final Database db = await database; - final rows = await db.rawQuery( + final db = await sqliteAsyncDB; + final rows = await db.getAll( ''' SELECT id FROM device_collections ''', @@ -133,34 +127,42 @@ extension DeviceFiles on FilesDB { List localPathAssets, { bool shouldAutoBackup = false, }) async { - final Database db = await database; + final db = await sqliteAsyncDB; final Map> pathIDToLocalIDsMap = {}; try { - final batch = db.batch(); final Set existingPathIds = await getDevicePathIDs(); + final parameterSetsForUpdate = >[]; + final parameterSetsForInsert = >[]; for (LocalPathAsset localPathAsset in localPathAssets) { if (localPathAsset.localIDs.isNotEmpty) { pathIDToLocalIDsMap[localPathAsset.pathID] = localPathAsset.localIDs; } if (existingPathIds.contains(localPathAsset.pathID)) { - batch.rawUpdate( - "UPDATE device_collections SET name = ? where id = " - "?", - [localPathAsset.pathName, localPathAsset.pathID], - ); + parameterSetsForUpdate + .add([localPathAsset.pathName, localPathAsset.pathID]); } else if (localPathAsset.localIDs.isNotEmpty) { - batch.insert( - "device_collections", - { - "id": localPathAsset.pathID, - "name": localPathAsset.pathName, - "should_backup": shouldAutoBackup ? _sqlBoolTrue : _sqlBoolFalse, - }, - conflictAlgorithm: ConflictAlgorithm.ignore, - ); + parameterSetsForInsert.add([ + localPathAsset.pathID, + localPathAsset.pathName, + shouldAutoBackup ? _sqlBoolTrue : _sqlBoolFalse, + ]); } } - await batch.commit(noResult: true); + + await db.executeBatch( + ''' + INSERT OR IGNORE INTO device_collections (id, name, should_backup) VALUES (?, ?, ?); + ''', + parameterSetsForInsert, + ); + + await db.executeBatch( + ''' + UPDATE device_collections SET name = ? WHERE id = ?; + ''', + parameterSetsForUpdate, + ); + // add the mappings for localIDs if (pathIDToLocalIDsMap.isNotEmpty) { await insertPathIDToLocalIDMapping(pathIDToLocalIDsMap); @@ -177,7 +179,7 @@ extension DeviceFiles on FilesDB { }) async { bool hasUpdated = false; try { - final Database db = await database; + final db = await sqliteAsyncDB; final Set existingPathIds = await getDevicePathIDs(); for (Tuple2 tup in devicePathInfo) { final AssetPathEntity pathEntity = tup.item1; @@ -185,35 +187,42 @@ extension DeviceFiles on FilesDB { final String localID = tup.item2; final bool shouldUpdate = existingPathIds.contains(pathEntity.id); if (shouldUpdate) { - final rowUpdated = await db.rawUpdate( - "UPDATE device_collections SET name = ?, cover_id = ?, count" - " = ? where id = ? AND (name != ? OR cover_id != ? OR count != ?)", - [ - pathEntity.name, - localID, - assetCount, - pathEntity.id, - pathEntity.name, - localID, - assetCount, - ], - ); + final rowUpdated = await db.writeTransaction((tx) async { + await tx.execute( + "UPDATE device_collections SET name = ?, cover_id = ?, count" + " = ? where id = ? AND (name != ? OR cover_id != ? OR count != ?)", + [ + pathEntity.name, + localID, + assetCount, + pathEntity.id, + pathEntity.name, + localID, + assetCount, + ], + ); + final result = await tx.get("SELECT changes();"); + return result["changes()"] as int; + }); + if (rowUpdated > 0) { _logger.fine("Updated $rowUpdated rows for ${pathEntity.name}"); hasUpdated = true; } } else { hasUpdated = true; - await db.insert( - "device_collections", - { - "id": pathEntity.id, - "name": pathEntity.name, - "count": assetCount, - "cover_id": localID, - "should_backup": shouldBackup ? _sqlBoolTrue : _sqlBoolFalse, - }, - conflictAlgorithm: ConflictAlgorithm.ignore, + await db.execute( + ''' + INSERT INTO device_collections (id, name, count, cover_id, should_backup) + VALUES (?, ?, ?, ?, ?); + ''', + [ + pathEntity.id, + pathEntity.name, + assetCount, + localID, + shouldBackup ? _sqlBoolTrue : _sqlBoolFalse, + ], ); } } @@ -231,15 +240,17 @@ extension DeviceFiles on FilesDB { // feature, where we delete files which are backed up. Deleting such // entries here result in us losing out on the information that // those folders were marked for automatic backup. - await db.delete( - "device_collections", - where: 'id = ? and should_backup = $_sqlBoolFalse ', - whereArgs: [pathID], + await db.execute( + ''' + DELETE FROM device_collections WHERE id = ? AND should_backup = $_sqlBoolFalse; + ''', + [pathID], ); - await db.delete( - "device_files", - where: 'path_id = ?', - whereArgs: [pathID], + await db.execute( + ''' + DELETE FROM device_files WHERE path_id = ?; + ''', + [pathID], ); } } @@ -253,8 +264,8 @@ extension DeviceFiles on FilesDB { // getDeviceSyncCollectionIDs returns the collectionIDs for the // deviceCollections which are marked for auto-backup Future> getDeviceSyncCollectionIDs() async { - final Database db = await database; - final rows = await db.rawQuery( + final db = await sqliteAsyncDB; + final rows = await db.getAll( ''' SELECT collection_id FROM device_collections where should_backup = $_sqlBoolTrue @@ -268,40 +279,47 @@ extension DeviceFiles on FilesDB { return result; } - Future updateDevicePathSyncStatus(Map syncStatus) async { - final db = await database; - var batch = db.batch(); + Future updateDevicePathSyncStatus( + Map syncStatus, + ) async { + final db = await sqliteAsyncDB; int batchCounter = 0; + final parameterSets = >[]; for (MapEntry e in syncStatus.entries) { final String pathID = e.key; + parameterSets.add([e.value ? _sqlBoolTrue : _sqlBoolFalse, pathID]); + batchCounter++; + if (batchCounter == 400) { - await batch.commit(noResult: true); - batch = db.batch(); + await db.executeBatch( + ''' + UPDATE device_collections SET should_backup = ? WHERE id = ?; + ''', + parameterSets, + ); + parameterSets.clear(); batchCounter = 0; } - batch.update( - "device_collections", - { - "should_backup": e.value ? _sqlBoolTrue : _sqlBoolFalse, - }, - where: 'id = ?', - whereArgs: [pathID], - ); - batchCounter++; } - await batch.commit(noResult: true); + + await db.executeBatch( + ''' + UPDATE device_collections SET should_backup = ? WHERE id = ?; + ''', + parameterSets, + ); } Future updateDeviceCollection( String pathID, int collectionID, ) async { - final db = await database; - await db.update( - "device_collections", - {"collection_id": collectionID}, - where: 'id = ?', - whereArgs: [pathID], + final db = await sqliteAsyncDB; + await db.execute( + ''' + UPDATE device_collections SET collection_id = ? WHERE id = ?; + ''', + [collectionID, pathID], ); return; } @@ -314,7 +332,7 @@ extension DeviceFiles on FilesDB { int? limit, bool? asc, }) async { - final db = await database; + final db = await sqliteAsyncDB; final order = (asc ?? false ? 'ASC' : 'DESC'); final String rawQuery = ''' SELECT * @@ -329,7 +347,7 @@ extension DeviceFiles on FilesDB { ORDER BY ${FilesDB.columnCreationTime} $order , ${FilesDB.columnModificationTime} $order ''' + (limit != null ? ' limit $limit;' : ';'); - final results = await db.rawQuery(rawQuery); + final results = await db.getAll(rawQuery); final files = convertToFiles(results); final dedupe = deduplicateByLocalID(files); return FileLoadResult(dedupe, files.length == limit); @@ -339,7 +357,7 @@ extension DeviceFiles on FilesDB { String pathID, int ownerID, ) async { - final db = await database; + final db = await sqliteAsyncDB; const String rawQuery = ''' SELECT ${FilesDB.columnLocalID}, ${FilesDB.columnUploadedFileID}, ${FilesDB.columnFileSize} @@ -351,7 +369,7 @@ extension DeviceFiles on FilesDB { ${FilesDB.columnLocalID} IN (SELECT id FROM device_files where path_id = ?) '''; - final results = await db.rawQuery(rawQuery, [ownerID, pathID]); + final results = await db.getAll(rawQuery, [ownerID, pathID]); final localIDs = {}; final uploadedIDs = {}; int localSize = 0; @@ -375,17 +393,17 @@ extension DeviceFiles on FilesDB { "$includeCoverThumbnail", ); try { - final db = await database; + final db = await sqliteAsyncDB; final coverFiles = []; if (includeCoverThumbnail) { - final fileRows = await db.rawQuery( + final fileRows = await db.getAll( '''SELECT * FROM FILES where local_id in (select cover_id from device_collections) group by local_id; ''', ); final files = convertToFiles(fileRows); coverFiles.addAll(files); } - final deviceCollectionRows = await db.rawQuery( + final deviceCollectionRows = await db.getAll( '''SELECT * from device_collections''', ); final List deviceCollections = []; @@ -433,8 +451,8 @@ extension DeviceFiles on FilesDB { Future getDeviceCollectionThumbnail(String pathID) async { debugPrint("Call fallback method to get potential thumbnail"); - final db = await database; - final fileRows = await db.rawQuery( + final db = await sqliteAsyncDB; + final fileRows = await db.getAll( '''SELECT * FROM FILES f JOIN device_files df on f.local_id = df.id and df.path_id= ? order by f.creation_time DESC limit 1; ''', @@ -447,4 +465,28 @@ extension DeviceFiles on FilesDB { return null; } } + + Future _insertBatch( + List> parameterSets, + ConflictAlgorithm conflictAlgorithm, + ) async { + final db = await sqliteAsyncDB; + await db.executeBatch( + ''' + INSERT OR ${conflictAlgorithm.name.toUpperCase()} + INTO device_files (id, path_id) VALUES (?, ?); + ''', + parameterSets, + ); + } + + Future _deleteBatch(List> parameterSets) async { + final db = await sqliteAsyncDB; + await db.executeBatch( + ''' + DELETE FROM device_files WHERE id = ? AND path_id = ?; + ''', + parameterSets, + ); + } } diff --git a/mobile/lib/db/entities_db.dart b/mobile/lib/db/entities_db.dart index cee32641a0..3cd9d47639 100644 --- a/mobile/lib/db/entities_db.dart +++ b/mobile/lib/db/entities_db.dart @@ -10,53 +10,78 @@ extension EntitiesDB on FilesDB { ConflictAlgorithm conflictAlgorithm = ConflictAlgorithm.replace, }) async { debugPrint("entitiesDB: upsertEntities ${data.length} entities"); - final db = await database; - var batch = db.batch(); + final db = await sqliteAsyncDB; + final parameterSets = >[]; int batchCounter = 0; for (LocalEntityData e in data) { + parameterSets.add([ + e.id, + e.type.name, + e.ownerID, + e.data, + e.updatedAt, + ]); + batchCounter++; + if (batchCounter == 400) { - await batch.commit(noResult: true); - batch = db.batch(); + await db.executeBatch( + ''' + INSERT OR ${conflictAlgorithm.name.toUpperCase()} + INTO entities (id, type, ownerID, data, updatedAt) + VALUES (?, ?, ?, ?, ?) +''', + parameterSets, + ); + parameterSets.clear(); batchCounter = 0; } - batch.insert( - "entities", - e.toJson(), - conflictAlgorithm: conflictAlgorithm, - ); - batchCounter++; } - await batch.commit(noResult: true); + await db.executeBatch( + ''' + INSERT OR ${conflictAlgorithm.name.toUpperCase()} + INTO entities (id, type, ownerID, data, updatedAt) + VALUES (?, ?, ?, ?, ?) +''', + parameterSets, + ); } Future deleteEntities( List ids, ) async { - final db = await database; - var batch = db.batch(); + final db = await sqliteAsyncDB; + final parameterSets = >[]; int batchCounter = 0; for (String id in ids) { - if (batchCounter == 400) { - await batch.commit(noResult: true); - batch = db.batch(); - batchCounter = 0; - } - batch.delete( - "entities", - where: "id = ?", - whereArgs: [id], + parameterSets.add( + [id], ); batchCounter++; + + if (batchCounter == 400) { + await db.executeBatch( + ''' + DELETE FROM entities WHERE id = ? + ''', + parameterSets, + ); + parameterSets.clear(); + batchCounter = 0; + } } - await batch.commit(noResult: true); + await db.executeBatch( + ''' + DELETE FROM entities WHERE id = ? + ''', + parameterSets, + ); } Future> getEntities(EntityType type) async { - final db = await database; - final List> maps = await db.query( - "entities", - where: "type = ?", - whereArgs: [type.typeToString()], + final db = await sqliteAsyncDB; + final List> maps = await db.getAll( + 'SELECT * FROM entities WHERE type = ?', + [type.name], ); return List.generate(maps.length, (i) { return LocalEntityData.fromJson(maps[i]); @@ -64,11 +89,10 @@ extension EntitiesDB on FilesDB { } Future getEntity(EntityType type, String id) async { - final db = await database; - final List> maps = await db.query( - "entities", - where: "type = ? AND id = ?", - whereArgs: [type.typeToString(), id], + final db = await sqliteAsyncDB; + final List> maps = await db.getAll( + 'SELECT * FROM entities WHERE type = ? AND id = ?', + [type.name, id], ); if (maps.isEmpty) { return null; diff --git a/mobile/lib/db/files_db.dart b/mobile/lib/db/files_db.dart index f72ecb32a4..c6ba617e0e 100644 --- a/mobile/lib/db/files_db.dart +++ b/mobile/lib/db/files_db.dart @@ -1,3 +1,4 @@ +import "dart:async"; import "dart:io"; import "package:computer/computer.dart"; @@ -14,9 +15,9 @@ 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:sqflite/sqflite.dart'; -import 'package:sqflite_migration/sqflite_migration.dart'; -import 'package:sqlite_async/sqlite_async.dart' as sqlite_async; +import "package:photos/utils/primitive_wrapper.dart"; +import "package:photos/utils/sqlite_util.dart"; +import 'package:sqlite_async/sqlite_async.dart'; class FilesDB { /* @@ -72,10 +73,11 @@ class FilesDB { // we need to write query based on that field static const columnMMdVisibility = 'mmd_visibility'; - static final initializationScript = [ - ...createTable(filesTable), - ]; +//If adding or removing a new column, make sure to update the `_columnNames` list +//and update `_generateColumnsAndPlaceholdersForInsert` and +//`_generateUpdateAssignmentsWithPlaceholders` static final migrationScripts = [ + ...createTable(filesTable), ...alterDeviceFolderToAllowNULL(), ...alterTimestampColumnTypes(), ...addIndices(), @@ -90,10 +92,38 @@ class FilesDB { ...addAddedTime(), ]; - final dbConfig = MigrationConfig( - initializationScript: initializationScript, - migrationScripts: migrationScripts, - ); + static const List _columnNames = [ + columnGeneratedID, + columnLocalID, + columnUploadedFileID, + columnOwnerID, + columnCollectionID, + columnTitle, + columnDeviceFolder, + columnLatitude, + columnLongitude, + columnFileType, + columnModificationTime, + columnEncryptedKey, + columnKeyDecryptionNonce, + columnFileDecryptionHeader, + columnThumbnailDecryptionHeader, + columnMetadataDecryptionHeader, + columnCreationTime, + columnUpdationTime, + columnFileSubType, + columnDuration, + columnExif, + columnHash, + columnMetadataVersion, + columnMMdEncodedJson, + columnMMdVersion, + columnMMdVisibility, + columnPubMMdEncodedJson, + columnPubMMdVersion, + columnFileSize, + columnAddedTime, + ]; // make this a singleton class FilesDB._privateConstructor(); @@ -101,36 +131,46 @@ class FilesDB { static final FilesDB instance = FilesDB._privateConstructor(); // only have a single app-wide reference to the database - static Future? _dbFuture; - static Future? _sqliteAsyncDBFuture; + static Future? _sqliteAsyncDBFuture; - @Deprecated("Use sqliteAsyncDB instead (sqlite_async)") - Future get database async { + Future get sqliteAsyncDB async { // lazily instantiate the db the first time it is accessed - _dbFuture ??= _initDatabase(); - return _dbFuture!; - } - - Future get sqliteAsyncDB async { _sqliteAsyncDBFuture ??= _initSqliteAsyncDatabase(); return _sqliteAsyncDBFuture!; } // this opens the database (and creates it if it doesn't exist) - Future _initDatabase() async { + Future _initSqliteAsyncDatabase() async { final Directory documentsDirectory = await getApplicationDocumentsDirectory(); final String path = join(documentsDirectory.path, _databaseName); _logger.info("DB path " + path); - return await openDatabaseWithMigration(path, dbConfig); + final database = SqliteDatabase(path: path); + await _migrate(database); + + return database; } - Future _initSqliteAsyncDatabase() async { - final Directory documentsDirectory = - await getApplicationDocumentsDirectory(); - final String path = join(documentsDirectory.path, _databaseName); - _logger.info("DB path " + path); - return sqlite_async.SqliteDatabase(path: path); + Future _migrate( + SqliteDatabase database, + ) async { + final result = await database.execute('PRAGMA user_version'); + final currentVersion = result[0]['user_version'] as int; + final toVersion = migrationScripts.length; + + if (currentVersion < toVersion) { + _logger.info("Migrating database from $currentVersion to $toVersion"); + await database.writeTransaction((tx) async { + for (int i = currentVersion + 1; i <= toVersion; i++) { + await tx.execute(migrationScripts[i - 1]); + } + await tx.execute('PRAGMA user_version = $toVersion'); + }); + } else if (currentVersion > toVersion) { + throw AssertionError( + "currentVersion($currentVersion) cannot be greater than toVersion($toVersion)", + ); + } } // SQL code to create the database table @@ -400,11 +440,11 @@ class FilesDB { } Future clearTable() async { - final db = await instance.database; - await db.delete(filesTable); - await db.delete("device_files"); - await db.delete("device_collections"); - await db.delete("entities"); + final db = await instance.sqliteAsyncDB; + await db.execute('DELETE FROM $filesTable'); + await db.execute('DELETE FROM device_files'); + await db.execute('DELETE FROM device_collections'); + await db.execute('DELETE FROM entities'); } Future deleteDB() async { @@ -414,32 +454,75 @@ class FilesDB { await getApplicationDocumentsDirectory(); final String path = join(documentsDirectory.path, _databaseName); File(path).deleteSync(recursive: true); - _dbFuture = null; + _sqliteAsyncDBFuture = null; } } Future insertMultiple( List files, { - ConflictAlgorithm conflictAlgorithm = ConflictAlgorithm.replace, + SqliteAsyncConflictAlgorithm conflictAlgorithm = + SqliteAsyncConflictAlgorithm.replace, }) async { + if (files.isEmpty) return; + final startTime = DateTime.now(); - final db = await database; - var batch = db.batch(); - int batchCounter = 0; + 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 genIdNullcolumnNames = + _columnNames.where((element) => element != columnGeneratedID); + for (EnteFile file in files) { - if (batchCounter == 400) { - await batch.commit(noResult: true); - batch = db.batch(); - batchCounter = 0; + final fileGenIdIsNull = file.generatedID == null; + + if (!fileGenIdIsNull) { + await _batchAndInsertFile( + file, + conflictAlgorithm, + db, + genIdNotNullParameterSets, + genIdNotNullbatchCounter, + isGenIdNull: fileGenIdIsNull, + ); + } else { + await _batchAndInsertFile( + file, + conflictAlgorithm, + db, + genIdNullParameterSets, + genIdNullbatchCounter, + isGenIdNull: fileGenIdIsNull, + ); } - batch.insert( - filesTable, - _getRowForFile(file), - conflictAlgorithm: conflictAlgorithm, - ); - batchCounter++; } - await batch.commit(noResult: true); + + if (genIdNotNullbatchCounter.value > 0) { + await _insertBatch( + conflictAlgorithm, + _columnNames, + db, + genIdNotNullParameterSets, + ); + 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: @@ -454,16 +537,35 @@ class FilesDB { ); } - Future insert(EnteFile file) async { + Future insert(EnteFile file) async { _logger.info("Inserting $file"); - final db = await instance.database; - return db.insert( - filesTable, - _getRowForFile(file), - conflictAlgorithm: ConflictAlgorithm.replace, + final db = await instance.sqliteAsyncDB; + final columnsAndPlaceholders = + _generateColumnsAndPlaceholdersForInsert(fileGenId: file.generatedID); + final values = _getParameterSetForFile(file); + + await db.execute( + 'INSERT OR REPLACE INTO $filesTable (${columnsAndPlaceholders["columns"]}) VALUES (${columnsAndPlaceholders["placeholders"]})', + values, ); } + Future insertAndGetId(EnteFile file) async { + _logger.info("Inserting $file"); + final db = await instance.sqliteAsyncDB; + final columnsAndPlaceholders = + _generateColumnsAndPlaceholdersForInsert(fileGenId: file.generatedID); + final values = _getParameterSetForFile(file); + return await db.writeTransaction((tx) async { + await tx.execute( + 'INSERT OR REPLACE INTO $filesTable (${columnsAndPlaceholders["columns"]}) VALUES (${columnsAndPlaceholders["placeholders"]})', + values, + ); + final result = await tx.get('SELECT last_insert_rowid()'); + return result["last_insert_rowid()"] as int; + }); + } + Future getFile(int generatedID) async { final db = await instance.sqliteAsyncDB; final results = await db.getAll( @@ -522,13 +624,11 @@ class FilesDB { Future<(Set, Map)> getUploadAndHash( int collectionID, ) async { - final db = await instance.database; - final results = await db.query( - filesTable, - columns: [columnUploadedFileID, columnHash], - where: - '$columnCollectionID = ? AND ($columnUploadedFileID IS NOT NULL AND $columnUploadedFileID IS NOT -1)', - whereArgs: [ + final db = await instance.sqliteAsyncDB; + final results = await db.getAll( + 'SELECT $columnUploadedFileID, $columnHash FROM $filesTable' + ' WHERE $columnCollectionID = ? AND ($columnUploadedFileID IS NOT NULL AND $columnUploadedFileID IS NOT -1)', + [ collectionID, ], ); @@ -801,15 +901,13 @@ class FilesDB { // Files which user added to a collection manually but they are not // uploaded yet or files belonging to a collection which is marked for backup Future> getFilesPendingForUpload() async { - final db = await instance.database; - final results = await db.query( - filesTable, - where: - '($columnUploadedFileID IS NULL OR $columnUploadedFileID IS -1) AND ' - '$columnCollectionID IS NOT NULL AND $columnCollectionID IS NOT -1 AND ' - '$columnLocalID IS NOT NULL AND $columnLocalID IS NOT -1', - orderBy: '$columnCreationTime DESC', - groupBy: columnLocalID, + final db = await instance.sqliteAsyncDB; + final results = await db.getAll( + 'SELECT * FROM $filesTable WHERE ($columnUploadedFileID IS NULL OR ' + '$columnUploadedFileID IS -1) AND $columnCollectionID IS NOT NULL AND ' + '$columnCollectionID IS NOT -1 AND $columnLocalID IS NOT NULL AND ' + '$columnLocalID IS NOT -1 GROUP BY $columnLocalID ' + 'ORDER BY $columnCreationTime DESC', ); final files = convertToFiles(results); // future-safe filter just to ensure that the query doesn't end up returning files @@ -824,29 +922,23 @@ class FilesDB { } Future> getUnUploadedLocalFiles() async { - final db = await instance.database; - final results = await db.query( - filesTable, - where: - '($columnUploadedFileID IS NULL OR $columnUploadedFileID IS -1) AND $columnLocalID IS NOT NULL', - orderBy: '$columnCreationTime DESC', - groupBy: columnLocalID, + final db = await instance.sqliteAsyncDB; + final results = await db.getAll( + 'SELECT * FROM $filesTable WHERE ($columnUploadedFileID IS NULL OR ' + '$columnUploadedFileID IS -1) AND $columnLocalID IS NOT NULL ' + 'GROUP BY $columnLocalID ORDER BY $columnCreationTime DESC', ); return convertToFiles(results); } Future> getUploadedFileIDsToBeUpdated(int ownerID) async { - final db = await instance.database; - final rows = await db.query( - filesTable, - columns: [columnUploadedFileID], - where: '($columnLocalID IS NOT NULL AND $columnOwnerID = ? AND ' - '($columnUploadedFileID ' - 'IS NOT ' - 'NULL AND $columnUploadedFileID IS NOT -1) AND $columnUpdationTime IS NULL)', - whereArgs: [ownerID], - orderBy: '$columnCreationTime DESC', - distinct: true, + final db = await instance.sqliteAsyncDB; + final rows = await db.getAll( + 'SELECT DISTINCT $columnUploadedFileID FROM $filesTable WHERE ' + '($columnLocalID IS NOT NULL AND $columnOwnerID = ? AND ' + '($columnUploadedFileID IS NOT NULL AND $columnUploadedFileID IS NOT -1) ' + 'AND $columnUpdationTime IS NULL) ORDER BY $columnCreationTime DESC ', + [ownerID], ); final uploadedFileIDs = []; for (final row in rows) { @@ -859,15 +951,11 @@ class FilesDB { int uploadedFileID, int userID, ) async { - final db = await instance.database; - final results = await db.query( - filesTable, - where: '$columnLocalID IS NOT NULL AND $columnOwnerID = ? AND ' - '$columnUploadedFileID = ?', - whereArgs: [ - userID, - uploadedFileID, - ], + final db = await instance.sqliteAsyncDB; + final results = await db.getAll( + 'SELECT * FROM $filesTable WHERE $columnLocalID IS NOT NULL AND ' + '$columnOwnerID = ? AND $columnUploadedFileID = ?', + [userID, uploadedFileID], ); if (results.isEmpty) { return []; @@ -876,14 +964,12 @@ class FilesDB { } Future> getExistingLocalFileIDs(int ownerID) async { - final db = await instance.database; - final rows = await db.query( - filesTable, - columns: [columnLocalID], - distinct: true, - where: '$columnLocalID IS NOT NULL AND ($columnOwnerID IS NULL OR ' - '$columnOwnerID = ?)', - whereArgs: [ownerID], + final db = await instance.sqliteAsyncDB; + final rows = await db.getAll( + 'SELECT DISTINCT $columnLocalID FROM $filesTable ' + 'WHERE $columnLocalID IS NOT NULL AND ($columnOwnerID IS NULL OR ' + '$columnOwnerID = ?)', + [ownerID], ); final result = {}; for (final row in rows) { @@ -893,16 +979,13 @@ class FilesDB { } Future> getLocalIDsMarkedForOrAlreadyUploaded(int ownerID) async { - final db = await instance.database; - final rows = await db.query( - filesTable, - columns: [columnLocalID], - distinct: true, - where: '$columnLocalID IS NOT NULL AND ($columnCollectionID IS NOT NULL ' - 'AND ' - '$columnCollectionID != -1) AND ($columnOwnerID = ? OR ' - '$columnOwnerID IS NULL)', - whereArgs: [ownerID], + final db = await instance.sqliteAsyncDB; + final rows = await db.getAll( + 'SELECT DISTINCT $columnLocalID FROM $filesTable ' + 'WHERE $columnLocalID IS NOT NULL AND ($columnCollectionID IS NOT NULL ' + 'AND $columnCollectionID != -1) AND ($columnOwnerID = ? OR ' + '$columnOwnerID IS NULL)', + [ownerID], ); final result = {}; for (final row in rows) { @@ -912,12 +995,11 @@ class FilesDB { } Future> getLocalFileIDsForCollection(int collectionID) async { - final db = await instance.database; - final rows = await db.query( - filesTable, - columns: [columnLocalID], - where: '$columnLocalID IS NOT NULL AND $columnCollectionID = ?', - whereArgs: [collectionID], + final db = await instance.sqliteAsyncDB; + final rows = await db.getAll( + 'SELECT $columnLocalID FROM $filesTable ' + 'WHERE $columnLocalID IS NOT NULL AND $columnCollectionID = ?', + [collectionID], ); final result = {}; for (final row in rows) { @@ -928,17 +1010,17 @@ class FilesDB { // Sets the collectionID for the files with given LocalIDs if the // corresponding file entries are not already mapped to some other collection - Future setCollectionIDForUnMappedLocalFiles( + Future setCollectionIDForUnMappedLocalFiles( int collectionID, Set localIDs, ) async { - final db = await instance.database; + final db = await instance.sqliteAsyncDB; String inParam = ""; for (final localID in localIDs) { inParam += "'" + localID + "',"; } inParam = inParam.substring(0, inParam.length - 1); - return await db.rawUpdate( + await db.execute( ''' UPDATE $filesTable SET $columnCollectionID = $collectionID @@ -948,7 +1030,7 @@ class FilesDB { ); } - Future markFilesForReUpload( + Future markFilesForReUpload( int ownerID, String localID, String? title, @@ -957,22 +1039,30 @@ class FilesDB { int modificationTime, FileType fileType, ) async { - final db = await instance.database; - return await db.update( - filesTable, - { - columnTitle: title, - columnLatitude: location?.latitude, - columnLongitude: location?.longitude, - columnCreationTime: creationTime, - columnModificationTime: modificationTime, - // #hack reset updation time to null for re-upload - columnUpdationTime: null, - columnFileType: getInt(fileType), - }, - where: - '$columnLocalID = ? AND ($columnOwnerID = ? OR $columnOwnerID IS NULL)', - whereArgs: [localID, ownerID], + final db = await instance.sqliteAsyncDB; + + await db.execute( + ''' + UPDATE $filesTable + SET $columnTitle = ?, + $columnLatitude = ?, + $columnLongitude = ?, + $columnCreationTime = ?, + $columnModificationTime = ?, + $columnUpdationTime = NULL, + $columnFileType = ? + WHERE $columnLocalID = ? AND ($columnOwnerID = ? OR $columnOwnerID IS NULL); + ''', + [ + title, + location?.latitude, + location?.longitude, + creationTime, + modificationTime, + getInt(fileType), + localID, + ownerID, + ], ); } @@ -987,12 +1077,12 @@ class FilesDB { required String title, required String deviceFolder, }) async { - final db = await instance.database; + final db = await instance.sqliteAsyncDB; // on iOS, match using localID and fileType. title can either match or // might be null based on how the file was imported - String whereClause = ''' ($columnOwnerID = ? OR $columnOwnerID IS NULL) AND - $columnLocalID = ? AND $columnFileType = ? AND - ($columnTitle=? OR $columnTitle IS NULL) '''; + String query = '''SELECT * FROM $filesTable WHERE ($columnOwnerID = ? + OR $columnOwnerID IS NULL) AND $columnLocalID = ? + AND $columnFileType = ? AND ($columnTitle=? OR $columnTitle IS NULL) '''; List whereArgs = [ ownerID, localID, @@ -1000,9 +1090,9 @@ class FilesDB { title, ]; if (Platform.isAndroid) { - whereClause = ''' ($columnOwnerID = ? OR $columnOwnerID IS NULL) AND - $columnLocalID = ? AND $columnFileType = ? AND $columnTitle=? AND $columnDeviceFolder= ? - '''; + query = '''SELECT * FROM $filesTable WHERE ($columnOwnerID = ? OR + $columnOwnerID IS NULL) AND $columnLocalID = ? AND $columnFileType = ? + AND $columnTitle=? AND $columnDeviceFolder= ? '''; whereArgs = [ ownerID, localID, @@ -1012,10 +1102,9 @@ class FilesDB { ]; } - final rows = await db.query( - filesTable, - where: whereClause, - whereArgs: whereArgs, + final rows = await db.getAll( + query, + whereArgs, ); return convertToFiles(rows); @@ -1037,7 +1126,7 @@ class FilesDB { return {}; } final inParam = hashes.map((e) => "'$e'").join(','); - final rows = await db.execute(''' + final rows = await db.getAll(''' SELECT * FROM $filesTable WHERE $columnHash IN ($inParam) AND $columnOwnerID = $userID; '''); final matchedFiles = convertToFiles(rows); @@ -1053,14 +1142,12 @@ class FilesDB { if (fileType == FileType.livePhoto && hashData.zipHash != null) { inParam += ",'${hashData.zipHash}'"; } - final db = await instance.database; - final rows = await db.query( - filesTable, - where: '($columnUploadedFileID != NULL OR $columnUploadedFileID != -1) ' - 'AND $columnOwnerID = ? AND $columnFileType =' - ' ? ' - 'AND $columnHash IN ($inParam)', - whereArgs: [ + final db = await instance.sqliteAsyncDB; + final rows = await db.getAll( + 'SELECT * FROM $filesTable WHERE ($columnUploadedFileID != NULL OR ' + '$columnUploadedFileID != -1) AND $columnOwnerID = ? AND ' + '$columnFileType = ? AND $columnHash IN ($inParam)', + [ ownerID, getInt(fileType), ], @@ -1068,87 +1155,90 @@ class FilesDB { return convertToFiles(rows); } - Future update(EnteFile file) async { - final db = await instance.database; - return await db.update( - filesTable, - _getRowForFile(file), - where: '$columnGeneratedID = ?', - whereArgs: [file.generatedID], + Future update(EnteFile file) async { + final db = await instance.sqliteAsyncDB; + final parameterSet = _getParameterSetForFile(file)..add(file.generatedID); + final updateAssignments = _generateUpdateAssignmentsWithPlaceholders( + fileGenId: file.generatedID, + ); + await db.execute( + 'UPDATE $filesTable ' + 'SET $updateAssignments WHERE $columnGeneratedID = ?', + parameterSet, ); } - Future updateUploadedFileAcrossCollections(EnteFile file) async { - final db = await instance.database; - return await db.update( - filesTable, - _getRowForFileWithoutCollection(file), - where: '$columnUploadedFileID = ?', - whereArgs: [file.uploadedFileID], + Future updateUploadedFileAcrossCollections(EnteFile file) async { + final db = await instance.sqliteAsyncDB; + final parameterSet = _getParameterSetForFile(file, omitCollectionId: true) + ..add(file.uploadedFileID); + final updateAssignments = _generateUpdateAssignmentsWithPlaceholders( + fileGenId: file.generatedID, + omitCollectionId: true, + ); + await db.execute( + 'UPDATE $filesTable' + 'SET $updateAssignments WHERE $columnUploadedFileID = ?', + parameterSet, ); } - Future updateLocalIDForUploaded(int uploadedID, String localID) async { - final db = await instance.database; - return await db.update( - filesTable, - {columnLocalID: localID}, - where: '$columnUploadedFileID = ? AND $columnLocalID IS NULL', - whereArgs: [uploadedID], + Future updateLocalIDForUploaded(int uploadedID, String localID) async { + final db = await instance.sqliteAsyncDB; + await db.execute( + 'UPDATE $filesTable SET $columnLocalID = ? WHERE $columnUploadedFileID = ?' + ' AND $columnLocalID IS NULL', + [localID, uploadedID], ); } - Future delete(int uploadedFileID) async { - final db = await instance.database; - return db.delete( - filesTable, - where: '$columnUploadedFileID =?', - whereArgs: [uploadedFileID], + Future deleteByGeneratedID(int genID) async { + final db = await instance.sqliteAsyncDB; + + await db.execute( + 'DELETE FROM $filesTable WHERE $columnGeneratedID = ?', + [genID], ); } - Future deleteByGeneratedID(int genID) async { - final db = await instance.database; - return db.delete( - filesTable, - where: '$columnGeneratedID =?', - whereArgs: [genID], + Future deleteMultipleUploadedFiles(List uploadedFileIDs) async { + final db = await instance.sqliteAsyncDB; + final inParam = uploadedFileIDs.join(','); + + await db.execute( + 'DELETE FROM $filesTable WHERE $columnUploadedFileID IN ($inParam)', ); } - Future deleteMultipleUploadedFiles(List uploadedFileIDs) async { - final db = await instance.database; - return await db.delete( - filesTable, - where: '$columnUploadedFileID IN (${uploadedFileIDs.join(', ')})', - ); - } - - Future deleteMultipleByGeneratedIDs(List generatedIDs) async { + Future deleteMultipleByGeneratedIDs(List generatedIDs) async { if (generatedIDs.isEmpty) { - return 0; + return; } - final db = await instance.database; - return await db.delete( - filesTable, - where: '$columnGeneratedID IN (${generatedIDs.join(', ')})', + + final db = await instance.sqliteAsyncDB; + final inParam = generatedIDs.join(','); + + await db.execute( + 'DELETE FROM $filesTable WHERE $columnGeneratedID IN ($inParam)', ); } - Future deleteLocalFile(EnteFile file) async { - final db = await instance.database; + Future deleteLocalFile(EnteFile file) async { + final db = await instance.sqliteAsyncDB; if (file.localID != null) { // delete all files with same local ID - return db.delete( - filesTable, - where: '$columnLocalID =?', - whereArgs: [file.localID], + unawaited( + db.execute( + 'DELETE FROM $filesTable WHERE $columnLocalID = ?', + [file.localID], + ), ); } else { - return db.delete( - filesTable, - where: '$columnGeneratedID =?', - whereArgs: [file.generatedID], + unawaited( + db.execute( + 'DELETE FROM $filesTable WHERE $columnGeneratedID = ?', + [file.generatedID], + ), ); } } @@ -1159,8 +1249,8 @@ class FilesDB { inParam += "'" + localID + "',"; } inParam = inParam.substring(0, inParam.length - 1); - final db = await instance.database; - await db.rawQuery( + final db = await instance.sqliteAsyncDB; + await db.execute( ''' UPDATE $filesTable SET $columnLocalID = NULL @@ -1175,34 +1265,30 @@ class FilesDB { inParam += "'" + localID + "',"; } inParam = inParam.substring(0, inParam.length - 1); - final db = await instance.database; - final results = await db.query( - filesTable, - where: '$columnLocalID IN ($inParam)', + final db = await instance.sqliteAsyncDB; + final results = await db.getAll( + ''' + SELECT * FROM $filesTable + WHERE $columnLocalID IN ($inParam); + ''', ); return convertToFiles(results); } - Future deleteUnSyncedLocalFiles(List localIDs) async { + Future deleteUnSyncedLocalFiles(List localIDs) async { String inParam = ""; for (final localID in localIDs) { inParam += "'" + localID + "',"; } inParam = inParam.substring(0, inParam.length - 1); - final db = await instance.database; - return db.delete( - filesTable, - where: - '($columnUploadedFileID is NULL OR $columnUploadedFileID = -1 ) AND $columnLocalID IN ($inParam)', - ); - } - - Future deleteFromCollection(int uploadedFileID, int collectionID) async { - final db = await instance.database; - return db.delete( - filesTable, - where: '$columnUploadedFileID = ? AND $columnCollectionID = ?', - whereArgs: [uploadedFileID, collectionID], + final db = await instance.sqliteAsyncDB; + unawaited( + db.execute( + ''' + DELETE FROM $filesTable + WHERE ($columnUploadedFileID is NULL OR $columnUploadedFileID = -1 ) AND $columnLocalID IN ($inParam) + ''', + ), ); } @@ -1210,24 +1296,27 @@ class FilesDB { int collectionID, List uploadedFileIDs, ) async { - final db = await instance.database; - return db.delete( - filesTable, - where: - '$columnCollectionID = ? AND $columnUploadedFileID IN (${uploadedFileIDs.join(', ')})', - whereArgs: [collectionID], - ); + final db = await instance.sqliteAsyncDB; + return db.writeTransaction((tx) async { + await tx.execute( + ''' + DELETE FROM $filesTable + WHERE $columnCollectionID = ? AND $columnUploadedFileID IN (${uploadedFileIDs.join(', ')}); + ''', + [collectionID], + ); + final res = await tx.get('SELECT changes()'); + return res['changes()'] as int; + }); } Future collectionFileCount(int collectionID) async { - final db = await instance.database; - final count = Sqflite.firstIntValue( - await db.rawQuery( - 'SELECT COUNT(*) FROM $filesTable where $columnCollectionID = ' - '$collectionID AND $columnUploadedFileID IS NOT -1', - ), + final db = await instance.sqliteAsyncDB; + final row = await db.get( + 'SELECT COUNT(*) FROM $filesTable where $columnCollectionID = ' + '$collectionID AND $columnUploadedFileID IS NOT -1', ); - return count ?? 0; + return row['COUNT(*)'] as int; } Future archivedFilesCount( @@ -1235,43 +1324,45 @@ class FilesDB { int ownerID, Set hiddenCollections, ) async { - final db = await instance.database; - final count = Sqflite.firstIntValue( - await db.rawQuery( - 'SELECT COUNT(distinct($columnUploadedFileID)) FROM $filesTable where ' - '$columnMMdVisibility' - ' = $visibility AND $columnOwnerID = $ownerID AND $columnCollectionID NOT IN (${hiddenCollections.join(', ')})', + final db = await instance.sqliteAsyncDB; + final count = await db.getAll( + 'SELECT COUNT(distinct($columnUploadedFileID)) as COUNT FROM $filesTable where ' + '$columnMMdVisibility' + ' = $visibility AND $columnOwnerID = $ownerID AND $columnCollectionID NOT IN (${hiddenCollections.join(', ')})', + ); + return count.first['COUNT'] as int; + } + + Future deleteCollection(int collectionID) async { + final db = await instance.sqliteAsyncDB; + unawaited( + db.execute( + 'DELETE FROM $filesTable WHERE $columnCollectionID = ?', + [collectionID], ), ); - return count ?? 0; } - Future deleteCollection(int collectionID) async { - final db = await instance.database; - return db.delete( - filesTable, - where: '$columnCollectionID = ?', - whereArgs: [collectionID], - ); - } - - Future removeFromCollection(int collectionID, List fileIDs) async { - final db = await instance.database; - return db.delete( - filesTable, - where: - '$columnCollectionID =? AND $columnUploadedFileID IN (${fileIDs.join(', ')})', - whereArgs: [collectionID], + Future removeFromCollection(int collectionID, List fileIDs) async { + final db = await instance.sqliteAsyncDB; + final inParam = fileIDs.join(','); + unawaited( + db.execute( + ''' + DELETE FROM $filesTable + WHERE $columnCollectionID = ? AND $columnUploadedFileID IN ($inParam); + ''', + [collectionID], + ), ); } Future> getPendingUploadForCollection(int collectionID) async { - final db = await instance.database; - final results = await db.query( - filesTable, - where: '$columnCollectionID = ? AND ($columnUploadedFileID IS NULL OR ' - '$columnUploadedFileID = -1)', - whereArgs: [collectionID], + final db = await instance.sqliteAsyncDB; + final results = await db.getAll( + 'SELECT * FROM $filesTable WHERE $columnCollectionID = ? AND ' + '($columnUploadedFileID IS NULL OR $columnUploadedFileID = -1)', + [collectionID], ); return convertToFiles(results); } @@ -1287,8 +1378,8 @@ class FilesDB { } } inParam = inParam.substring(0, inParam.length - 1); - final db = await instance.database; - final rows = await db.rawQuery( + final db = await instance.sqliteAsyncDB; + final rows = await db.getAll( ''' SELECT $columnLocalID FROM $filesTable @@ -1307,8 +1398,8 @@ class FilesDB { // creationTime of the files in the collection. Future> getCollectionIDToMaxCreationTime() async { final enteWatch = EnteWatch("getCollectionIDToMaxCreationTime")..start(); - final db = await instance.database; - final rows = await db.rawQuery( + final db = await instance.sqliteAsyncDB; + final rows = await db.getAll( ''' SELECT $columnCollectionID, MAX($columnCreationTime) AS max_creation_time FROM $filesTable @@ -1350,16 +1441,17 @@ class FilesDB { int collectionID, bool sortAsc, ) async { - final db = await instance.database; + final db = await instance.sqliteAsyncDB; final order = sortAsc ? 'ASC' : 'DESC'; - final rows = await db.query( - filesTable, - where: '$columnCollectionID = ? AND ($columnUploadedFileID IS NOT NULL ' - 'AND $columnUploadedFileID IS NOT -1)', - whereArgs: [collectionID], - orderBy: - '$columnCreationTime ' + order + ', $columnModificationTime ' + order, - limit: 1, + final rows = await db.getAll( + ''' + SELECT * FROM $filesTable + WHERE $columnCollectionID = ? AND ($columnUploadedFileID IS NOT NULL + AND $columnUploadedFileID IS NOT -1) + ORDER BY $columnCreationTime $order, $columnModificationTime $order + LIMIT 1; + ''', + [collectionID], ); if (rows.isEmpty) { return null; @@ -1376,8 +1468,8 @@ class FilesDB { inParam += "'" + localID + "',"; } inParam = inParam.substring(0, inParam.length - 1); - final db = await instance.database; - await db.rawUpdate( + final db = await instance.sqliteAsyncDB; + await db.execute( ''' UPDATE $filesTable SET $columnUpdationTime = NULL @@ -1391,12 +1483,11 @@ class FilesDB { int uploadedFileID, int collectionID, ) async { - final db = await instance.database; - final rows = await db.query( - filesTable, - where: '$columnUploadedFileID = ? AND $columnCollectionID = ?', - whereArgs: [uploadedFileID, collectionID], - limit: 1, + final db = await instance.sqliteAsyncDB; + final rows = await db.getAll( + 'SELECT * FROM $filesTable WHERE $columnUploadedFileID = ? AND ' + '$columnCollectionID = ? LIMIT 1', + [uploadedFileID, collectionID], ); return rows.isNotEmpty; } @@ -1411,10 +1502,9 @@ class FilesDB { inParam += "'" + id.toString() + "',"; } inParam = inParam.substring(0, inParam.length - 1); - final db = await instance.database; - final results = await db.query( - filesTable, - where: '$columnUploadedFileID IN ($inParam)', + final db = await instance.sqliteAsyncDB; + final results = await db.getAll( + 'SELECT * FROM $filesTable WHERE $columnUploadedFileID IN ($inParam)', ); final files = convertToFiles(results); for (final file in files) { @@ -1433,10 +1523,9 @@ class FilesDB { inParam += "'" + id.toString() + "',"; } inParam = inParam.substring(0, inParam.length - 1); - final db = await instance.database; - final results = await db.query( - filesTable, - where: '$columnGeneratedID IN ($inParam)', + final db = await instance.sqliteAsyncDB; + final results = await db.getAll( + 'SELECT * FROM $filesTable WHERE $columnGeneratedID IN ($inParam)', ); final files = convertToFiles(results); for (final file in files) { @@ -1457,10 +1546,9 @@ class FilesDB { inParam += "'" + id.toString() + "',"; } inParam = inParam.substring(0, inParam.length - 1); - final db = await instance.database; - final results = await db.query( - filesTable, - where: '$columnUploadedFileID IN ($inParam)', + final db = await instance.sqliteAsyncDB; + final results = await db.getAll( + 'SELECT * FROM $filesTable WHERE $columnUploadedFileID IN ($inParam)', ); final files = convertToFiles(results); for (EnteFile eachFile in files) { @@ -1475,13 +1563,13 @@ class FilesDB { Future> getAllCollectionIDsOfFile( int uploadedFileID, ) async { - final db = await instance.database; - final results = await db.query( - filesTable, - where: '$columnUploadedFileID = ? AND $columnCollectionID != -1', - columns: [columnCollectionID], - whereArgs: [uploadedFileID], - distinct: true, + final db = await instance.sqliteAsyncDB; + final results = await db.getAll( + ''' + SELECT DISTINCT $columnCollectionID FROM $filesTable + WHERE $columnUploadedFileID = ? AND $columnCollectionID != -1 + ''', + [uploadedFileID], ); final collectionIDsOfFile = {}; for (var result in results) { @@ -1510,14 +1598,13 @@ class FilesDB { int cutOffTime, int ownerID, ) async { - final db = await instance.database; - final rows = await db.query( - filesTable, - columns: [columnGeneratedID], - distinct: true, - where: - '$columnCreationTime <= ? AND ($columnOwnerID IS NULL OR $columnOwnerID = ?)', - whereArgs: [cutOffTime, ownerID], + final db = await instance.sqliteAsyncDB; + final rows = await db.getAll( + ''' + SELECT DISTINCT $columnGeneratedID FROM $filesTable + WHERE $columnCreationTime <= ? AND ($columnOwnerID IS NULL OR $columnOwnerID = ?) + ''', + [cutOffTime, ownerID], ); final result = []; for (final row in rows) { @@ -1529,15 +1616,14 @@ class FilesDB { // For givenUserID, get List of unique LocalIDs for files which are // uploaded by the given user and location is missing Future> getLocalIDsForFilesWithoutLocation(int ownerID) async { - final db = await instance.database; - final rows = await db.query( - filesTable, - columns: [columnLocalID], - distinct: true, - where: '$columnOwnerID = ? AND $columnLocalID IS NOT NULL AND ' - '($columnLatitude IS NULL OR ' - '$columnLongitude IS NULL OR $columnLongitude = 0.0 or $columnLongitude = 0.0)', - whereArgs: [ownerID], + final db = await instance.sqliteAsyncDB; + final rows = await db.getAll( + ''' + SELECT DISTINCT $columnLocalID FROM $filesTable + WHERE $columnOwnerID = ? AND $columnLocalID IS NOT NULL AND + ($columnLatitude IS NULL OR $columnLongitude IS NULL OR $columnLatitude = 0.0 or $columnLongitude = 0.0) + ''', + [ownerID], ); final result = []; for (final row in rows) { @@ -1548,13 +1634,13 @@ class FilesDB { // For a given userID, return unique uploadedFileId for the given userID Future> getUploadIDsWithMissingSize(int userId) async { - final db = await instance.database; - final rows = await db.query( - filesTable, - columns: [columnUploadedFileID], - distinct: true, - where: '$columnOwnerID = ? AND $columnFileSize IS NULL', - whereArgs: [userId], + final db = await instance.sqliteAsyncDB; + final rows = await db.getAll( + ''' + SELECT DISTINCT $columnUploadedFileID FROM $filesTable + WHERE $columnOwnerID = ? AND $columnFileSize IS NULL + ''', + [userId], ); final result = []; for (final row in rows) { @@ -1565,14 +1651,13 @@ class FilesDB { // For a given userID, return unique localID for all uploaded live photos Future> getLivePhotosForUser(int userId) async { - final db = await instance.database; - final rows = await db.query( - filesTable, - columns: [columnLocalID], - distinct: true, - where: '$columnOwnerID = ? AND ' - '$columnFileType = ? AND $columnLocalID IS NOT NULL', - whereArgs: [userId, getInt(FileType.livePhoto)], + final db = await instance.sqliteAsyncDB; + final rows = await db.getAll( + ''' + SELECT DISTINCT $columnLocalID FROM $filesTable + WHERE $columnOwnerID = ? AND $columnFileType = ? AND $columnLocalID IS NOT NULL + ''', + [userId, getInt(FileType.livePhoto)], ); final result = []; for (final row in rows) { @@ -1582,15 +1667,16 @@ class FilesDB { } Future> getLocalFilesBackedUpWithoutLocation(int userId) async { - final db = await instance.database; - final rows = await db.query( - filesTable, - columns: [columnLocalID], - distinct: true, - where: - '$columnOwnerID = ? AND $columnLocalID IS NOT NULL AND ($columnUploadedFileID IS NOT NULL AND $columnUploadedFileID IS NOT -1) ' - 'AND ($columnLatitude IS NULL OR $columnLongitude IS NULL OR $columnLongitude = 0.0 or $columnLongitude = 0.0)', - whereArgs: [userId], + final db = await instance.sqliteAsyncDB; + final rows = await db.getAll( + ''' + SELECT DISTINCT $columnLocalID FROM $filesTable + WHERE $columnOwnerID = ? AND $columnLocalID IS NOT NULL AND + ($columnUploadedFileID IS NOT NULL AND $columnUploadedFileID IS NOT -1) + AND ($columnLatitude IS NULL OR $columnLongitude IS NULL OR + $columnLatitude = 0.0 or $columnLongitude = 0.0) + ''', + [userId], ); final result = []; for (final row in rows) { @@ -1607,17 +1693,24 @@ class FilesDB { if (uploadedFileIDToSize.isEmpty) { return; } - final db = await instance.database; - final batch = db.batch(); + final db = await instance.sqliteAsyncDB; + final parameterSets = >[]; + for (final uploadedFileID in uploadedFileIDToSize.keys) { - batch.update( - filesTable, - {columnFileSize: uploadedFileIDToSize[uploadedFileID]}, - where: '$columnUploadedFileID = ?', - whereArgs: [uploadedFileID], - ); + parameterSets.add([ + uploadedFileIDToSize[uploadedFileID], + uploadedFileID, + ]); } - await batch.commit(noResult: true); + + await db.executeBatch( + ''' + UPDATE $filesTable + SET $columnFileSize = ? + WHERE $columnUploadedFileID = ?; + ''', + parameterSets, + ); } Future> getAllFilesFromDB( @@ -1642,9 +1735,13 @@ class FilesDB { } Future> fetchFilesCountbyType(int userID) async { - final db = await instance.database; - final result = await db.rawQuery( - "SELECT $columnFileType, COUNT(DISTINCT $columnUploadedFileID) FROM $filesTable WHERE $columnUploadedFileID != -1 AND $columnOwnerID == $userID GROUP BY $columnFileType", + final db = await instance.sqliteAsyncDB; + final result = await db.getAll( + ''' + SELECT $columnFileType, COUNT(DISTINCT $columnUploadedFileID) + FROM $filesTable WHERE $columnUploadedFileID != -1 AND + $columnOwnerID IS $userID GROUP BY $columnFileType + ''', ); final filesCount = {}; @@ -1663,18 +1760,28 @@ class FilesDB { bool? asc, required DBFilterOptions? filterOptions, }) async { - final db = await instance.database; + final db = await instance.sqliteAsyncDB; final order = (asc ?? false ? 'ASC' : 'DESC'); - final results = await db.query( - filesTable, - where: - '$columnLatitude IS NOT NULL AND $columnLongitude IS NOT NULL AND ($columnLatitude IS NOT 0 OR $columnLongitude IS NOT 0)' - ' AND $columnCreationTime >= ? AND $columnCreationTime <= ?' - ' AND ($columnLocalID IS NOT NULL OR ($columnCollectionID IS NOT NULL AND $columnCollectionID IS NOT -1))', - whereArgs: [startTime, endTime], - orderBy: - '$columnCreationTime ' + order + ', $columnModificationTime ' + order, - limit: limit, + String query = ''' + SELECT * FROM $filesTable + WHERE $columnLatitude IS NOT NULL AND $columnLongitude IS NOT NULL AND + ($columnLatitude IS NOT 0 OR $columnLongitude IS NOT 0) AND + $columnCreationTime >= ? AND $columnCreationTime <= ? AND + ($columnLocalID IS NOT NULL OR ($columnCollectionID IS NOT NULL AND + $columnCollectionID IS NOT -1)) + ORDER BY $columnCreationTime $order, $columnModificationTime $order + '''; + + final args = [startTime, endTime]; + + if (limit != null) { + query += ' LIMIT ?'; + args.add(limit); + } + + final results = await db.getAll( + query, + args, ); final files = convertToFiles(results); final List filteredFiles = @@ -1718,100 +1825,144 @@ class FilesDB { return convertToFiles(results); } - Map _getRowForFile(EnteFile file) { - final row = {}; - if (file.generatedID != null) { - row[columnGeneratedID] = file.generatedID; + ///Returns "columnName1 = ?, columnName2 = ?, ..." + String _generateUpdateAssignmentsWithPlaceholders({ + required int? fileGenId, + bool omitCollectionId = false, + }) { + final assignments = []; + + for (String columnName in _columnNames) { + if (columnName == columnGeneratedID && fileGenId == null) { + continue; + } + if (columnName == columnCollectionID && omitCollectionId) { + continue; + } + assignments.add("$columnName = ?"); } - row[columnLocalID] = file.localID; - row[columnUploadedFileID] = file.uploadedFileID ?? -1; - row[columnOwnerID] = file.ownerID; - row[columnCollectionID] = file.collectionID ?? -1; - row[columnTitle] = file.title; - row[columnDeviceFolder] = file.deviceFolder; - // if (file.location == null || - // (file.location!.latitude == null && file.location!.longitude == null)) { - // file.location = Location.randomLocation(); - // } - if (file.location != null) { - row[columnLatitude] = file.location!.latitude; - row[columnLongitude] = file.location!.longitude; + + return assignments.join(","); + } + + Map _generateColumnsAndPlaceholdersForInsert({ + required int? fileGenId, + }) { + final columnNames = []; + + for (String columnName in _columnNames) { + if (columnName == columnGeneratedID && fileGenId == null) { + continue; + } + + columnNames.add(columnName); } - row[columnFileType] = getInt(file.fileType); - row[columnCreationTime] = file.creationTime; - row[columnModificationTime] = file.modificationTime; - row[columnUpdationTime] = file.updationTime; - row[columnAddedTime] = - file.addedTime ?? DateTime.now().microsecondsSinceEpoch; - row[columnEncryptedKey] = file.encryptedKey; - row[columnKeyDecryptionNonce] = file.keyDecryptionNonce; - row[columnFileDecryptionHeader] = file.fileDecryptionHeader; - row[columnThumbnailDecryptionHeader] = file.thumbnailDecryptionHeader; - row[columnMetadataDecryptionHeader] = file.metadataDecryptionHeader; - row[columnFileSubType] = file.fileSubType ?? -1; - row[columnDuration] = file.duration ?? 0; - row[columnExif] = file.exif; - row[columnHash] = file.hash; - row[columnMetadataVersion] = file.metadataVersion; - row[columnFileSize] = file.fileSize; - row[columnMMdVersion] = file.mMdVersion; - row[columnMMdEncodedJson] = file.mMdEncodedJson ?? '{}'; - row[columnMMdVisibility] = file.magicMetadata.visibility; - row[columnPubMMdVersion] = file.pubMmdVersion; - row[columnPubMMdEncodedJson] = file.pubMmdEncodedJson ?? '{}'; - // override existing fields to avoid re-writing all queries and logic + + return { + "columns": columnNames.join(","), + "placeholders": List.filled(columnNames.length, "?").join(","), + }; + } + + List _getParameterSetForFile( + EnteFile file, { + bool omitCollectionId = false, + }) { + final values = []; + + double? latitude = file.location?.latitude; + double? longitude = file.location?.longitude; + + int? creationTime = file.creationTime; if (file.pubMagicMetadata != null) { if (file.pubMagicMetadata!.editedTime != null) { - row[columnCreationTime] = file.pubMagicMetadata!.editedTime; + creationTime = file.pubMagicMetadata!.editedTime; } if (file.pubMagicMetadata!.lat != null && file.pubMagicMetadata!.long != null) { - row[columnLatitude] = file.pubMagicMetadata!.lat; - row[columnLongitude] = file.pubMagicMetadata!.long; + latitude = file.pubMagicMetadata!.lat; + longitude = file.pubMagicMetadata!.long; } } - return row; + + if (file.generatedID != null) { + values.add(file.generatedID); + } + values.addAll([ + file.localID, + file.uploadedFileID ?? -1, + file.ownerID, + file.collectionID ?? -1, + file.title, + file.deviceFolder, + latitude, + longitude, + getInt(file.fileType), + file.modificationTime, + file.encryptedKey, + file.keyDecryptionNonce, + file.fileDecryptionHeader, + file.thumbnailDecryptionHeader, + file.metadataDecryptionHeader, + creationTime, + file.updationTime, + file.fileSubType ?? -1, + file.duration ?? 0, + file.exif, + file.hash, + file.metadataVersion, + file.mMdEncodedJson ?? '{}', + file.mMdVersion, + file.magicMetadata.visibility, + file.pubMmdEncodedJson ?? '{}', + file.pubMmdVersion, + file.fileSize, + file.addedTime ?? DateTime.now().microsecondsSinceEpoch, + ]); + + if (omitCollectionId) { + values.removeAt(3); + } + + return values; } - Map _getRowForFileWithoutCollection(EnteFile file) { - final row = {}; - row[columnLocalID] = file.localID; - row[columnUploadedFileID] = file.uploadedFileID ?? -1; - row[columnOwnerID] = file.ownerID; - row[columnTitle] = file.title; - row[columnDeviceFolder] = file.deviceFolder; - if (file.location != null) { - row[columnLatitude] = file.location!.latitude; - row[columnLongitude] = file.location!.longitude; - } - row[columnFileType] = getInt(file.fileType); - row[columnCreationTime] = file.creationTime; - row[columnModificationTime] = file.modificationTime; - row[columnUpdationTime] = file.updationTime; - row[columnAddedTime] = - file.addedTime ?? DateTime.now().microsecondsSinceEpoch; - row[columnFileDecryptionHeader] = file.fileDecryptionHeader; - row[columnThumbnailDecryptionHeader] = file.thumbnailDecryptionHeader; - row[columnMetadataDecryptionHeader] = file.metadataDecryptionHeader; - row[columnFileSubType] = file.fileSubType ?? -1; - row[columnDuration] = file.duration ?? 0; - row[columnExif] = file.exif; - row[columnHash] = file.hash; - row[columnMetadataVersion] = file.metadataVersion; + Future _batchAndInsertFile( + EnteFile file, + SqliteAsyncConflictAlgorithm conflictAlgorithm, + SqliteDatabase db, + List> parameterSets, + PrimitiveWrapper batchCounter, { + required bool isGenIdNull, + }) async { + parameterSets.add(_getParameterSetForFile(file)); + batchCounter.value++; - row[columnMMdVersion] = file.mMdVersion; - row[columnMMdEncodedJson] = file.mMdEncodedJson ?? '{}'; - row[columnMMdVisibility] = file.magicMetadata.visibility; - - row[columnPubMMdVersion] = file.pubMmdVersion; - row[columnPubMMdEncodedJson] = file.pubMmdEncodedJson ?? '{}'; - if (file.pubMagicMetadata != null && - file.pubMagicMetadata!.editedTime != null) { - // override existing creationTime to avoid re-writing all queries related - // to loading the gallery - row[columnCreationTime] = file.pubMagicMetadata!.editedTime!; + 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(); } - return row; + } + + Future _insertBatch( + SqliteAsyncConflictAlgorithm conflictAlgorithm, + Iterable columnNames, + SqliteDatabase db, + List> parameterSets, + ) async { + final valuesPlaceholders = List.filled(columnNames.length, "?").join(","); + final columnNamesJoined = columnNames.join(","); + await db.executeBatch( + ''' + INSERT OR ${conflictAlgorithm.name.toUpperCase()} INTO $filesTable($columnNamesJoined) VALUES($valuesPlaceholders) + ''', + parameterSets, + ); } EnteFile _getFileFromRow(Map row) { diff --git a/mobile/lib/face/db.dart b/mobile/lib/face/db.dart index 3ad90915d4..7ff180efed 100644 --- a/mobile/lib/face/db.dart +++ b/mobile/lib/face/db.dart @@ -35,6 +35,15 @@ class FaceMLDataDB { static final FaceMLDataDB instance = FaceMLDataDB._privateConstructor(); + static final _migrationScripts = [ + createFacesTable, + createFaceClustersTable, + createClusterPersonTable, + createClusterSummaryTable, + createNotPersonFeedbackTable, + fcClusterIDIndex, + ]; + // only have a single app-wide reference to the database static Future? _sqliteAsyncDBFuture; @@ -50,17 +59,42 @@ class FaceMLDataDB { _logger.info("Opening sqlite_async access: DB path " + databaseDirectory); final asyncDBConnection = SqliteDatabase(path: databaseDirectory, maxReaders: 2); - await _onCreate(asyncDBConnection); + final stopwatch = Stopwatch()..start(); + _logger.info("FaceMLDataDB: Starting migration"); + await _migrate(asyncDBConnection); + _logger.info( + "FaceMLDataDB Migration took ${stopwatch.elapsedMilliseconds} ms", + ); + stopwatch.stop(); + return asyncDBConnection; } - Future _onCreate(SqliteDatabase asyncDBConnection) async { - await asyncDBConnection.execute(createFacesTable); - await asyncDBConnection.execute(createFaceClustersTable); - await asyncDBConnection.execute(createClusterPersonTable); - await asyncDBConnection.execute(createClusterSummaryTable); - await asyncDBConnection.execute(createNotPersonFeedbackTable); - await asyncDBConnection.execute(fcClusterIDIndex); + Future _migrate( + SqliteDatabase database, + ) async { + final result = await database.execute('PRAGMA user_version'); + final currentVersion = result[0]['user_version'] as int; + final toVersion = _migrationScripts.length; + + if (currentVersion < toVersion) { + _logger.info("Migrating database from $currentVersion to $toVersion"); + await database.writeTransaction((tx) async { + for (int i = currentVersion + 1; i <= toVersion; i++) { + try { + await tx.execute(_migrationScripts[i - 1]); + } catch (e) { + _logger.severe("Error running migration script index ${i - 1}", e); + rethrow; + } + } + await tx.execute('PRAGMA user_version = $toVersion'); + }); + } else if (currentVersion > toVersion) { + throw AssertionError( + "currentVersion($currentVersion) cannot be greater than toVersion($toVersion)", + ); + } } // bulkInsertFaces inserts the faces in the database in batches of 1000. @@ -195,10 +229,10 @@ class FaceMLDataDB { final db = await instance.asyncDB; await db.execute(deleteFacesTable); - await db.execute(dropClusterPersonTable); - await db.execute(dropClusterSummaryTable); - await db.execute(deletePersonTable); - await db.execute(dropNotPersonFeedbackTable); + await db.execute(deleteFaceClustersTable); + await db.execute(deleteClusterPersonTable); + await db.execute(deleteClusterSummaryTable); + await db.execute(deleteNotPersonFeedbackTable); } Future> getFaceEmbeddingsForCluster( @@ -734,7 +768,7 @@ class FaceMLDataDB { try { final db = await instance.asyncDB; - await db.execute(dropFaceClustersTable); + await db.execute(deleteFaceClustersTable); await db.execute(createFaceClustersTable); await db.execute(fcClusterIDIndex); } catch (e, s) { @@ -807,6 +841,18 @@ class FaceMLDataDB { await db.executeBatch(sql, parameterSets); } + Future removeNotPersonFeedback({ + required String personID, + required int clusterID, + }) async { + final db = await instance.asyncDB; + + const String sql = ''' + DELETE FROM $notPersonFeedback WHERE $personIdColumn = ? AND $clusterIDColumn = ? + '''; + await db.execute(sql, [personID, clusterID]); + } + Future removeClusterToPerson({ required String personID, required int clusterID, @@ -945,16 +991,15 @@ class FaceMLDataDB { if (faces) { await db.execute(deleteFacesTable); await db.execute(createFacesTable); - await db.execute(dropFaceClustersTable); + await db.execute(deleteFaceClustersTable); await db.execute(createFaceClustersTable); await db.execute(fcClusterIDIndex); } - await db.execute(deletePersonTable); - await db.execute(dropClusterPersonTable); - await db.execute(dropNotPersonFeedbackTable); - await db.execute(dropClusterSummaryTable); - await db.execute(dropFaceClustersTable); + await db.execute(deleteClusterPersonTable); + await db.execute(deleteNotPersonFeedbackTable); + await db.execute(deleteClusterSummaryTable); + await db.execute(deleteFaceClustersTable); await db.execute(createClusterPersonTable); await db.execute(createNotPersonFeedbackTable); @@ -972,9 +1017,8 @@ class FaceMLDataDB { final db = await instance.asyncDB; // Drop the tables - await db.execute(deletePersonTable); - await db.execute(dropClusterPersonTable); - await db.execute(dropNotPersonFeedbackTable); + await db.execute(deleteClusterPersonTable); + await db.execute(deleteNotPersonFeedbackTable); // Recreate the tables await db.execute(createClusterPersonTable); diff --git a/mobile/lib/face/db_fields.dart b/mobile/lib/face/db_fields.dart index e6a70a7d4e..8ad14ae282 100644 --- a/mobile/lib/face/db_fields.dart +++ b/mobile/lib/face/db_fields.dart @@ -29,7 +29,7 @@ const createFacesTable = '''CREATE TABLE IF NOT EXISTS $facesTable ( ); '''; -const deleteFacesTable = 'DROP TABLE IF EXISTS $facesTable'; +const deleteFacesTable = 'DELETE FROM $facesTable'; // End of Faces Table Fields & Schema Queries //##region Face Clusters Table Fields & Schema Queries @@ -48,15 +48,9 @@ CREATE TABLE IF NOT EXISTS $faceClustersTable ( // -- Creating a non-unique index on clusterID for query optimization const fcClusterIDIndex = '''CREATE INDEX IF NOT EXISTS idx_fcClusterID ON $faceClustersTable($fcClusterID);'''; -const dropFaceClustersTable = 'DROP TABLE IF EXISTS $faceClustersTable'; +const deleteFaceClustersTable = 'DELETE FROM $faceClustersTable'; //##endregion -// People Table Fields & Schema Queries -const personTable = 'person'; - -const deletePersonTable = 'DROP TABLE IF EXISTS $personTable'; -//End People Table Fields & Schema Queries - // Clusters Table Fields & Schema Queries const clusterPersonTable = 'cluster_person'; const personIdColumn = 'person_id'; @@ -69,7 +63,7 @@ CREATE TABLE IF NOT EXISTS $clusterPersonTable ( PRIMARY KEY($personIdColumn, $clusterIDColumn) ); '''; -const dropClusterPersonTable = 'DROP TABLE IF EXISTS $clusterPersonTable'; +const deleteClusterPersonTable = 'DELETE FROM $clusterPersonTable'; // End Clusters Table Fields & Schema Queries /// Cluster Summary Table Fields & Schema Queries @@ -85,7 +79,7 @@ CREATE TABLE IF NOT EXISTS $clusterSummaryTable ( ); '''; -const dropClusterSummaryTable = 'DROP TABLE IF EXISTS $clusterSummaryTable'; +const deleteClusterSummaryTable = 'DELETE FROM $clusterSummaryTable'; /// End Cluster Summary Table Fields & Schema Queries @@ -99,5 +93,5 @@ CREATE TABLE IF NOT EXISTS $notPersonFeedback ( PRIMARY KEY($personIdColumn, $clusterIDColumn) ); '''; -const dropNotPersonFeedbackTable = 'DROP TABLE IF EXISTS $notPersonFeedback'; +const deleteNotPersonFeedbackTable = 'DELETE FROM $notPersonFeedback'; // End Clusters Table Fields & Schema Queries diff --git a/mobile/lib/generated/intl/messages_cs.dart b/mobile/lib/generated/intl/messages_cs.dart index 6301af5611..3de2f825b3 100644 --- a/mobile/lib/generated/intl/messages_cs.dart +++ b/mobile/lib/generated/intl/messages_cs.dart @@ -50,8 +50,6 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Enter person name"), "faceRecognition": MessageLookupByLibrary.simpleMessage("Face recognition"), - "faceRecognitionIndexingDescription": MessageLookupByLibrary.simpleMessage( - "Please note that this will result in a higher bandwidth and battery usage until all items are indexed."), "fileTypes": MessageLookupByLibrary.simpleMessage("File types"), "foundFaces": MessageLookupByLibrary.simpleMessage("Found faces"), "indexingIsPaused": MessageLookupByLibrary.simpleMessage( diff --git a/mobile/lib/generated/intl/messages_de.dart b/mobile/lib/generated/intl/messages_de.dart index 49c7ac93c6..9c398a4b57 100644 --- a/mobile/lib/generated/intl/messages_de.dart +++ b/mobile/lib/generated/intl/messages_de.dart @@ -41,13 +41,16 @@ class MessageLookup extends MessageLookupByLibrary { static String m7(versionValue) => "Version: ${versionValue}"; - static String m8(paymentProvider) => + static String m8(freeAmount, storageUnit) => + "${freeAmount} ${storageUnit} frei"; + + static String m9(paymentProvider) => "Bitte kĂŒndigen Sie Ihr aktuelles Abo ĂŒber ${paymentProvider} zuerst"; - static String m9(user) => + static String m10(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 m10(isFamilyMember, storageAmountInGb) => + static String m11(isFamilyMember, storageAmountInGb) => "${Intl.select(isFamilyMember, { 'true': 'Deine Familiengruppe hat bereits ${storageAmountInGb} GB erhalten', @@ -55,50 +58,47 @@ class MessageLookup extends MessageLookupByLibrary { 'other': 'Du hast bereits ${storageAmountInGb} GB erhalten!', })}"; - static String m11(albumName) => + static String m12(albumName) => "Kollaborativer Link fĂŒr ${albumName} erstellt"; - static String m12(familyAdminEmail) => + static String m13(familyAdminEmail) => "Bitte kontaktiere ${familyAdminEmail} um dein Abo zu verwalten"; - static String m13(provider) => + static String m14(provider) => "Bitte kontaktieren Sie uns ĂŒber support@ente.io, um Ihr ${provider} Abo zu verwalten."; - static String m14(count) => + static String m15(count) => "${Intl.plural(count, one: 'Lösche ${count} Element', other: 'Lösche ${count} Elemente')}"; - static String m15(currentlyDeleting, totalCount) => + static String m16(currentlyDeleting, totalCount) => "Lösche ${currentlyDeleting} / ${totalCount}"; - static String m16(albumName) => + static String m17(albumName) => "Der öffentliche Link zum Zugriff auf \"${albumName}\" wird entfernt."; - static String m17(supportEmail) => + static String m18(supportEmail) => "Bitte sende eine E-Mail an ${supportEmail} von deiner registrierten E-Mail-Adresse"; - static String m18(count, storageSaved) => + static String m19(count, storageSaved) => "Du hast ${Intl.plural(count, one: '${count} duplizierte Datei', other: '${count} dupliziere Dateien')} gelöscht und (${storageSaved}!) freigegeben"; - static String m19(count, formattedSize) => + static String m20(count, formattedSize) => "${count} Dateien, ${formattedSize} jede"; - static String m20(newEmail) => "E-Mail-Adresse geĂ€ndert zu ${newEmail}"; + static String m21(newEmail) => "E-Mail-Adresse geĂ€ndert zu ${newEmail}"; - static String m21(email) => + static String m22(email) => "${email} hat kein Ente-Konto.\n\nSenden Sie eine Einladung, um Fotos zu teilen."; - static String m22(count, formattedNumber) => + static String m23(count, formattedNumber) => "${Intl.plural(count, one: '1 Datei', other: '${formattedNumber} Dateien')} auf diesem GerĂ€t wurde(n) sicher gespeichert"; - static String m23(count, formattedNumber) => + static String m24(count, formattedNumber) => "${Intl.plural(count, one: '1 Datei', other: '${formattedNumber} Dateien')} in diesem Album wurde(n) sicher gespeichert"; - static String m24(storageAmountInGB) => + static String m25(storageAmountInGB) => "${storageAmountInGB} GB jedes Mal, wenn sich jemand mit deinem Code fĂŒr einen bezahlten Tarif anmeldet"; - static String m25(freeAmount, storageUnit) => - "${freeAmount} ${storageUnit} kostenlos"; - static String m26(endDate) => "Kostenlose Demo verfĂŒgbar bis zum ${endDate}"; static String m27(count) => @@ -368,6 +368,7 @@ class MessageLookup extends MessageLookupByLibrary { "authenticationSuccessful": MessageLookupByLibrary.simpleMessage( "Authentifizierung erfogreich!"), "available": MessageLookupByLibrary.simpleMessage("VerfĂŒgbar"), + "availableStorageSpace": m8, "backedUpFolders": MessageLookupByLibrary.simpleMessage("Gesicherte Ordner"), "backup": MessageLookupByLibrary.simpleMessage("Backup"), @@ -393,10 +394,10 @@ class MessageLookup extends MessageLookupByLibrary { "canOnlyRemoveFilesOwnedByYou": MessageLookupByLibrary.simpleMessage( "Du kannst nur Dateien entfernen, die dir gehören"), "cancel": MessageLookupByLibrary.simpleMessage("Abbrechen"), - "cancelOtherSubscription": m8, + "cancelOtherSubscription": m9, "cancelSubscription": MessageLookupByLibrary.simpleMessage("Abonnement kĂŒndigen"), - "cannotAddMorePhotosAfterBecomingViewer": m9, + "cannotAddMorePhotosAfterBecomingViewer": m10, "cannotDeleteSharedFiles": MessageLookupByLibrary.simpleMessage( "Konnte geteilte Dateien nicht löschen"), "castInstruction": MessageLookupByLibrary.simpleMessage( @@ -421,7 +422,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Freien Speicher einlösen"), "claimMore": MessageLookupByLibrary.simpleMessage("Mehr einlösen!"), "claimed": MessageLookupByLibrary.simpleMessage("Eingelöst"), - "claimedStorageSoFar": m10, + "claimedStorageSoFar": m11, "cleanUncategorized": MessageLookupByLibrary.simpleMessage("Unkategorisiert leeren"), "cleanUncategorizedDescription": MessageLookupByLibrary.simpleMessage( @@ -448,7 +449,7 @@ class MessageLookup extends MessageLookupByLibrary { "Erstelle einen Link, um anderen zu ermöglichen, Fotos in deinem geteilten Album hinzuzufĂŒgen und zu sehen - ohne dass diese ein Konto von ente.io oder die App benötigen. Ideal, um Fotos von Events zu sammeln."), "collaborativeLink": MessageLookupByLibrary.simpleMessage("Gemeinschaftlicher Link"), - "collaborativeLinkCreatedFor": m11, + "collaborativeLinkCreatedFor": m12, "collaborator": MessageLookupByLibrary.simpleMessage("Bearbeiter"), "collaboratorsCanAddPhotosAndVideosToTheSharedAlbum": MessageLookupByLibrary.simpleMessage( @@ -475,10 +476,10 @@ class MessageLookup extends MessageLookupByLibrary { "WiederherstellungsschlĂŒssel bestĂ€tigen"), "confirmYourRecoveryKey": MessageLookupByLibrary.simpleMessage( "BestĂ€tigen Sie ihren WiederherstellungsschlĂŒssel"), - "contactFamilyAdmin": m12, + "contactFamilyAdmin": m13, "contactSupport": MessageLookupByLibrary.simpleMessage("Support kontaktieren"), - "contactToManageSubscription": m13, + "contactToManageSubscription": m14, "contacts": MessageLookupByLibrary.simpleMessage("Kontakte"), "contents": MessageLookupByLibrary.simpleMessage("Inhalte"), "continueLabel": MessageLookupByLibrary.simpleMessage("Weiter"), @@ -558,11 +559,11 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Vom GerĂ€t löschen"), "deleteFromEnte": MessageLookupByLibrary.simpleMessage("Auf ente.io löschen"), - "deleteItemCount": m14, + "deleteItemCount": m15, "deleteLocation": MessageLookupByLibrary.simpleMessage("Standort löschen"), "deletePhotos": MessageLookupByLibrary.simpleMessage("Fotos löschen"), - "deleteProgress": m15, + "deleteProgress": m16, "deleteReason1": MessageLookupByLibrary.simpleMessage( "Es fehlt eine zentrale Funktion, die ich benötige"), "deleteReason2": MessageLookupByLibrary.simpleMessage( @@ -582,8 +583,6 @@ class MessageLookup extends MessageLookupByLibrary { "designedToOutlive": MessageLookupByLibrary.simpleMessage("Entwickelt um zu bewahren"), "details": MessageLookupByLibrary.simpleMessage("Details"), - "devAccountChanged": MessageLookupByLibrary.simpleMessage( - "Das Entwicklerkonto, das wir verwenden, um ente im App Store zu veröffentlichen, hat sich geĂ€ndert. Aus diesem Grund musst du dich erneut anmelden.\n\nWir entschuldigen uns fĂŒr die Unannehmlichkeiten, aber das war unvermeidlich."), "deviceCodeHint": MessageLookupByLibrary.simpleMessage("Code eingeben"), "deviceFilesAutoUploading": MessageLookupByLibrary.simpleMessage( "Dateien, die zu diesem Album hinzugefĂŒgt werden, werden automatisch zu ente hochgeladen."), @@ -598,7 +597,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": m16, + "disableLinkMessage": m17, "disableTwofactor": MessageLookupByLibrary.simpleMessage( "Zweiten Faktor (2FA) deaktivieren"), "disablingTwofactorAuthentication": @@ -621,9 +620,9 @@ class MessageLookup extends MessageLookupByLibrary { "Herunterladen fehlgeschlagen"), "downloading": MessageLookupByLibrary.simpleMessage("Wird heruntergeladen..."), - "dropSupportEmail": m17, - "duplicateFileCountWithStorageSaved": m18, - "duplicateItemsGroup": m19, + "dropSupportEmail": m18, + "duplicateFileCountWithStorageSaved": m19, + "duplicateItemsGroup": m20, "edit": MessageLookupByLibrary.simpleMessage("Bearbeiten"), "editLocation": MessageLookupByLibrary.simpleMessage("Standort bearbeiten"), @@ -636,8 +635,8 @@ class MessageLookup extends MessageLookupByLibrary { "Änderungen des Standorts werden nur in ente sichtbar sein"), "eligible": MessageLookupByLibrary.simpleMessage("zulĂ€ssig"), "email": MessageLookupByLibrary.simpleMessage("E-Mail"), - "emailChangedTo": m20, - "emailNoEnteAccount": m21, + "emailChangedTo": m21, + "emailNoEnteAccount": m22, "emailVerificationToggle": MessageLookupByLibrary.simpleMessage("E-Mail-Verifizierung"), "emailYourLogs": MessageLookupByLibrary.simpleMessage( @@ -706,8 +705,6 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Daten exportieren"), "faceRecognition": MessageLookupByLibrary.simpleMessage("Face recognition"), - "faceRecognitionIndexingDescription": MessageLookupByLibrary.simpleMessage( - "Please note that this will result in a higher bandwidth and battery usage until all items are indexed."), "faces": MessageLookupByLibrary.simpleMessage("Gesichter"), "failedToApplyCode": MessageLookupByLibrary.simpleMessage( "Der Code konnte nicht aktiviert werden"), @@ -743,8 +740,8 @@ class MessageLookup extends MessageLookupByLibrary { "fileTypes": MessageLookupByLibrary.simpleMessage("Dateitypen"), "fileTypesAndNames": MessageLookupByLibrary.simpleMessage("Dateitypen und -namen"), - "filesBackedUpFromDevice": m22, - "filesBackedUpInAlbum": m23, + "filesBackedUpFromDevice": m23, + "filesBackedUpInAlbum": m24, "filesDeleted": MessageLookupByLibrary.simpleMessage("Dateien gelöscht"), "findPeopleByName": MessageLookupByLibrary.simpleMessage( @@ -757,8 +754,7 @@ class MessageLookup extends MessageLookupByLibrary { "foundFaces": MessageLookupByLibrary.simpleMessage("Found faces"), "freeStorageClaimed": MessageLookupByLibrary.simpleMessage( "Kostenlos hinzugefĂŒgter Speicherplatz"), - "freeStorageOnReferralSuccess": m24, - "freeStorageSpace": m25, + "freeStorageOnReferralSuccess": m25, "freeStorageUsable": MessageLookupByLibrary.simpleMessage( "Freier Speicherplatz nutzbar"), "freeTrial": @@ -928,8 +924,6 @@ class MessageLookup extends MessageLookupByLibrary { "machineLearning": MessageLookupByLibrary.simpleMessage("Maschinelles Lernen"), "magicSearch": MessageLookupByLibrary.simpleMessage("Magische Suche"), - "magicSearchDescription": MessageLookupByLibrary.simpleMessage( - "Bitte beachten Sie, dass dies mehr Bandbreite nutzt und zu einem höheren Akkuverbrauch fĂŒhrt, bis alle Elemente indiziert sind."), "manage": MessageLookupByLibrary.simpleMessage("Verwalten"), "manageDeviceStorage": MessageLookupByLibrary.simpleMessage("GerĂ€tespeicher verwalten"), diff --git a/mobile/lib/generated/intl/messages_en.dart b/mobile/lib/generated/intl/messages_en.dart index 320df2c1d0..0a05cf9f16 100644 --- a/mobile/lib/generated/intl/messages_en.dart +++ b/mobile/lib/generated/intl/messages_en.dart @@ -41,64 +41,64 @@ class MessageLookup extends MessageLookupByLibrary { static String m7(versionValue) => "Version: ${versionValue}"; - static String m8(paymentProvider) => + static String m8(freeAmount, storageUnit) => + "${freeAmount} ${storageUnit} free"; + + static String m9(paymentProvider) => "Please cancel your existing subscription from ${paymentProvider} first"; - static String m9(user) => + static String m10(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 m10(isFamilyMember, storageAmountInGb) => + static String m11(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 m11(albumName) => "Collaborative link created for ${albumName}"; + static String m12(albumName) => "Collaborative link created for ${albumName}"; - static String m12(familyAdminEmail) => + static String m13(familyAdminEmail) => "Please contact ${familyAdminEmail} to manage your subscription"; - static String m13(provider) => + static String m14(provider) => "Please contact us at support@ente.io to manage your ${provider} subscription."; static String m69(endpoint) => "Connected to ${endpoint}"; - static String m14(count) => + static String m15(count) => "${Intl.plural(count, one: 'Delete ${count} item', other: 'Delete ${count} items')}"; - static String m15(currentlyDeleting, totalCount) => + static String m16(currentlyDeleting, totalCount) => "Deleting ${currentlyDeleting} / ${totalCount}"; - static String m16(albumName) => + static String m17(albumName) => "This will remove the public link for accessing \"${albumName}\"."; - static String m17(supportEmail) => + static String m18(supportEmail) => "Please drop an email to ${supportEmail} from your registered email address"; - static String m18(count, storageSaved) => + static String m19(count, storageSaved) => "You have cleaned up ${Intl.plural(count, one: '${count} duplicate file', other: '${count} duplicate files')}, saving (${storageSaved}!)"; - static String m19(count, formattedSize) => + static String m20(count, formattedSize) => "${count} files, ${formattedSize} each"; - static String m20(newEmail) => "Email changed to ${newEmail}"; + static String m21(newEmail) => "Email changed to ${newEmail}"; - static String m21(email) => + static String m22(email) => "${email} does not have an Ente account.\n\nSend them an invite to share photos."; - static String m22(count, formattedNumber) => + static String m23(count, formattedNumber) => "${Intl.plural(count, one: '1 file', other: '${formattedNumber} files')} on this device have been backed up safely"; - static String m23(count, formattedNumber) => + static String m24(count, formattedNumber) => "${Intl.plural(count, one: '1 file', other: '${formattedNumber} files')} in this album has been backed up safely"; - static String m24(storageAmountInGB) => + static String m25(storageAmountInGB) => "${storageAmountInGB} GB each time someone signs up for a paid plan and applies your code"; - static String m25(freeAmount, storageUnit) => - "${freeAmount} ${storageUnit} free"; - static String m26(endDate) => "Free trial valid till ${endDate}"; static String m27(count) => @@ -362,10 +362,13 @@ class MessageLookup extends MessageLookupByLibrary { "You\'ll see available Cast devices here."), "autoCastiOSPermission": MessageLookupByLibrary.simpleMessage( "Make sure Local Network permissions are turned on for the Ente Photos app, in Settings."), + "autoLogoutMessage": MessageLookupByLibrary.simpleMessage( + "Due to technical glitch, you have been logged out. Our apologies for the inconvenience."), "autoPair": MessageLookupByLibrary.simpleMessage("Auto pair"), "autoPairDesc": MessageLookupByLibrary.simpleMessage( "Auto pair works only with devices that support Chromecast."), "available": MessageLookupByLibrary.simpleMessage("Available"), + "availableStorageSpace": m8, "backedUpFolders": MessageLookupByLibrary.simpleMessage("Backed up folders"), "backup": MessageLookupByLibrary.simpleMessage("Backup"), @@ -389,10 +392,10 @@ class MessageLookup extends MessageLookupByLibrary { "canOnlyRemoveFilesOwnedByYou": MessageLookupByLibrary.simpleMessage( "Can only remove files owned by you"), "cancel": MessageLookupByLibrary.simpleMessage("Cancel"), - "cancelOtherSubscription": m8, + "cancelOtherSubscription": m9, "cancelSubscription": MessageLookupByLibrary.simpleMessage("Cancel subscription"), - "cannotAddMorePhotosAfterBecomingViewer": m9, + "cannotAddMorePhotosAfterBecomingViewer": m10, "cannotDeleteSharedFiles": MessageLookupByLibrary.simpleMessage("Cannot delete shared files"), "castIPMismatchBody": MessageLookupByLibrary.simpleMessage( @@ -420,7 +423,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Claim free storage"), "claimMore": MessageLookupByLibrary.simpleMessage("Claim more!"), "claimed": MessageLookupByLibrary.simpleMessage("Claimed"), - "claimedStorageSoFar": m10, + "claimedStorageSoFar": m11, "cleanUncategorized": MessageLookupByLibrary.simpleMessage("Clean Uncategorized"), "cleanUncategorizedDescription": MessageLookupByLibrary.simpleMessage( @@ -447,7 +450,7 @@ 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": m11, + "collaborativeLinkCreatedFor": m12, "collaborator": MessageLookupByLibrary.simpleMessage("Collaborator"), "collaboratorsCanAddPhotosAndVideosToTheSharedAlbum": MessageLookupByLibrary.simpleMessage( @@ -476,10 +479,10 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Confirm your recovery key"), "connectToDevice": MessageLookupByLibrary.simpleMessage("Connect to device"), - "contactFamilyAdmin": m12, + "contactFamilyAdmin": m13, "contactSupport": MessageLookupByLibrary.simpleMessage("Contact support"), - "contactToManageSubscription": m13, + "contactToManageSubscription": m14, "contacts": MessageLookupByLibrary.simpleMessage("Contacts"), "contents": MessageLookupByLibrary.simpleMessage("Contents"), "continueLabel": MessageLookupByLibrary.simpleMessage("Continue"), @@ -557,11 +560,11 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Delete from device"), "deleteFromEnte": MessageLookupByLibrary.simpleMessage("Delete from Ente"), - "deleteItemCount": m14, + "deleteItemCount": m15, "deleteLocation": MessageLookupByLibrary.simpleMessage("Delete location"), "deletePhotos": MessageLookupByLibrary.simpleMessage("Delete photos"), - "deleteProgress": m15, + "deleteProgress": m16, "deleteReason1": MessageLookupByLibrary.simpleMessage( "It’s missing a key feature that I need"), "deleteReason2": MessageLookupByLibrary.simpleMessage( @@ -581,8 +584,6 @@ class MessageLookup extends MessageLookupByLibrary { "designedToOutlive": MessageLookupByLibrary.simpleMessage("Designed to outlive"), "details": MessageLookupByLibrary.simpleMessage("Details"), - "devAccountChanged": MessageLookupByLibrary.simpleMessage( - "The developer account we use to publish Ente on App Store has changed. Because of this, you will need to login again.\n\nOur apologies for the inconvenience, but this was unavoidable."), "developerSettings": MessageLookupByLibrary.simpleMessage("Developer settings"), "developerSettingsWarning": MessageLookupByLibrary.simpleMessage( @@ -602,7 +603,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": m16, + "disableLinkMessage": m17, "disableTwofactor": MessageLookupByLibrary.simpleMessage("Disable two-factor"), "disablingTwofactorAuthentication": @@ -623,9 +624,9 @@ class MessageLookup extends MessageLookupByLibrary { "downloadFailed": MessageLookupByLibrary.simpleMessage("Download failed"), "downloading": MessageLookupByLibrary.simpleMessage("Downloading..."), - "dropSupportEmail": m17, - "duplicateFileCountWithStorageSaved": m18, - "duplicateItemsGroup": m19, + "dropSupportEmail": m18, + "duplicateFileCountWithStorageSaved": m19, + "duplicateItemsGroup": m20, "edit": MessageLookupByLibrary.simpleMessage("Edit"), "editLocation": MessageLookupByLibrary.simpleMessage("Edit location"), "editLocationTagTitle": @@ -636,8 +637,8 @@ class MessageLookup extends MessageLookupByLibrary { "Edits to location will only be seen within Ente"), "eligible": MessageLookupByLibrary.simpleMessage("eligible"), "email": MessageLookupByLibrary.simpleMessage("Email"), - "emailChangedTo": m20, - "emailNoEnteAccount": m21, + "emailChangedTo": m21, + "emailNoEnteAccount": m22, "emailVerificationToggle": MessageLookupByLibrary.simpleMessage("Email verification"), "emailYourLogs": @@ -704,8 +705,6 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Export your data"), "faceRecognition": MessageLookupByLibrary.simpleMessage("Face recognition"), - "faceRecognitionIndexingDescription": MessageLookupByLibrary.simpleMessage( - "Please note that this will result in a higher bandwidth and battery usage until all items are indexed."), "faces": MessageLookupByLibrary.simpleMessage("Faces"), "failedToApplyCode": MessageLookupByLibrary.simpleMessage("Failed to apply code"), @@ -740,8 +739,8 @@ class MessageLookup extends MessageLookupByLibrary { "fileTypes": MessageLookupByLibrary.simpleMessage("File types"), "fileTypesAndNames": MessageLookupByLibrary.simpleMessage("File types and names"), - "filesBackedUpFromDevice": m22, - "filesBackedUpInAlbum": m23, + "filesBackedUpFromDevice": m23, + "filesBackedUpInAlbum": m24, "filesDeleted": MessageLookupByLibrary.simpleMessage("Files deleted"), "filesSavedToGallery": MessageLookupByLibrary.simpleMessage("Files saved to gallery"), @@ -755,8 +754,7 @@ class MessageLookup extends MessageLookupByLibrary { "foundFaces": MessageLookupByLibrary.simpleMessage("Found faces"), "freeStorageClaimed": MessageLookupByLibrary.simpleMessage("Free storage claimed"), - "freeStorageOnReferralSuccess": m24, - "freeStorageSpace": m25, + "freeStorageOnReferralSuccess": m25, "freeStorageUsable": MessageLookupByLibrary.simpleMessage("Free storage usable"), "freeTrial": MessageLookupByLibrary.simpleMessage("Free trial"), @@ -814,7 +812,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Incorrect recovery key"), "indexedItems": MessageLookupByLibrary.simpleMessage("Indexed items"), "indexingIsPaused": MessageLookupByLibrary.simpleMessage( - "Indexing is paused, will automatically resume when device is ready"), + "Indexing is paused. It will automatically resume when device is ready."), "insecureDevice": MessageLookupByLibrary.simpleMessage("Insecure device"), "installManually": @@ -921,8 +919,6 @@ class MessageLookup extends MessageLookupByLibrary { "machineLearning": MessageLookupByLibrary.simpleMessage("Machine learning"), "magicSearch": MessageLookupByLibrary.simpleMessage("Magic search"), - "magicSearchDescription": MessageLookupByLibrary.simpleMessage( - "Please note that this will result in a higher bandwidth and battery usage until all items are indexed."), "manage": MessageLookupByLibrary.simpleMessage("Manage"), "manageDeviceStorage": MessageLookupByLibrary.simpleMessage("Manage device storage"), @@ -939,6 +935,8 @@ class MessageLookup extends MessageLookupByLibrary { "matrix": MessageLookupByLibrary.simpleMessage("Matrix"), "memoryCount": m33, "merchandise": MessageLookupByLibrary.simpleMessage("Merchandise"), + "mlIndexingDescription": MessageLookupByLibrary.simpleMessage( + "Please note that machine learning will result in a higher bandwidth and battery usage until all items are indexed."), "mobileWebDesktop": MessageLookupByLibrary.simpleMessage("Mobile, Web, Desktop"), "moderateStrength": MessageLookupByLibrary.simpleMessage("Moderate"), @@ -1226,7 +1224,7 @@ class MessageLookup extends MessageLookupByLibrary { "searchDatesEmptySection": MessageLookupByLibrary.simpleMessage( "Search by a date, month or year"), "searchFaceEmptySection": MessageLookupByLibrary.simpleMessage( - "Persons will be shown here once indexing is done"), + "People will be shown here once indexing is done"), "searchFileTypesAndNamesEmptySection": MessageLookupByLibrary.simpleMessage("File types and names"), "searchHint1": diff --git a/mobile/lib/generated/intl/messages_es.dart b/mobile/lib/generated/intl/messages_es.dart index f0b4c87f5c..1fa3763da2 100644 --- a/mobile/lib/generated/intl/messages_es.dart +++ b/mobile/lib/generated/intl/messages_es.dart @@ -38,13 +38,13 @@ class MessageLookup extends MessageLookupByLibrary { static String m7(versionValue) => "VersiĂłn: ${versionValue}"; - static String m8(paymentProvider) => + static String m9(paymentProvider) => "Por favor, cancele primero su suscripciĂłn existente de ${paymentProvider}"; - static String m9(user) => + static String m10(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 m10(isFamilyMember, storageAmountInGb) => + static String m11(isFamilyMember, storageAmountInGb) => "${Intl.select(isFamilyMember, { 'true': 'Su familia ha reclamado ${storageAmountInGb} GB hasta el momento', @@ -54,44 +54,41 @@ class MessageLookup extends MessageLookupByLibrary { 'ÂĄTĂș has reclamado ${storageAmountInGb} GB hasta el momento!', })}"; - static String m11(albumName) => + static String m12(albumName) => "Enlace colaborativo creado para ${albumName}"; - static String m12(familyAdminEmail) => + static String m13(familyAdminEmail) => "Por favor contacta con ${familyAdminEmail} para administrar tu suscripciĂłn"; - static String m13(provider) => + static String m14(provider) => "Por favor, contĂĄctenos en support@ente.io para gestionar su suscripciĂłn a ${provider}."; - static String m15(currentlyDeleting, totalCount) => + static String m16(currentlyDeleting, totalCount) => "Borrando ${currentlyDeleting} / ${totalCount}"; - static String m16(albumName) => + static String m17(albumName) => "Esto eliminarĂĄ el enlace pĂșblico para acceder a \"${albumName}\"."; - static String m17(supportEmail) => + static String m18(supportEmail) => "Por favor, envĂ­e un email a ${supportEmail} desde su direcciĂłn de correo electrĂłnico registrada"; - static String m18(count, storageSaved) => + static String m19(count, storageSaved) => "ÂĄHas limpiado ${Intl.plural(count, one: '${count} archivo duplicado', other: '${count} archivos duplicados')}, ahorrando (${storageSaved}!)"; - static String m20(newEmail) => "Correo cambiado a ${newEmail}"; + static String m21(newEmail) => "Correo cambiado a ${newEmail}"; - static String m21(email) => + static String m22(email) => "${email} no tiene una cuenta ente.\n\nEnvĂ­ale una invitaciĂłn para compartir fotos."; - static String m22(count, formattedNumber) => + static String m23(count, formattedNumber) => "${Intl.plural(count, one: '1 archivo', other: '${formattedNumber} archivos')} en este dispositivo han sido respaldados de forma segura"; - static String m23(count, formattedNumber) => + static String m24(count, formattedNumber) => "${Intl.plural(count, one: '1 archivo', other: '${formattedNumber} archivos')} en este ĂĄlbum ha sido respaldado de forma segura"; - static String m24(storageAmountInGB) => + static String m25(storageAmountInGB) => "${storageAmountInGB} GB cada vez que alguien se registra en un plan de pago y aplica tu cĂłdigo"; - static String m25(freeAmount, storageUnit) => - "${freeAmount} ${storageUnit} gratis"; - static String m26(endDate) => "Prueba gratuita vĂĄlida hasta${endDate}"; static String m27(count) => @@ -335,10 +332,10 @@ class MessageLookup extends MessageLookupByLibrary { "canOnlyRemoveFilesOwnedByYou": MessageLookupByLibrary.simpleMessage( "SĂłlo puede eliminar archivos de tu propiedad"), "cancel": MessageLookupByLibrary.simpleMessage("Cancelar"), - "cancelOtherSubscription": m8, + "cancelOtherSubscription": m9, "cancelSubscription": MessageLookupByLibrary.simpleMessage("Cancelar suscripciĂłn"), - "cannotAddMorePhotosAfterBecomingViewer": m9, + "cannotAddMorePhotosAfterBecomingViewer": m10, "centerPoint": MessageLookupByLibrary.simpleMessage("Punto central"), "changeEmail": MessageLookupByLibrary.simpleMessage("Cambiar correo electrĂłnico"), @@ -359,7 +356,7 @@ class MessageLookup extends MessageLookupByLibrary { "Reclamar almacenamiento gratis"), "claimMore": MessageLookupByLibrary.simpleMessage("ÂĄReclama mĂĄs!"), "claimed": MessageLookupByLibrary.simpleMessage("Reclamado"), - "claimedStorageSoFar": m10, + "claimedStorageSoFar": m11, "clearCaches": MessageLookupByLibrary.simpleMessage("Limpiar cachĂ©"), "click": MessageLookupByLibrary.simpleMessage("‱ Click"), "clickOnTheOverflowMenu": MessageLookupByLibrary.simpleMessage( @@ -379,7 +376,7 @@ class MessageLookup extends MessageLookupByLibrary { "Crea un enlace para que la gente pueda añadir y ver fotos en tu ĂĄlbum compartido sin necesidad de la aplicaciĂłn ente o una cuenta. Genial para recolectar fotos de eventos."), "collaborativeLink": MessageLookupByLibrary.simpleMessage("Enlace colaborativo"), - "collaborativeLinkCreatedFor": m11, + "collaborativeLinkCreatedFor": m12, "collaborator": MessageLookupByLibrary.simpleMessage("Colaborador"), "collaboratorsCanAddPhotosAndVideosToTheSharedAlbum": MessageLookupByLibrary.simpleMessage( @@ -404,10 +401,10 @@ class MessageLookup extends MessageLookupByLibrary { "Confirmar clave de recuperaciĂłn"), "confirmYourRecoveryKey": MessageLookupByLibrary.simpleMessage( "Confirme su clave de recuperaciĂłn"), - "contactFamilyAdmin": m12, + "contactFamilyAdmin": m13, "contactSupport": MessageLookupByLibrary.simpleMessage("Contactar con soporte"), - "contactToManageSubscription": m13, + "contactToManageSubscription": m14, "contacts": MessageLookupByLibrary.simpleMessage("Contacts"), "continueLabel": MessageLookupByLibrary.simpleMessage("Continuar"), "continueOnFreeTrial": MessageLookupByLibrary.simpleMessage( @@ -482,7 +479,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Borrar la ubicaciĂłn"), "deletePhotos": MessageLookupByLibrary.simpleMessage("Borrar las fotos"), - "deleteProgress": m15, + "deleteProgress": m16, "deleteReason1": MessageLookupByLibrary.simpleMessage( "Falta una caracterĂ­stica clave que necesito"), "deleteReason2": MessageLookupByLibrary.simpleMessage( @@ -503,8 +500,6 @@ class MessageLookup extends MessageLookupByLibrary { "designedToOutlive": MessageLookupByLibrary.simpleMessage("Diseñado para sobrevivir"), "details": MessageLookupByLibrary.simpleMessage("Detalles"), - "devAccountChanged": MessageLookupByLibrary.simpleMessage( - "La cuenta de desarrollador que utilizamos para publicar ente en la App Store ha cambiado. Por eso, tendrĂĄs que iniciar sesiĂłn de nuevo.\n\nNuestras disculpas por las molestias, pero esto era inevitable."), "deviceFilesAutoUploading": MessageLookupByLibrary.simpleMessage( "Los archivos añadidos a este ĂĄlbum de dispositivo se subirĂĄn automĂĄticamente a ente."), "deviceLockExplanation": MessageLookupByLibrary.simpleMessage( @@ -516,7 +511,7 @@ class MessageLookup extends MessageLookupByLibrary { "Los espectadores todavĂ­a pueden tomar capturas de pantalla o guardar una copia de sus fotos usando herramientas externas"), "disableDownloadWarningTitle": MessageLookupByLibrary.simpleMessage("Por favor tenga en cuenta"), - "disableLinkMessage": m16, + "disableLinkMessage": m17, "disableTwofactor": MessageLookupByLibrary.simpleMessage("Deshabilitar dos factores"), "disablingTwofactorAuthentication": @@ -537,8 +532,8 @@ class MessageLookup extends MessageLookupByLibrary { "downloadFailed": MessageLookupByLibrary.simpleMessage("Descarga fallida"), "downloading": MessageLookupByLibrary.simpleMessage("Descargando..."), - "dropSupportEmail": m17, - "duplicateFileCountWithStorageSaved": m18, + "dropSupportEmail": m18, + "duplicateFileCountWithStorageSaved": m19, "edit": MessageLookupByLibrary.simpleMessage("Editar"), "editLocation": MessageLookupByLibrary.simpleMessage("Edit location"), "editLocationTagTitle": @@ -550,8 +545,8 @@ class MessageLookup extends MessageLookupByLibrary { "Edits to location will only be seen within Ente"), "eligible": MessageLookupByLibrary.simpleMessage("elegible"), "email": MessageLookupByLibrary.simpleMessage("Correo electrĂłnico"), - "emailChangedTo": m20, - "emailNoEnteAccount": m21, + "emailChangedTo": m21, + "emailNoEnteAccount": m22, "emailYourLogs": MessageLookupByLibrary.simpleMessage( "EnvĂ­e sus registros por correo electrĂłnico"), "empty": MessageLookupByLibrary.simpleMessage("Vaciar"), @@ -615,8 +610,6 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Exportar tus datos"), "faceRecognition": MessageLookupByLibrary.simpleMessage("Face recognition"), - "faceRecognitionIndexingDescription": MessageLookupByLibrary.simpleMessage( - "Please note that this will result in a higher bandwidth and battery usage until all items are indexed."), "failedToApplyCode": MessageLookupByLibrary.simpleMessage("Error al aplicar el cĂłdigo"), "failedToCancel": @@ -646,8 +639,8 @@ class MessageLookup extends MessageLookupByLibrary { "fileSavedToGallery": MessageLookupByLibrary.simpleMessage( "Archivo guardado en la galerĂ­a"), "fileTypes": MessageLookupByLibrary.simpleMessage("File types"), - "filesBackedUpFromDevice": m22, - "filesBackedUpInAlbum": m23, + "filesBackedUpFromDevice": m23, + "filesBackedUpInAlbum": m24, "filesDeleted": MessageLookupByLibrary.simpleMessage("Archivos eliminados"), "flip": MessageLookupByLibrary.simpleMessage("Voltear"), @@ -658,8 +651,7 @@ class MessageLookup extends MessageLookupByLibrary { "foundFaces": MessageLookupByLibrary.simpleMessage("Found faces"), "freeStorageClaimed": MessageLookupByLibrary.simpleMessage( "Almacenamiento gratuito reclamado"), - "freeStorageOnReferralSuccess": m24, - "freeStorageSpace": m25, + "freeStorageOnReferralSuccess": m25, "freeStorageUsable": MessageLookupByLibrary.simpleMessage( "Almacenamiento libre disponible"), "freeTrial": MessageLookupByLibrary.simpleMessage("Prueba gratuita"), diff --git a/mobile/lib/generated/intl/messages_fr.dart b/mobile/lib/generated/intl/messages_fr.dart index 5c4a2b4e47..e45c164c01 100644 --- a/mobile/lib/generated/intl/messages_fr.dart +++ b/mobile/lib/generated/intl/messages_fr.dart @@ -38,13 +38,16 @@ class MessageLookup extends MessageLookupByLibrary { static String m7(versionValue) => "Version : ${versionValue}"; - static String m8(paymentProvider) => + static String m8(freeAmount, storageUnit) => + "${freeAmount} ${storageUnit} libre"; + + static String m9(paymentProvider) => "Veuillez d\'abord annuler votre abonnement existant de ${paymentProvider}"; - static String m9(user) => + static String m10(user) => "${user} ne pourra pas ajouter plus de photos Ă  cet album\n\nIl pourrait toujours supprimer les photos existantes ajoutĂ©es par eux"; - static String m10(isFamilyMember, storageAmountInGb) => + static String m11(isFamilyMember, storageAmountInGb) => "${Intl.select(isFamilyMember, { 'true': 'Votre famille a demandĂ© ${storageAmountInGb} GB jusqu\'Ă  prĂ©sent', @@ -54,49 +57,46 @@ class MessageLookup extends MessageLookupByLibrary { 'Vous avez rĂ©clamĂ© ${storageAmountInGb} GB jusqu\'Ă  prĂ©sent!', })}"; - static String m11(albumName) => "Lien collaboratif créé pour ${albumName}"; + static String m12(albumName) => "Lien collaboratif créé pour ${albumName}"; - static String m12(familyAdminEmail) => + static String m13(familyAdminEmail) => "Veuillez contacter ${familyAdminEmail} pour gĂ©rer votre abonnement"; - static String m13(provider) => + static String m14(provider) => "Veuillez nous contacter Ă  support@ente.io pour gĂ©rer votre abonnement ${provider}."; - static String m14(count) => + static String m15(count) => "${Intl.plural(count, one: 'Supprimer le fichier', other: 'Supprimer ${count} fichiers')}"; - static String m15(currentlyDeleting, totalCount) => + static String m16(currentlyDeleting, totalCount) => "Suppression de ${currentlyDeleting} / ${totalCount}"; - static String m16(albumName) => + static String m17(albumName) => "Cela supprimera le lien public pour accĂ©der Ă  \"${albumName}\"."; - static String m17(supportEmail) => + static String m18(supportEmail) => "Veuillez envoyer un e-mail Ă  ${supportEmail} depuis votre adresse enregistrĂ©e"; - static String m18(count, storageSaved) => + static String m19(count, storageSaved) => "Vous avez nettoyĂ© ${Intl.plural(count, one: '${count} fichier dupliquĂ©', other: '${count} fichiers dupliquĂ©s')}, sauvegarde (${storageSaved}!)"; - static String m19(count, formattedSize) => + static String m20(count, formattedSize) => "${count} fichiers, ${formattedSize} chacun"; - static String m20(newEmail) => "L\'e-mail a Ă©tĂ© changĂ© en ${newEmail}"; + static String m21(newEmail) => "L\'e-mail a Ă©tĂ© changĂ© en ${newEmail}"; - static String m21(email) => + static String m22(email) => "${email} n\'a pas de compte ente.\n\nEnvoyez une invitation pour partager des photos."; - static String m22(count, formattedNumber) => + static String m23(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 m23(count, formattedNumber) => + static String m24(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 m24(storageAmountInGB) => + static String m25(storageAmountInGB) => "${storageAmountInGB} Go chaque fois que quelqu\'un s\'inscrit Ă  une offre payante et applique votre code"; - static String m25(freeAmount, storageUnit) => - "${freeAmount} ${storageUnit} libre"; - static String m26(endDate) => "Essai gratuit valide jusqu’au ${endDate}"; static String m27(count) => @@ -362,6 +362,7 @@ class MessageLookup extends MessageLookupByLibrary { "authenticationSuccessful": MessageLookupByLibrary.simpleMessage("Authentification rĂ©ussie!"), "available": MessageLookupByLibrary.simpleMessage("Disponible"), + "availableStorageSpace": m8, "backedUpFolders": MessageLookupByLibrary.simpleMessage("Dossiers sauvegardĂ©s"), "backup": MessageLookupByLibrary.simpleMessage("Sauvegarde"), @@ -388,10 +389,10 @@ class MessageLookup extends MessageLookupByLibrary { "canOnlyRemoveFilesOwnedByYou": MessageLookupByLibrary.simpleMessage( "Vous ne pouvez supprimer que les fichiers que vous possĂ©dez"), "cancel": MessageLookupByLibrary.simpleMessage("Annuler"), - "cancelOtherSubscription": m8, + "cancelOtherSubscription": m9, "cancelSubscription": MessageLookupByLibrary.simpleMessage("Annuler l\'abonnement"), - "cannotAddMorePhotosAfterBecomingViewer": m9, + "cannotAddMorePhotosAfterBecomingViewer": m10, "cannotDeleteSharedFiles": MessageLookupByLibrary.simpleMessage( "Les fichiers partagĂ©s ne peuvent pas ĂȘtre supprimĂ©s"), "centerPoint": MessageLookupByLibrary.simpleMessage("Point central"), @@ -414,7 +415,7 @@ class MessageLookup extends MessageLookupByLibrary { "RĂ©clamer le stockage gratuit"), "claimMore": MessageLookupByLibrary.simpleMessage("RĂ©clamez plus !"), "claimed": MessageLookupByLibrary.simpleMessage("RĂ©clamĂ©e"), - "claimedStorageSoFar": m10, + "claimedStorageSoFar": m11, "clearCaches": MessageLookupByLibrary.simpleMessage("Nettoyer le cache"), "click": MessageLookupByLibrary.simpleMessage("‱ Click"), @@ -437,7 +438,7 @@ class MessageLookup extends MessageLookupByLibrary { "CrĂ©ez un lien pour permettre aux gens 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 collecter des photos d\'Ă©vĂ©nement."), "collaborativeLink": MessageLookupByLibrary.simpleMessage("Lien collaboratif"), - "collaborativeLinkCreatedFor": m11, + "collaborativeLinkCreatedFor": m12, "collaborator": MessageLookupByLibrary.simpleMessage("Collaborateur"), "collaboratorsCanAddPhotosAndVideosToTheSharedAlbum": MessageLookupByLibrary.simpleMessage( @@ -465,10 +466,10 @@ class MessageLookup extends MessageLookupByLibrary { "Confirmer la clĂ© de rĂ©cupĂ©ration"), "confirmYourRecoveryKey": MessageLookupByLibrary.simpleMessage( "Confirmer la clĂ© de rĂ©cupĂ©ration"), - "contactFamilyAdmin": m12, + "contactFamilyAdmin": m13, "contactSupport": MessageLookupByLibrary.simpleMessage("Contacter l\'assistance"), - "contactToManageSubscription": m13, + "contactToManageSubscription": m14, "contacts": MessageLookupByLibrary.simpleMessage("Contacts"), "contents": MessageLookupByLibrary.simpleMessage("Contenus"), "continueLabel": MessageLookupByLibrary.simpleMessage("Continuer"), @@ -551,12 +552,12 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Supprimer de l\'appareil"), "deleteFromEnte": MessageLookupByLibrary.simpleMessage("Supprimer de ente"), - "deleteItemCount": m14, + "deleteItemCount": m15, "deleteLocation": MessageLookupByLibrary.simpleMessage("Supprimer la localisation"), "deletePhotos": MessageLookupByLibrary.simpleMessage("Supprimer des photos"), - "deleteProgress": m15, + "deleteProgress": m16, "deleteReason1": MessageLookupByLibrary.simpleMessage( "Il manque une fonction clĂ© dont j\'ai besoin"), "deleteReason2": MessageLookupByLibrary.simpleMessage( @@ -577,8 +578,6 @@ class MessageLookup extends MessageLookupByLibrary { "designedToOutlive": MessageLookupByLibrary.simpleMessage("Conçu pour survivre"), "details": MessageLookupByLibrary.simpleMessage("DĂ©tails"), - "devAccountChanged": MessageLookupByLibrary.simpleMessage( - "Le compte dĂ©veloppeur que nous utilisons pour publier ente sur l\'App Store a changĂ©. Pour cette raison, vous devrez vous connecter Ă  nouveau.\n\nNous nous excusons pour la gĂȘne occasionnĂ©e, mais cela Ă©tait inĂ©vitable."), "deviceFilesAutoUploading": MessageLookupByLibrary.simpleMessage( "Les fichiers ajoutĂ©s Ă  cet album seront automatiquement tĂ©lĂ©chargĂ©s sur ente."), "deviceLockExplanation": MessageLookupByLibrary.simpleMessage( @@ -590,7 +589,7 @@ class MessageLookup extends MessageLookupByLibrary { "Les tĂ©lĂ©spectateurs 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": m16, + "disableLinkMessage": m17, "disableTwofactor": MessageLookupByLibrary.simpleMessage( "DĂ©sactiver la double-authentification"), "disablingTwofactorAuthentication": @@ -611,9 +610,9 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Échec du tĂ©lĂ©chargement"), "downloading": MessageLookupByLibrary.simpleMessage("TĂ©lĂ©chargement en cours..."), - "dropSupportEmail": m17, - "duplicateFileCountWithStorageSaved": m18, - "duplicateItemsGroup": m19, + "dropSupportEmail": m18, + "duplicateFileCountWithStorageSaved": m19, + "duplicateItemsGroup": m20, "edit": MessageLookupByLibrary.simpleMessage("Éditer"), "editLocation": MessageLookupByLibrary.simpleMessage("Edit location"), "editLocationTagTitle": @@ -625,8 +624,8 @@ class MessageLookup extends MessageLookupByLibrary { "Edits to location will only be seen within Ente"), "eligible": MessageLookupByLibrary.simpleMessage("Ă©ligible"), "email": MessageLookupByLibrary.simpleMessage("E-mail"), - "emailChangedTo": m20, - "emailNoEnteAccount": m21, + "emailChangedTo": m21, + "emailNoEnteAccount": m22, "emailVerificationToggle": MessageLookupByLibrary.simpleMessage( "VĂ©rification de l\'adresse e-mail"), "emailYourLogs": @@ -694,8 +693,6 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Exportez vos donnĂ©es"), "faceRecognition": MessageLookupByLibrary.simpleMessage("Face recognition"), - "faceRecognitionIndexingDescription": MessageLookupByLibrary.simpleMessage( - "Please note that this will result in a higher bandwidth and battery usage until all items are indexed."), "faces": MessageLookupByLibrary.simpleMessage("Visages"), "failedToApplyCode": MessageLookupByLibrary.simpleMessage( "Impossible d\'appliquer le code"), @@ -731,8 +728,8 @@ class MessageLookup extends MessageLookupByLibrary { "fileTypes": MessageLookupByLibrary.simpleMessage("Types de fichiers"), "fileTypesAndNames": MessageLookupByLibrary.simpleMessage("Types et noms de fichiers"), - "filesBackedUpFromDevice": m22, - "filesBackedUpInAlbum": m23, + "filesBackedUpFromDevice": m23, + "filesBackedUpInAlbum": m24, "filesDeleted": MessageLookupByLibrary.simpleMessage("Fichiers supprimĂ©s"), "flip": MessageLookupByLibrary.simpleMessage("Retourner"), @@ -743,8 +740,7 @@ class MessageLookup extends MessageLookupByLibrary { "foundFaces": MessageLookupByLibrary.simpleMessage("Found faces"), "freeStorageClaimed": MessageLookupByLibrary.simpleMessage("Stockage gratuit rĂ©clamĂ©"), - "freeStorageOnReferralSuccess": m24, - "freeStorageSpace": m25, + "freeStorageOnReferralSuccess": m25, "freeStorageUsable": MessageLookupByLibrary.simpleMessage("Stockage gratuit utilisable"), "freeTrial": MessageLookupByLibrary.simpleMessage("Essai gratuit"), diff --git a/mobile/lib/generated/intl/messages_it.dart b/mobile/lib/generated/intl/messages_it.dart index d7a902db84..db3baf3b56 100644 --- a/mobile/lib/generated/intl/messages_it.dart +++ b/mobile/lib/generated/intl/messages_it.dart @@ -38,13 +38,16 @@ class MessageLookup extends MessageLookupByLibrary { static String m7(versionValue) => "Versione: ${versionValue}"; - static String m8(paymentProvider) => + static String m8(freeAmount, storageUnit) => + "${freeAmount} ${storageUnit} liberi"; + + static String m9(paymentProvider) => "Annulla prima il tuo abbonamento esistente da ${paymentProvider}"; - static String m9(user) => + static String m10(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 m10(isFamilyMember, storageAmountInGb) => + static String m11(isFamilyMember, storageAmountInGb) => "${Intl.select(isFamilyMember, { 'true': 'Il tuo piano famiglia ha giĂ  richiesto ${storageAmountInGb} GB finora', @@ -52,48 +55,45 @@ class MessageLookup extends MessageLookupByLibrary { 'other': 'Hai giĂ  richiesto ${storageAmountInGb} GB finora!', })}"; - static String m11(albumName) => "Link collaborativo creato per ${albumName}"; + static String m12(albumName) => "Link collaborativo creato per ${albumName}"; - static String m12(familyAdminEmail) => + static String m13(familyAdminEmail) => "Contatta ${familyAdminEmail} per gestire il tuo abbonamento"; - static String m13(provider) => + static String m14(provider) => "Scrivi all\'indirizzo support@ente.io per gestire il tuo abbonamento ${provider}."; - static String m14(count) => + static String m15(count) => "${Intl.plural(count, one: 'Elimina ${count} elemento', other: 'Elimina ${count} elementi')}"; - static String m15(currentlyDeleting, totalCount) => + static String m16(currentlyDeleting, totalCount) => "Eliminazione di ${currentlyDeleting} / ${totalCount}"; - static String m16(albumName) => + static String m17(albumName) => "Questo rimuoverĂ  il link pubblico per accedere a \"${albumName}\"."; - static String m17(supportEmail) => + static String m18(supportEmail) => "Per favore invia un\'email a ${supportEmail} dall\'indirizzo email con cui ti sei registrato"; - static String m18(count, storageSaved) => + static String m19(count, storageSaved) => "Hai ripulito ${Intl.plural(count, one: '${count} doppione', other: '${count} doppioni')}, salvando (${storageSaved}!)"; - static String m19(count, formattedSize) => + static String m20(count, formattedSize) => "${count} file, ${formattedSize} l\'uno"; - static String m20(newEmail) => "Email cambiata in ${newEmail}"; + static String m21(newEmail) => "Email cambiata in ${newEmail}"; - static String m21(email) => + static String m22(email) => "${email} non ha un account su ente.\n\nInvia un invito per condividere foto."; - static String m22(count, formattedNumber) => - "${Intl.plural(count, one: '1 file', other: '${formattedNumber} file')} di quest\'album sono stati salvati in modo sicuro"; - static String m23(count, formattedNumber) => "${Intl.plural(count, one: '1 file', other: '${formattedNumber} file')} di quest\'album sono stati salvati in modo sicuro"; - static String m24(storageAmountInGB) => - "${storageAmountInGB} GB ogni volta che qualcuno si iscrive a un piano a pagamento e applica il tuo codice"; + static String m24(count, formattedNumber) => + "${Intl.plural(count, one: '1 file', other: '${formattedNumber} file')} di quest\'album sono stati salvati in modo sicuro"; - static String m25(freeAmount, storageUnit) => - "${freeAmount} ${storageUnit} liberi"; + static String m25(storageAmountInGB) => + "${storageAmountInGB} GB ogni volta che qualcuno si iscrive a un piano a pagamento e applica il tuo codice"; static String m26(endDate) => "La prova gratuita termina il ${endDate}"; @@ -353,6 +353,7 @@ class MessageLookup extends MessageLookupByLibrary { "authenticationSuccessful": MessageLookupByLibrary.simpleMessage("Autenticazione riuscita!"), "available": MessageLookupByLibrary.simpleMessage("Disponibile"), + "availableStorageSpace": m8, "backedUpFolders": MessageLookupByLibrary.simpleMessage("Cartelle salvate"), "backup": MessageLookupByLibrary.simpleMessage("Backup"), @@ -375,10 +376,10 @@ class MessageLookup extends MessageLookupByLibrary { "canOnlyRemoveFilesOwnedByYou": MessageLookupByLibrary.simpleMessage( "Puoi rimuovere solo i file di tua proprietĂ "), "cancel": MessageLookupByLibrary.simpleMessage("Annulla"), - "cancelOtherSubscription": m8, + "cancelOtherSubscription": m9, "cancelSubscription": MessageLookupByLibrary.simpleMessage("Annulla abbonamento"), - "cannotAddMorePhotosAfterBecomingViewer": m9, + "cannotAddMorePhotosAfterBecomingViewer": m10, "cannotDeleteSharedFiles": MessageLookupByLibrary.simpleMessage( "Impossibile eliminare i file condivisi"), "centerPoint": MessageLookupByLibrary.simpleMessage("Punto centrale"), @@ -401,7 +402,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Richiedi spazio gratuito"), "claimMore": MessageLookupByLibrary.simpleMessage("Richiedine di piĂč!"), "claimed": MessageLookupByLibrary.simpleMessage("Riscattato"), - "claimedStorageSoFar": m10, + "claimedStorageSoFar": m11, "clearCaches": MessageLookupByLibrary.simpleMessage("Svuota cache"), "click": MessageLookupByLibrary.simpleMessage("‱ Clic"), "clickOnTheOverflowMenu": @@ -423,7 +424,7 @@ 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": m11, + "collaborativeLinkCreatedFor": m12, "collaborator": MessageLookupByLibrary.simpleMessage("Collaboratore"), "collaboratorsCanAddPhotosAndVideosToTheSharedAlbum": MessageLookupByLibrary.simpleMessage( @@ -451,10 +452,10 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Conferma chiave di recupero"), "confirmYourRecoveryKey": MessageLookupByLibrary.simpleMessage( "Conferma la tua chiave di recupero"), - "contactFamilyAdmin": m12, + "contactFamilyAdmin": m13, "contactSupport": MessageLookupByLibrary.simpleMessage("Contatta il supporto"), - "contactToManageSubscription": m13, + "contactToManageSubscription": m14, "contacts": MessageLookupByLibrary.simpleMessage("Contacts"), "continueLabel": MessageLookupByLibrary.simpleMessage("Continua"), "continueOnFreeTrial": @@ -532,11 +533,11 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Elimina dal dispositivo"), "deleteFromEnte": MessageLookupByLibrary.simpleMessage("Elimina da ente"), - "deleteItemCount": m14, + "deleteItemCount": m15, "deleteLocation": MessageLookupByLibrary.simpleMessage("Elimina posizione"), "deletePhotos": MessageLookupByLibrary.simpleMessage("Elimina foto"), - "deleteProgress": m15, + "deleteProgress": m16, "deleteReason1": MessageLookupByLibrary.simpleMessage( "Manca una caratteristica chiave di cui ho bisogno"), "deleteReason2": MessageLookupByLibrary.simpleMessage( @@ -557,8 +558,6 @@ class MessageLookup extends MessageLookupByLibrary { "designedToOutlive": MessageLookupByLibrary.simpleMessage("Progettato per sopravvivere"), "details": MessageLookupByLibrary.simpleMessage("Dettagli"), - "devAccountChanged": MessageLookupByLibrary.simpleMessage( - "L\'account sviluppatore che utilizziamo per pubblicare ente su App Store Ăš cambiato. Per questo motivo dovrai effettuare nuovamente il login.\n\nCi dispiace per il disagio, ma era inevitabile."), "deviceFilesAutoUploading": MessageLookupByLibrary.simpleMessage( "I file aggiunti in questa cartella del dispositivo verranno automaticamente caricati su ente."), "deviceLockExplanation": MessageLookupByLibrary.simpleMessage( @@ -570,7 +569,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": m16, + "disableLinkMessage": m17, "disableTwofactor": MessageLookupByLibrary.simpleMessage( "Disabilita autenticazione a due fattori"), "disablingTwofactorAuthentication": @@ -591,9 +590,9 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Scaricamento fallito"), "downloading": MessageLookupByLibrary.simpleMessage("Scaricamento in corso..."), - "dropSupportEmail": m17, - "duplicateFileCountWithStorageSaved": m18, - "duplicateItemsGroup": m19, + "dropSupportEmail": m18, + "duplicateFileCountWithStorageSaved": m19, + "duplicateItemsGroup": m20, "edit": MessageLookupByLibrary.simpleMessage("Modifica"), "editLocation": MessageLookupByLibrary.simpleMessage("Edit location"), "editLocationTagTitle": @@ -604,8 +603,8 @@ class MessageLookup extends MessageLookupByLibrary { "Edits to location will only be seen within Ente"), "eligible": MessageLookupByLibrary.simpleMessage("idoneo"), "email": MessageLookupByLibrary.simpleMessage("Email"), - "emailChangedTo": m20, - "emailNoEnteAccount": m21, + "emailChangedTo": m21, + "emailNoEnteAccount": m22, "emailVerificationToggle": MessageLookupByLibrary.simpleMessage("Verifica Email"), "emailYourLogs": MessageLookupByLibrary.simpleMessage( @@ -671,8 +670,6 @@ class MessageLookup extends MessageLookupByLibrary { "exportYourData": MessageLookupByLibrary.simpleMessage("Esporta dati"), "faceRecognition": MessageLookupByLibrary.simpleMessage("Face recognition"), - "faceRecognitionIndexingDescription": MessageLookupByLibrary.simpleMessage( - "Please note that this will result in a higher bandwidth and battery usage until all items are indexed."), "failedToApplyCode": MessageLookupByLibrary.simpleMessage( "Impossibile applicare il codice"), "failedToCancel": @@ -704,8 +701,8 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Aggiungi descrizione..."), "fileSavedToGallery": MessageLookupByLibrary.simpleMessage("File salvato nella galleria"), - "filesBackedUpFromDevice": m22, - "filesBackedUpInAlbum": m23, + "filesBackedUpFromDevice": m23, + "filesBackedUpInAlbum": m24, "filesDeleted": MessageLookupByLibrary.simpleMessage("File eliminati"), "flip": MessageLookupByLibrary.simpleMessage("Capovolgi"), "forYourMemories": @@ -715,8 +712,7 @@ class MessageLookup extends MessageLookupByLibrary { "foundFaces": MessageLookupByLibrary.simpleMessage("Found faces"), "freeStorageClaimed": MessageLookupByLibrary.simpleMessage("Spazio gratuito richiesto"), - "freeStorageOnReferralSuccess": m24, - "freeStorageSpace": m25, + "freeStorageOnReferralSuccess": m25, "freeStorageUsable": MessageLookupByLibrary.simpleMessage("Spazio libero utilizzabile"), "freeTrial": MessageLookupByLibrary.simpleMessage("Prova gratuita"), diff --git a/mobile/lib/generated/intl/messages_ko.dart b/mobile/lib/generated/intl/messages_ko.dart index 614d860dc3..a174b77072 100644 --- a/mobile/lib/generated/intl/messages_ko.dart +++ b/mobile/lib/generated/intl/messages_ko.dart @@ -50,8 +50,6 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Enter person name"), "faceRecognition": MessageLookupByLibrary.simpleMessage("Face recognition"), - "faceRecognitionIndexingDescription": MessageLookupByLibrary.simpleMessage( - "Please note that this will result in a higher bandwidth and battery usage until all items are indexed."), "fileTypes": MessageLookupByLibrary.simpleMessage("File types"), "foundFaces": MessageLookupByLibrary.simpleMessage("Found faces"), "indexingIsPaused": MessageLookupByLibrary.simpleMessage( diff --git a/mobile/lib/generated/intl/messages_nl.dart b/mobile/lib/generated/intl/messages_nl.dart index 1981b338ce..71ccc51b85 100644 --- a/mobile/lib/generated/intl/messages_nl.dart +++ b/mobile/lib/generated/intl/messages_nl.dart @@ -41,13 +41,16 @@ class MessageLookup extends MessageLookupByLibrary { static String m7(versionValue) => "Versie: ${versionValue}"; - static String m8(paymentProvider) => + static String m8(freeAmount, storageUnit) => + "${freeAmount} ${storageUnit} vrij"; + + static String m9(paymentProvider) => "Annuleer eerst uw bestaande abonnement bij ${paymentProvider}"; - static String m9(user) => + static String m10(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 m10(isFamilyMember, storageAmountInGb) => + static String m11(isFamilyMember, storageAmountInGb) => "${Intl.select(isFamilyMember, { 'true': 'Jouw familie heeft ${storageAmountInGb} GB geclaimd tot nu toe', @@ -55,52 +58,49 @@ class MessageLookup extends MessageLookupByLibrary { 'other': 'Je hebt ${storageAmountInGb} GB geclaimd tot nu toe!', })}"; - static String m11(albumName) => + static String m12(albumName) => "Gezamenlijke link aangemaakt voor ${albumName}"; - static String m12(familyAdminEmail) => + static String m13(familyAdminEmail) => "Neem contact op met ${familyAdminEmail} om uw abonnement te beheren"; - static String m13(provider) => + static String m14(provider) => "Neem contact met ons op via support@ente.io om uw ${provider} abonnement te beheren."; static String m69(endpoint) => "Verbonden met ${endpoint}"; - static String m14(count) => + static String m15(count) => "${Intl.plural(count, one: 'Verwijder ${count} bestand', other: 'Verwijder ${count} bestanden')}"; - static String m15(currentlyDeleting, totalCount) => + static String m16(currentlyDeleting, totalCount) => "Verwijderen van ${currentlyDeleting} / ${totalCount}"; - static String m16(albumName) => + static String m17(albumName) => "Dit verwijdert de openbare link voor toegang tot \"${albumName}\"."; - static String m17(supportEmail) => + static String m18(supportEmail) => "Stuur een e-mail naar ${supportEmail} vanaf het door jou geregistreerde e-mailadres"; - static String m18(count, storageSaved) => + static String m19(count, storageSaved) => "Je hebt ${Intl.plural(count, one: '${count} dubbel bestand', other: '${count} dubbele bestanden')} opgeruimd, totaal (${storageSaved}!)"; - static String m19(count, formattedSize) => + static String m20(count, formattedSize) => "${count} bestanden, elk ${formattedSize}"; - static String m20(newEmail) => "E-mailadres gewijzigd naar ${newEmail}"; + static String m21(newEmail) => "E-mailadres gewijzigd naar ${newEmail}"; - static String m21(email) => + static String m22(email) => "${email} heeft geen Ente account.\n\nStuur ze een uitnodiging om foto\'s te delen."; - static String m22(count, formattedNumber) => + static String m23(count, formattedNumber) => "${Intl.plural(count, one: '1 bestand', other: '${formattedNumber} bestanden')} in dit album zijn veilig geback-upt"; - static String m23(count, formattedNumber) => + static String m24(count, formattedNumber) => "${Intl.plural(count, one: '1 bestand', other: '${formattedNumber} bestanden')} in dit album is veilig geback-upt"; - static String m24(storageAmountInGB) => + static String m25(storageAmountInGB) => "${storageAmountInGB} GB telkens als iemand zich aanmeldt voor een betaald abonnement en je code toepast"; - static String m25(freeAmount, storageUnit) => - "${freeAmount} ${storageUnit} vrij"; - static String m26(endDate) => "Gratis proefversie geldig tot ${endDate}"; static String m27(count) => @@ -377,6 +377,7 @@ class MessageLookup extends MessageLookupByLibrary { "autoPairDesc": MessageLookupByLibrary.simpleMessage( "Automatisch koppelen werkt alleen met apparaten die Chromecast ondersteunen."), "available": MessageLookupByLibrary.simpleMessage("Beschikbaar"), + "availableStorageSpace": m8, "backedUpFolders": MessageLookupByLibrary.simpleMessage("Back-up mappen"), "backup": MessageLookupByLibrary.simpleMessage("Back-up"), @@ -401,10 +402,10 @@ class MessageLookup extends MessageLookupByLibrary { "canOnlyRemoveFilesOwnedByYou": MessageLookupByLibrary.simpleMessage( "Kan alleen bestanden verwijderen die jouw eigendom zijn"), "cancel": MessageLookupByLibrary.simpleMessage("Annuleer"), - "cancelOtherSubscription": m8, + "cancelOtherSubscription": m9, "cancelSubscription": MessageLookupByLibrary.simpleMessage("Abonnement opzeggen"), - "cannotAddMorePhotosAfterBecomingViewer": m9, + "cannotAddMorePhotosAfterBecomingViewer": m10, "cannotDeleteSharedFiles": MessageLookupByLibrary.simpleMessage( "Kan gedeelde bestanden niet verwijderen"), "castIPMismatchBody": MessageLookupByLibrary.simpleMessage( @@ -432,7 +433,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Claim gratis opslag"), "claimMore": MessageLookupByLibrary.simpleMessage("Claim meer!"), "claimed": MessageLookupByLibrary.simpleMessage("Geclaimd"), - "claimedStorageSoFar": m10, + "claimedStorageSoFar": m11, "cleanUncategorized": MessageLookupByLibrary.simpleMessage("Ongecategoriseerd opschonen"), "cleanUncategorizedDescription": MessageLookupByLibrary.simpleMessage( @@ -459,7 +460,7 @@ 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": m11, + "collaborativeLinkCreatedFor": m12, "collaborator": MessageLookupByLibrary.simpleMessage("Samenwerker"), "collaboratorsCanAddPhotosAndVideosToTheSharedAlbum": MessageLookupByLibrary.simpleMessage( @@ -489,10 +490,10 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Bevestig herstelsleutel"), "connectToDevice": MessageLookupByLibrary.simpleMessage( "Verbinding maken met apparaat"), - "contactFamilyAdmin": m12, + "contactFamilyAdmin": m13, "contactSupport": MessageLookupByLibrary.simpleMessage("Contacteer klantenservice"), - "contactToManageSubscription": m13, + "contactToManageSubscription": m14, "contacts": MessageLookupByLibrary.simpleMessage("Contacten"), "contents": MessageLookupByLibrary.simpleMessage("Inhoud"), "continueLabel": MessageLookupByLibrary.simpleMessage("Doorgaan"), @@ -572,12 +573,12 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Verwijder van apparaat"), "deleteFromEnte": MessageLookupByLibrary.simpleMessage("Verwijder van Ente"), - "deleteItemCount": m14, + "deleteItemCount": m15, "deleteLocation": MessageLookupByLibrary.simpleMessage("Verwijder locatie"), "deletePhotos": MessageLookupByLibrary.simpleMessage("Foto\'s verwijderen"), - "deleteProgress": m15, + "deleteProgress": m16, "deleteReason1": MessageLookupByLibrary.simpleMessage( "Ik mis een belangrijke functie"), "deleteReason2": MessageLookupByLibrary.simpleMessage( @@ -598,8 +599,6 @@ class MessageLookup extends MessageLookupByLibrary { "designedToOutlive": MessageLookupByLibrary.simpleMessage( "Ontworpen om levenslang mee te gaan"), "details": MessageLookupByLibrary.simpleMessage("Details"), - "devAccountChanged": MessageLookupByLibrary.simpleMessage( - "Het ontwikkelaarsaccount dat we gebruiken om te publiceren in de App Store is veranderd. Daarom moet je opnieuw inloggen.\n\nOnze excuses voor het ongemak, helaas was dit onvermijdelijk."), "developerSettings": MessageLookupByLibrary.simpleMessage("Ontwikkelaarsinstellingen"), "developerSettingsWarning": MessageLookupByLibrary.simpleMessage( @@ -619,7 +618,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": m16, + "disableLinkMessage": m17, "disableTwofactor": MessageLookupByLibrary.simpleMessage( "Tweestapsverificatie uitschakelen"), "disablingTwofactorAuthentication": @@ -640,9 +639,9 @@ class MessageLookup extends MessageLookupByLibrary { "downloadFailed": MessageLookupByLibrary.simpleMessage("Download mislukt"), "downloading": MessageLookupByLibrary.simpleMessage("Downloaden..."), - "dropSupportEmail": m17, - "duplicateFileCountWithStorageSaved": m18, - "duplicateItemsGroup": m19, + "dropSupportEmail": m18, + "duplicateFileCountWithStorageSaved": m19, + "duplicateItemsGroup": m20, "edit": MessageLookupByLibrary.simpleMessage("Bewerken"), "editLocation": MessageLookupByLibrary.simpleMessage("Locatie bewerken"), @@ -655,8 +654,8 @@ class MessageLookup extends MessageLookupByLibrary { "Bewerkte locatie wordt alleen gezien binnen Ente"), "eligible": MessageLookupByLibrary.simpleMessage("gerechtigd"), "email": MessageLookupByLibrary.simpleMessage("E-mail"), - "emailChangedTo": m20, - "emailNoEnteAccount": m21, + "emailChangedTo": m21, + "emailNoEnteAccount": m22, "emailVerificationToggle": MessageLookupByLibrary.simpleMessage("E-mailverificatie"), "emailYourLogs": @@ -727,8 +726,6 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Exporteer je gegevens"), "faceRecognition": MessageLookupByLibrary.simpleMessage("Face recognition"), - "faceRecognitionIndexingDescription": MessageLookupByLibrary.simpleMessage( - "Please note that this will result in a higher bandwidth and battery usage until all items are indexed."), "faces": MessageLookupByLibrary.simpleMessage("Gezichten"), "failedToApplyCode": MessageLookupByLibrary.simpleMessage("Code toepassen mislukt"), @@ -766,8 +763,8 @@ class MessageLookup extends MessageLookupByLibrary { "fileTypes": MessageLookupByLibrary.simpleMessage("Bestandstype"), "fileTypesAndNames": MessageLookupByLibrary.simpleMessage("Bestandstypen en namen"), - "filesBackedUpFromDevice": m22, - "filesBackedUpInAlbum": m23, + "filesBackedUpFromDevice": m23, + "filesBackedUpInAlbum": m24, "filesDeleted": MessageLookupByLibrary.simpleMessage("Bestanden verwijderd"), "filesSavedToGallery": MessageLookupByLibrary.simpleMessage( @@ -780,8 +777,7 @@ class MessageLookup extends MessageLookupByLibrary { "foundFaces": MessageLookupByLibrary.simpleMessage("Found faces"), "freeStorageClaimed": MessageLookupByLibrary.simpleMessage("Gratis opslag geclaimd"), - "freeStorageOnReferralSuccess": m24, - "freeStorageSpace": m25, + "freeStorageOnReferralSuccess": m25, "freeStorageUsable": MessageLookupByLibrary.simpleMessage("Gratis opslag bruikbaar"), "freeTrial": MessageLookupByLibrary.simpleMessage("Gratis proefversie"), @@ -952,8 +948,6 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Machine Learning"), "magicSearch": MessageLookupByLibrary.simpleMessage("Magische zoekfunctie"), - "magicSearchDescription": MessageLookupByLibrary.simpleMessage( - "Houd er rekening mee dat dit zal resulteren in een hoger internet- en batterijverbruik totdat alle items zijn geĂŻndexeerd."), "manage": MessageLookupByLibrary.simpleMessage("Beheren"), "manageDeviceStorage": MessageLookupByLibrary.simpleMessage("Apparaatopslag beheren"), diff --git a/mobile/lib/generated/intl/messages_no.dart b/mobile/lib/generated/intl/messages_no.dart index cce44555aa..d3f7ffa1f3 100644 --- a/mobile/lib/generated/intl/messages_no.dart +++ b/mobile/lib/generated/intl/messages_no.dart @@ -67,8 +67,6 @@ class MessageLookup extends MessageLookupByLibrary { "Skriv inn e-postadressen din"), "faceRecognition": MessageLookupByLibrary.simpleMessage("Face recognition"), - "faceRecognitionIndexingDescription": MessageLookupByLibrary.simpleMessage( - "Please note that this will result in a higher bandwidth and battery usage until all items are indexed."), "feedback": MessageLookupByLibrary.simpleMessage("Tilbakemelding"), "fileTypes": MessageLookupByLibrary.simpleMessage("File types"), "foundFaces": MessageLookupByLibrary.simpleMessage("Found faces"), diff --git a/mobile/lib/generated/intl/messages_pl.dart b/mobile/lib/generated/intl/messages_pl.dart index 01cb3cb615..0e16b5830b 100644 --- a/mobile/lib/generated/intl/messages_pl.dart +++ b/mobile/lib/generated/intl/messages_pl.dart @@ -115,8 +115,6 @@ class MessageLookup extends MessageLookupByLibrary { "WprowadĆș swĂłj klucz odzyskiwania"), "faceRecognition": MessageLookupByLibrary.simpleMessage("Face recognition"), - "faceRecognitionIndexingDescription": MessageLookupByLibrary.simpleMessage( - "Please note that this will result in a higher bandwidth and battery usage until all items are indexed."), "feedback": MessageLookupByLibrary.simpleMessage("Informacja zwrotna"), "fileTypes": MessageLookupByLibrary.simpleMessage("File types"), "forgotPassword": diff --git a/mobile/lib/generated/intl/messages_pt.dart b/mobile/lib/generated/intl/messages_pt.dart index e32fd36379..8906f25c36 100644 --- a/mobile/lib/generated/intl/messages_pt.dart +++ b/mobile/lib/generated/intl/messages_pt.dart @@ -41,13 +41,16 @@ class MessageLookup extends MessageLookupByLibrary { static String m7(versionValue) => "VersĂŁo: ${versionValue}"; - static String m8(paymentProvider) => + static String m8(freeAmount, storageUnit) => + "${freeAmount} ${storageUnit} livre"; + + static String m9(paymentProvider) => "Por favor, cancele sua assinatura existente do ${paymentProvider} primeiro"; - static String m9(user) => + static String m10(user) => "${user} NĂŁo poderĂĄ adicionar mais fotos a este ĂĄlbum\n\nEles ainda poderĂŁo remover as fotos existentes adicionadas por eles"; - static String m10(isFamilyMember, storageAmountInGb) => + static String m11(isFamilyMember, storageAmountInGb) => "${Intl.select(isFamilyMember, { 'true': 'Sua famĂ­lia reeinvindicou ${storageAmountInGb} GB atĂ© agora', @@ -55,51 +58,48 @@ class MessageLookup extends MessageLookupByLibrary { 'other': 'VocĂȘ reeinvindicou ${storageAmountInGb} GB atĂ© agora', })}"; - static String m11(albumName) => "Link colaborativo criado para ${albumName}"; + static String m12(albumName) => "Link colaborativo criado para ${albumName}"; - static String m12(familyAdminEmail) => + static String m13(familyAdminEmail) => "Entre em contato com ${familyAdminEmail} para gerenciar sua assinatura"; - static String m13(provider) => + static String m14(provider) => "Entre em contato conosco pelo e-mail support@ente.io para gerenciar sua assinatura ${provider}."; static String m69(endpoint) => "Conectado a ${endpoint}"; - static String m14(count) => + static String m15(count) => "${Intl.plural(count, one: 'Excluir ${count} item', other: 'Excluir ${count} itens')}"; - static String m15(currentlyDeleting, totalCount) => + static String m16(currentlyDeleting, totalCount) => "Excluindo ${currentlyDeleting} / ${totalCount}"; - static String m16(albumName) => + static String m17(albumName) => "Isso removerĂĄ o link pĂșblico para acessar \"${albumName}\"."; - static String m17(supportEmail) => + static String m18(supportEmail) => "Por favor, envie um e-mail para ${supportEmail} a partir do seu endereço de e-mail registrado"; - static String m18(count, storageSaved) => + static String m19(count, storageSaved) => "VocĂȘ limpou ${Intl.plural(count, one: '${count} arquivo duplicado', other: '${count} arquivos duplicados')}, salvando (${storageSaved}!)"; - static String m19(count, formattedSize) => + static String m20(count, formattedSize) => "${count} Arquivos, ${formattedSize} cada"; - static String m20(newEmail) => "E-mail alterado para ${newEmail}"; + static String m21(newEmail) => "E-mail alterado para ${newEmail}"; - static String m21(email) => + static String m22(email) => "${email} nĂŁo possui uma conta Ente.\n\nEnvie um convite para compartilhar fotos."; - static String m22(count, formattedNumber) => + static String m23(count, formattedNumber) => "${Intl.plural(count, one: '1 arquivo', other: '${formattedNumber} arquivos')} neste dispositivo teve um backup seguro"; - static String m23(count, formattedNumber) => + static String m24(count, formattedNumber) => "${Intl.plural(count, one: '1 arquivo', other: '${formattedNumber} arquivos')} neste ĂĄlbum teve um backup seguro"; - static String m24(storageAmountInGB) => + static String m25(storageAmountInGB) => "${storageAmountInGB} GB cada vez que alguĂ©m se inscrever para um plano pago e aplica o seu cĂłdigo"; - static String m25(freeAmount, storageUnit) => - "${freeAmount} ${storageUnit} livre"; - static String m26(endDate) => "Teste gratuito acaba em ${endDate}"; static String m27(count) => @@ -375,6 +375,7 @@ class MessageLookup extends MessageLookupByLibrary { "autoPairDesc": MessageLookupByLibrary.simpleMessage( "O pareamento automĂĄtico funciona apenas com dispositivos que suportam o Chromecast."), "available": MessageLookupByLibrary.simpleMessage("DisponĂ­vel"), + "availableStorageSpace": m8, "backedUpFolders": MessageLookupByLibrary.simpleMessage("Backup de pastas concluĂ­do"), "backup": MessageLookupByLibrary.simpleMessage("Backup"), @@ -400,10 +401,10 @@ class MessageLookup extends MessageLookupByLibrary { "canOnlyRemoveFilesOwnedByYou": MessageLookupByLibrary.simpleMessage( "SĂł Ă© possĂ­vel remover arquivos de sua propriedade"), "cancel": MessageLookupByLibrary.simpleMessage("Cancelar"), - "cancelOtherSubscription": m8, + "cancelOtherSubscription": m9, "cancelSubscription": MessageLookupByLibrary.simpleMessage("Cancelar assinatura"), - "cannotAddMorePhotosAfterBecomingViewer": m9, + "cannotAddMorePhotosAfterBecomingViewer": m10, "cannotDeleteSharedFiles": MessageLookupByLibrary.simpleMessage( "NĂŁo Ă© possĂ­vel excluir arquivos compartilhados"), "castIPMismatchBody": MessageLookupByLibrary.simpleMessage( @@ -431,7 +432,7 @@ class MessageLookup extends MessageLookupByLibrary { "Reivindicar armazenamento gratuito"), "claimMore": MessageLookupByLibrary.simpleMessage("Reivindique mais!"), "claimed": MessageLookupByLibrary.simpleMessage("Reivindicado"), - "claimedStorageSoFar": m10, + "claimedStorageSoFar": m11, "cleanUncategorized": MessageLookupByLibrary.simpleMessage("Limpar Sem Categoria"), "cleanUncategorizedDescription": MessageLookupByLibrary.simpleMessage( @@ -458,7 +459,7 @@ 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": m11, + "collaborativeLinkCreatedFor": m12, "collaborator": MessageLookupByLibrary.simpleMessage("Colaborador"), "collaboratorsCanAddPhotosAndVideosToTheSharedAlbum": MessageLookupByLibrary.simpleMessage( @@ -487,10 +488,10 @@ class MessageLookup extends MessageLookupByLibrary { "Confirme sua chave de recuperação"), "connectToDevice": MessageLookupByLibrary.simpleMessage("Conectar ao dispositivo"), - "contactFamilyAdmin": m12, + "contactFamilyAdmin": m13, "contactSupport": MessageLookupByLibrary.simpleMessage("Contate o suporte"), - "contactToManageSubscription": m13, + "contactToManageSubscription": m14, "contacts": MessageLookupByLibrary.simpleMessage("Contatos"), "contents": MessageLookupByLibrary.simpleMessage("ConteĂșdos"), "continueLabel": MessageLookupByLibrary.simpleMessage("Continuar"), @@ -569,10 +570,10 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Excluir do dispositivo"), "deleteFromEnte": MessageLookupByLibrary.simpleMessage("Excluir do Ente"), - "deleteItemCount": m14, + "deleteItemCount": m15, "deleteLocation": MessageLookupByLibrary.simpleMessage("Excluir Local"), "deletePhotos": MessageLookupByLibrary.simpleMessage("Excluir fotos"), - "deleteProgress": m15, + "deleteProgress": m16, "deleteReason1": MessageLookupByLibrary.simpleMessage( "EstĂĄ faltando um recurso-chave que eu preciso"), "deleteReason2": MessageLookupByLibrary.simpleMessage( @@ -592,8 +593,6 @@ class MessageLookup extends MessageLookupByLibrary { "designedToOutlive": MessageLookupByLibrary.simpleMessage("Feito para ter longevidade"), "details": MessageLookupByLibrary.simpleMessage("Detalhes"), - "devAccountChanged": MessageLookupByLibrary.simpleMessage( - "A conta de desenvolvedor que usamos para publicar o Ente na App Store foi alterada. Por esse motivo, vocĂȘ precisarĂĄ fazer entrar novamente.\n\nPedimos desculpas pelo inconveniente, mas isso era inevitĂĄvel."), "developerSettings": MessageLookupByLibrary.simpleMessage( "ConfiguraçÔes de desenvolvedor"), "developerSettingsWarning": MessageLookupByLibrary.simpleMessage( @@ -613,7 +612,7 @@ class MessageLookup extends MessageLookupByLibrary { "Os espectadores ainda podem tirar screenshots ou salvar uma cĂłpia de suas fotos usando ferramentas externas"), "disableDownloadWarningTitle": MessageLookupByLibrary.simpleMessage("Observe"), - "disableLinkMessage": m16, + "disableLinkMessage": m17, "disableTwofactor": MessageLookupByLibrary.simpleMessage( "Desativar autenticação de dois fatores"), "disablingTwofactorAuthentication": @@ -629,7 +628,7 @@ class MessageLookup extends MessageLookupByLibrary { "doYouWantToDiscardTheEditsYouHaveMade": MessageLookupByLibrary.simpleMessage( "VocĂȘ quer descartar as ediçÔes que vocĂȘ fez?"), - "done": MessageLookupByLibrary.simpleMessage("ConcluĂ­do"), + "done": MessageLookupByLibrary.simpleMessage("Pronto"), "doubleYourStorage": MessageLookupByLibrary.simpleMessage("Dobre seu armazenamento"), "download": MessageLookupByLibrary.simpleMessage("Baixar"), @@ -637,9 +636,9 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Falha no download"), "downloading": MessageLookupByLibrary.simpleMessage("Fazendo download..."), - "dropSupportEmail": m17, - "duplicateFileCountWithStorageSaved": m18, - "duplicateItemsGroup": m19, + "dropSupportEmail": m18, + "duplicateFileCountWithStorageSaved": m19, + "duplicateItemsGroup": m20, "edit": MessageLookupByLibrary.simpleMessage("Editar"), "editLocation": MessageLookupByLibrary.simpleMessage("Editar local"), "editLocationTagTitle": @@ -650,8 +649,8 @@ class MessageLookup extends MessageLookupByLibrary { "EdiçÔes para local sĂł serĂŁo vistas dentro do Ente"), "eligible": MessageLookupByLibrary.simpleMessage("elegĂ­vel"), "email": MessageLookupByLibrary.simpleMessage("E-mail"), - "emailChangedTo": m20, - "emailNoEnteAccount": m21, + "emailChangedTo": m21, + "emailNoEnteAccount": m22, "emailVerificationToggle": MessageLookupByLibrary.simpleMessage("Verificação de e-mail"), "emailYourLogs": @@ -721,8 +720,6 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Exportar seus dados"), "faceRecognition": MessageLookupByLibrary.simpleMessage("Reconhecimento facial"), - "faceRecognitionIndexingDescription": MessageLookupByLibrary.simpleMessage( - "Por favor, note que isso resultarĂĄ em uma largura de banda maior e uso de bateria atĂ© que todos os itens sejam indexados."), "faces": MessageLookupByLibrary.simpleMessage("Rostos"), "failedToApplyCode": MessageLookupByLibrary.simpleMessage("Falha ao aplicar o cĂłdigo"), @@ -758,8 +755,8 @@ class MessageLookup extends MessageLookupByLibrary { "fileTypes": MessageLookupByLibrary.simpleMessage("Tipos de arquivo"), "fileTypesAndNames": MessageLookupByLibrary.simpleMessage("Tipos de arquivo e nomes"), - "filesBackedUpFromDevice": m22, - "filesBackedUpInAlbum": m23, + "filesBackedUpFromDevice": m23, + "filesBackedUpInAlbum": m24, "filesDeleted": MessageLookupByLibrary.simpleMessage("Arquivos excluĂ­dos"), "filesSavedToGallery": @@ -775,8 +772,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Rostos encontrados"), "freeStorageClaimed": MessageLookupByLibrary.simpleMessage( "Armazenamento gratuito reivindicado"), - "freeStorageOnReferralSuccess": m24, - "freeStorageSpace": m25, + "freeStorageOnReferralSuccess": m25, "freeStorageUsable": MessageLookupByLibrary.simpleMessage( "Armazenamento livre utilizĂĄvel"), "freeTrial": MessageLookupByLibrary.simpleMessage("Teste gratuito"), @@ -820,7 +816,7 @@ class MessageLookup extends MessageLookupByLibrary { "A autenticação biomĂ©trica nĂŁo estĂĄ configurada no seu dispositivo. Por favor, ative o Touch ID ou o Face ID no seu telefone."), "iOSLockOut": MessageLookupByLibrary.simpleMessage( "A Autenticação BiomĂ©trica estĂĄ desativada. Por favor, bloqueie e desbloqueie sua tela para ativĂĄ-la."), - "iOSOkButton": MessageLookupByLibrary.simpleMessage("Aceitar"), + "iOSOkButton": MessageLookupByLibrary.simpleMessage("Tudo bem"), "ignoreUpdate": MessageLookupByLibrary.simpleMessage("Ignorar"), "ignoredFolderUploadReason": MessageLookupByLibrary.simpleMessage( "Alguns arquivos neste ĂĄlbum sĂŁo ignorados do envio porque eles tinham sido anteriormente excluĂ­dos do Ente."), @@ -837,7 +833,7 @@ class MessageLookup extends MessageLookupByLibrary { "Chave de recuperação incorreta"), "indexedItems": MessageLookupByLibrary.simpleMessage("Itens indexados"), "indexingIsPaused": MessageLookupByLibrary.simpleMessage( - "Indexing is paused, will automatically resume when device is ready"), + "A indexação estĂĄ pausada, serĂĄ retomada automaticamente quando o dispositivo estiver pronto."), "insecureDevice": MessageLookupByLibrary.simpleMessage("Dispositivo nĂŁo seguro"), "installManually": @@ -950,8 +946,6 @@ class MessageLookup extends MessageLookupByLibrary { "machineLearning": MessageLookupByLibrary.simpleMessage("Aprendizagem de mĂĄquina"), "magicSearch": MessageLookupByLibrary.simpleMessage("Busca mĂĄgica"), - "magicSearchDescription": MessageLookupByLibrary.simpleMessage( - "Por favor, note que isso resultarĂĄ em uma largura de banda maior e uso de bateria atĂ© que todos os itens sejam indexados."), "manage": MessageLookupByLibrary.simpleMessage("Gerenciar"), "manageDeviceStorage": MessageLookupByLibrary.simpleMessage( "Gerenciar o armazenamento do dispositivo"), @@ -969,6 +963,8 @@ class MessageLookup extends MessageLookupByLibrary { "matrix": MessageLookupByLibrary.simpleMessage("Matrix"), "memoryCount": m33, "merchandise": MessageLookupByLibrary.simpleMessage("Produtos"), + "mlIndexingDescription": MessageLookupByLibrary.simpleMessage( + "Por favor, note que isso resultarĂĄ em uma largura de banda maior e uso de bateria atĂ© que todos os itens sejam indexados."), "mobileWebDesktop": MessageLookupByLibrary.simpleMessage("Mobile, Web, Desktop"), "moderateStrength": MessageLookupByLibrary.simpleMessage("Moderada"), @@ -1029,11 +1025,11 @@ class MessageLookup extends MessageLookupByLibrary { "nothingToSeeHere": MessageLookupByLibrary.simpleMessage("Nada para ver aqui! 👀"), "notifications": MessageLookupByLibrary.simpleMessage("NotificaçÔes"), - "ok": MessageLookupByLibrary.simpleMessage("Ok"), + "ok": MessageLookupByLibrary.simpleMessage("OK"), "onDevice": MessageLookupByLibrary.simpleMessage("No dispositivo"), "onEnte": MessageLookupByLibrary.simpleMessage( "Em ente"), - "oops": MessageLookupByLibrary.simpleMessage("Ops"), + "oops": MessageLookupByLibrary.simpleMessage("Opa"), "oopsCouldNotSaveEdits": MessageLookupByLibrary.simpleMessage( "Ops, nĂŁo foi possĂ­vel salvar ediçÔes"), "oopsSomethingWentWrong": @@ -1314,7 +1310,7 @@ class MessageLookup extends MessageLookupByLibrary { "As pastas selecionadas serĂŁo criptografadas e armazenadas em backup"), "selectedItemsWillBeDeletedFromAllAlbumsAndMoved": MessageLookupByLibrary.simpleMessage( - "Os itens selecionados serĂŁo excluĂ­dos de todos os ĂĄlbuns e movidos para o lixo."), + "Os itens selecionados serĂŁo excluĂ­dos de todos os ĂĄlbuns e movidos para a lixeira."), "selectedPhotos": m46, "selectedPhotosWithYours": m47, "send": MessageLookupByLibrary.simpleMessage("Enviar"), diff --git a/mobile/lib/generated/intl/messages_zh.dart b/mobile/lib/generated/intl/messages_zh.dart index ecca5d7b8f..4a9461e850 100644 --- a/mobile/lib/generated/intl/messages_zh.dart +++ b/mobile/lib/generated/intl/messages_zh.dart @@ -41,59 +41,56 @@ class MessageLookup extends MessageLookupByLibrary { static String m7(versionValue) => "ç‰ˆæœŹ: ${versionValue}"; - static String m8(paymentProvider) => "èŻ·ć…ˆć–æ¶ˆæ‚šçŽ°æœ‰çš„èźąé˜… ${paymentProvider}"; + static String m9(paymentProvider) => "èŻ·ć…ˆć–æ¶ˆæ‚šçŽ°æœ‰çš„èźąé˜… ${paymentProvider}"; - static String m9(user) => "${user} ć°†æ— æł•æ·»ćŠ æ›Žć€šç…§ç‰‡ćˆ°æ­€ç›žć†Œ\n\nä»–ä»Źä»ç„¶èƒœć€Ÿćˆ é™€ä»–ä»Źæ·»ćŠ çš„çŽ°æœ‰ç…§ç‰‡"; + static String m10(user) => "${user} ć°†æ— æł•æ·»ćŠ æ›Žć€šç…§ç‰‡ćˆ°æ­€ç›žć†Œ\n\nä»–ä»Źä»ç„¶èƒœć€Ÿćˆ é™€ä»–ä»Źæ·»ćŠ çš„çŽ°æœ‰ç…§ç‰‡"; - static String m10(isFamilyMember, storageAmountInGb) => + static String m11(isFamilyMember, storageAmountInGb) => "${Intl.select(isFamilyMember, { 'true': '戰盼才äžșæ­ąïŒŒæ‚šçš„ćź¶ćș­ć·Čç»éą†ć–äș† ${storageAmountInGb} GB', 'false': '戰盼才äžșæ­ąïŒŒæ‚šć·Čç»éą†ć–äș† ${storageAmountInGb} GB', 'other': '戰盼才äžșæ­ąïŒŒæ‚šć·Čç»éą†ć–äș†${storageAmountInGb} GB', })}"; - static String m11(albumName) => "äžș ${albumName} 戛ć»șäș†ćäœœé“ŸæŽ„"; + static String m12(albumName) => "äžș ${albumName} 戛ć»șäș†ćäœœé“ŸæŽ„"; - static String m12(familyAdminEmail) => + static String m13(familyAdminEmail) => "èŻ·è”çł» ${familyAdminEmail} æ„çźĄç†æ‚šçš„èźąé˜…"; - static String m13(provider) => + static String m14(provider) => "èŻ·é€šèż‡support@ente.io ç”šè‹±èŻ­è”çł»æˆ‘ä»Źæ„çźĄç†æ‚šçš„ ${provider} èźąé˜…ă€‚"; static String m69(endpoint) => "ć·ČèżžæŽ„è‡ł ${endpoint}"; - static String m14(count) => + static String m15(count) => "${Intl.plural(count, one: 'ćˆ é™€ ${count} äžȘéĄč盼', other: 'ćˆ é™€ ${count} äžȘéĄč盼')}"; - static String m15(currentlyDeleting, totalCount) => + static String m16(currentlyDeleting, totalCount) => "æ­Łćœšćˆ é™€ ${currentlyDeleting} /ć…± ${totalCount}"; - static String m16(albumName) => "èż™ć°†ćˆ é™€ç”šäșŽèźżé—ź\"${albumName}\"çš„ć…ŹćŒ€é“ŸæŽ„ă€‚"; + static String m17(albumName) => "èż™ć°†ćˆ é™€ç”šäșŽèźżé—ź\"${albumName}\"çš„ć…ŹćŒ€é“ŸæŽ„ă€‚"; - static String m17(supportEmail) => "èŻ·ä»Žæ‚šæłšć†Œçš„é‚źçź±ć‘é€äž€ć°é‚źä»¶ćˆ° ${supportEmail}"; + static String m18(supportEmail) => "èŻ·ä»Žæ‚šæłšć†Œçš„é‚źçź±ć‘é€äž€ć°é‚źä»¶ćˆ° ${supportEmail}"; - static String m18(count, storageSaved) => + static String m19(count, storageSaved) => "悚ć·Č经枅理äș† ${Intl.plural(count, other: '${count} äžȘé‡ć€æ–‡ä»¶')}, 释攟äș† (${storageSaved}!)"; - static String m19(count, formattedSize) => + static String m20(count, formattedSize) => "${count} äžȘæ–‡ä»¶ïŒŒæŻäžȘ文件 ${formattedSize}"; - static String m20(newEmail) => "甔歐邟件ć·Č曎æ”čäžș ${newEmail}"; + static String m21(newEmail) => "甔歐邟件ć·Č曎æ”čäžș ${newEmail}"; - static String m21(email) => "${email} æČĄæœ‰ Ente ćžæˆ·ă€‚\n\nć‘ä»–ä»Źć‘ć‡șć…±äș«ç…§ç‰‡çš„é‚€èŻ·ă€‚"; - - static String m22(count, formattedNumber) => - "æ­€èźŸć€‡äžŠçš„ ${Intl.plural(count, one: '1 äžȘ文件', other: '${formattedNumber} äžȘ文件')} ć·Čćź‰ć…šć€‡ä»œ"; + static String m22(email) => "${email} æČĄæœ‰ Ente ćžæˆ·ă€‚\n\nć‘ä»–ä»Źć‘ć‡șć…±äș«ç…§ç‰‡çš„é‚€èŻ·ă€‚"; static String m23(count, formattedNumber) => + "æ­€èźŸć€‡äžŠçš„ ${Intl.plural(count, one: '1 äžȘ文件', other: '${formattedNumber} äžȘ文件')} ć·Čćź‰ć…šć€‡ä»œ"; + + static String m24(count, formattedNumber) => "æ­€ç›žć†Œäž­çš„ ${Intl.plural(count, one: '1 äžȘ文件', other: '${formattedNumber} äžȘ文件')} ć·Čćź‰ć…šć€‡ä»œ"; - static String m24(storageAmountInGB) => + static String m25(storageAmountInGB) => "æŻćœ“æœ‰äșșäœżç”šæ‚šçš„ä»Łç æłšć†Œä»˜èŽčèźĄćˆ’æ—¶æ‚šć°†èŽ·ćŸ—${storageAmountInGB} GB"; - static String m25(freeAmount, storageUnit) => - "${freeAmount} ${storageUnit} ç©șé—Č"; - static String m26(endDate) => "慍èŽčèŻ•ç”šæœ‰æ•ˆæœŸè‡ł ${endDate}"; static String m27(count) => @@ -124,7 +121,7 @@ class MessageLookup extends MessageLookupByLibrary { static String m37(providerName) => "ćŠ‚æžœæ‚šèą«æ”¶ć–èŽčç”šïŒŒèŻ·ç”šè‹±èŻ­äžŽ ${providerName} çš„ćźąæœèŠć€©"; - static String m38(endDate) => "慍èŽčèŻ•ç”šæœ‰æ•ˆæœŸè‡ł ${endDate}。\næ‚šćŻä»„éšćŽèŽ­äč°ä»˜èŽčèźĄćˆ’ă€‚"; + static String m38(endDate) => "慍èŽčèŻ•ç”šæœ‰æ•ˆæœŸè‡ł ${endDate}。\nćœšæ­€äč‹ćŽæ‚šćŻä»„选择付èŽčèźĄćˆ’ă€‚"; static String m39(toEmail) => "èŻ·ç»™æˆ‘ä»Źć‘é€ç””ć­é‚źä»¶è‡ł ${toEmail}"; @@ -206,6 +203,7 @@ class MessageLookup extends MessageLookupByLibrary { "ackPasswordLostWarning": MessageLookupByLibrary.simpleMessage( "æˆ‘æ˜Žç™œïŒŒćŠ‚æžœæˆ‘äžąć€±ćŻ†ç ïŒŒæˆ‘ćŻèƒœäŒšäžąć€±æˆ‘çš„æ•°æźïŒŒć› äžșæˆ‘çš„æ•°æźæ˜Ż ç«Żćˆ°ç«ŻćŠ ćŻ†çš„ă€‚"), "activeSessions": MessageLookupByLibrary.simpleMessage("ć·Čç™»ćœ•çš„èźŸć€‡"), + "addAName": MessageLookupByLibrary.simpleMessage("æ·»ćŠ äž€äžȘ損称"), "addANewEmail": MessageLookupByLibrary.simpleMessage("æ·»ćŠ æ–°çš„ç””ć­é‚źä»¶"), "addCollaborator": MessageLookupByLibrary.simpleMessage("æ·»ćŠ ćäœœè€…"), "addCollaborators": m0, @@ -346,9 +344,9 @@ class MessageLookup extends MessageLookupByLibrary { "canOnlyRemoveFilesOwnedByYou": MessageLookupByLibrary.simpleMessage("ćȘèƒœćˆ é™€æ‚šæ‹„æœ‰çš„æ–‡ä»¶"), "cancel": MessageLookupByLibrary.simpleMessage("ć–æ¶ˆ"), - "cancelOtherSubscription": m8, + "cancelOtherSubscription": m9, "cancelSubscription": MessageLookupByLibrary.simpleMessage("ć–æ¶ˆèźąé˜…"), - "cannotAddMorePhotosAfterBecomingViewer": m9, + "cannotAddMorePhotosAfterBecomingViewer": m10, "cannotDeleteSharedFiles": MessageLookupByLibrary.simpleMessage("æ— æł•ćˆ é™€ć…±äș«æ–‡ä»¶"), "castIPMismatchBody": @@ -370,7 +368,7 @@ class MessageLookup extends MessageLookupByLibrary { "claimFreeStorage": MessageLookupByLibrary.simpleMessage("鱆揖慍èŽč歘悹"), "claimMore": MessageLookupByLibrary.simpleMessage("éą†ć–æ›Žć€šïŒ"), "claimed": MessageLookupByLibrary.simpleMessage("ć·Č鱆揖"), - "claimedStorageSoFar": m10, + "claimedStorageSoFar": m11, "cleanUncategorized": MessageLookupByLibrary.simpleMessage("枅陀æœȘćˆ†ç±»çš„"), "cleanUncategorizedDescription": MessageLookupByLibrary.simpleMessage("从“æœȘćˆ†ç±»â€äž­ćˆ é™€ć…¶ä»–ç›žć†Œäž­ć­˜ćœšçš„æ‰€æœ‰æ–‡ä»¶"), @@ -382,8 +380,7 @@ class MessageLookup extends MessageLookupByLibrary { "close": MessageLookupByLibrary.simpleMessage("慳闭"), "clubByCaptureTime": MessageLookupByLibrary.simpleMessage("æŒ‰æ‹æ‘„æ—¶é—Žćˆ†ç»„"), "clubByFileName": MessageLookupByLibrary.simpleMessage("æŒ‰æ–‡ä»¶ćæŽ’ćș"), - "clusteringProgress": - MessageLookupByLibrary.simpleMessage("Clustering progress"), + "clusteringProgress": MessageLookupByLibrary.simpleMessage("èšç±»èż›ć±•"), "codeAppliedPageTitle": MessageLookupByLibrary.simpleMessage("代码ć·Čćș”甚"), "codeCopiedToClipboard": MessageLookupByLibrary.simpleMessage("代码ć·Čć€ćˆ¶ćˆ°ć‰ȘèŽŽæż"), @@ -391,7 +388,7 @@ class MessageLookup extends MessageLookupByLibrary { "collabLinkSectionDescription": MessageLookupByLibrary.simpleMessage( "戛ć»ș侀äžȘé“ŸæŽ„æ„èź©ä»–äșș无需 Ente ćș”甚皋ćșæˆ–èŽŠæˆ·ćłćŻćœšæ‚šçš„ć…±äș«ç›žć†Œäž­æ·»ćŠ ć’ŒæŸ„çœ‹ç…§ç‰‡ă€‚éžćžžé€‚ćˆæ”¶é›†æŽ»ćŠšç…§ç‰‡ă€‚"), "collaborativeLink": MessageLookupByLibrary.simpleMessage("ćäœœé“ŸæŽ„"), - "collaborativeLinkCreatedFor": m11, + "collaborativeLinkCreatedFor": m12, "collaborator": MessageLookupByLibrary.simpleMessage("ćäœœè€…"), "collaboratorsCanAddPhotosAndVideosToTheSharedAlbum": MessageLookupByLibrary.simpleMessage("ćäœœè€…ćŻä»„ć°†ç…§ç‰‡ć’Œè§†éą‘æ·»ćŠ ćˆ°ć…±äș«ç›žć†Œäž­ă€‚"), @@ -413,9 +410,9 @@ class MessageLookup extends MessageLookupByLibrary { "confirmYourRecoveryKey": MessageLookupByLibrary.simpleMessage("çĄźèź€æ‚šçš„æąć€ćŻ†é’„"), "connectToDevice": MessageLookupByLibrary.simpleMessage("èżžæŽ„ćˆ°èźŸć€‡"), - "contactFamilyAdmin": m12, + "contactFamilyAdmin": m13, "contactSupport": MessageLookupByLibrary.simpleMessage("è”çł»æ”ŻæŒ"), - "contactToManageSubscription": m13, + "contactToManageSubscription": m14, "contacts": MessageLookupByLibrary.simpleMessage("è”çł»äșș"), "contents": MessageLookupByLibrary.simpleMessage("憅ćźč"), "continueLabel": MessageLookupByLibrary.simpleMessage("ç»§ç»­"), @@ -476,10 +473,10 @@ class MessageLookup extends MessageLookupByLibrary { "deleteFromBoth": MessageLookupByLibrary.simpleMessage("ćŒæ—¶ä»Žäž€è€…äž­ćˆ é™€"), "deleteFromDevice": MessageLookupByLibrary.simpleMessage("ä»ŽèźŸć€‡äž­ćˆ é™€"), "deleteFromEnte": MessageLookupByLibrary.simpleMessage("从 Ente äž­ćˆ é™€"), - "deleteItemCount": m14, + "deleteItemCount": m15, "deleteLocation": MessageLookupByLibrary.simpleMessage("ćˆ é™€äœçœź"), "deletePhotos": MessageLookupByLibrary.simpleMessage("ćˆ é™€ç…§ç‰‡"), - "deleteProgress": m15, + "deleteProgress": m16, "deleteReason1": MessageLookupByLibrary.simpleMessage("æ‰Ÿäžćˆ°æˆ‘æƒłèŠçš„ćŠŸèƒœ"), "deleteReason2": MessageLookupByLibrary.simpleMessage("ćș”甚或某äžȘćŠŸèƒœæČĄæœ‰æŒ‰æˆ‘çš„éą„æœŸèżèĄŒ"), @@ -495,8 +492,6 @@ class MessageLookup extends MessageLookupByLibrary { "deselectAll": MessageLookupByLibrary.simpleMessage("ć–æ¶ˆć…šé€‰"), "designedToOutlive": MessageLookupByLibrary.simpleMessage("经äč…耐甚"), "details": MessageLookupByLibrary.simpleMessage("èŻŠæƒ…"), - "devAccountChanged": MessageLookupByLibrary.simpleMessage( - "æˆ‘ä»Źç”šäșŽćœš App Store 侊揑澃 Ente çš„ćŒ€ć‘è€…èŽŠæˆ·ć·Č曎æ”čă€‚ć› æ­€ïŒŒæ‚šéœ€èŠé‡æ–°ç™»ćœ•ă€‚\n\nćŻčäșŽç»™æ‚šćžŠæ„çš„äžäŸżïŒŒæˆ‘ä»Źæ·±èĄšæ­‰æ„ïŒŒäœ†èż™æ˜ŻäžćŻéżć…çš„ă€‚"), "developerSettings": MessageLookupByLibrary.simpleMessage("ćŒ€ć‘è€…èźŸçœź"), "developerSettingsWarning": MessageLookupByLibrary.simpleMessage("æ‚šçĄźćźšèŠäżźæ”čćŒ€ć‘è€…èźŸçœźć—ïŒŸ"), @@ -512,7 +507,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("æŸ„çœ‹è€…ä»ç„¶ćŻä»„äœżç”šć€–éƒšć·„ć…·æˆȘć›Ÿæˆ–äżć­˜æ‚šçš„ç…§ç‰‡ć‰ŻæœŹ"), "disableDownloadWarningTitle": MessageLookupByLibrary.simpleMessage("èŻ·æłšæ„"), - "disableLinkMessage": m16, + "disableLinkMessage": m17, "disableTwofactor": MessageLookupByLibrary.simpleMessage("çŠç”šćŒé‡èź€èŻ"), "disablingTwofactorAuthentication": MessageLookupByLibrary.simpleMessage("æ­ŁćœšçŠç”šćŒé‡èź€èŻ..."), @@ -529,9 +524,9 @@ class MessageLookup extends MessageLookupByLibrary { "download": MessageLookupByLibrary.simpleMessage("䞋蜜"), "downloadFailed": MessageLookupByLibrary.simpleMessage("äž‹èŒ‰ć€±æ•—"), "downloading": MessageLookupByLibrary.simpleMessage("æ­Łćœšäž‹èœœ..."), - "dropSupportEmail": m17, - "duplicateFileCountWithStorageSaved": m18, - "duplicateItemsGroup": m19, + "dropSupportEmail": m18, + "duplicateFileCountWithStorageSaved": m19, + "duplicateItemsGroup": m20, "edit": MessageLookupByLibrary.simpleMessage("猖蟑"), "editLocation": MessageLookupByLibrary.simpleMessage("çŒ–èŸ‘äœçœź"), "editLocationTagTitle": MessageLookupByLibrary.simpleMessage("çŒ–èŸ‘äœçœź"), @@ -540,8 +535,8 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("ćŻčäœçœźçš„çŒ–èŸ‘ćȘèƒœćœš Ente 憅看戰"), "eligible": MessageLookupByLibrary.simpleMessage("çŹŠćˆè”„æ Œ"), "email": MessageLookupByLibrary.simpleMessage("ç””ć­é‚źä»¶ćœ°ć€"), - "emailChangedTo": m20, - "emailNoEnteAccount": m21, + "emailChangedTo": m21, + "emailNoEnteAccount": m22, "emailVerificationToggle": MessageLookupByLibrary.simpleMessage("甔歐邟件éȘŒè݁"), "emailYourLogs": MessageLookupByLibrary.simpleMessage("é€šèż‡ç””ć­é‚źä»¶ć‘é€æ‚šçš„æ—„ćż—"), @@ -576,6 +571,7 @@ class MessageLookup extends MessageLookupByLibrary { "enterPassword": MessageLookupByLibrary.simpleMessage("èŸ“ć…„ćŻ†ç "), "enterPasswordToEncrypt": MessageLookupByLibrary.simpleMessage("èŸ“ć…„æˆ‘ä»ŹćŻä»„ç”šæ„ćŠ ćŻ†æ‚šçš„æ•°æźçš„ćŻ†ç "), + "enterPersonName": MessageLookupByLibrary.simpleMessage("èŸ“ć…„äșșç‰©ćç§°"), "enterReferralCode": MessageLookupByLibrary.simpleMessage("èŸ“ć…„æŽšèä»Łç "), "enterThe6digitCodeFromnyourAuthenticatorApp": MessageLookupByLibrary.simpleMessage("从䜠的èș«ä»œéȘŒèŻć™šćș”甚䞭\nèŸ“ć…„6äœæ•°ć­—ä»Łç "), @@ -594,10 +590,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("歀铟掄ć·Čèż‡æœŸă€‚èŻ·é€‰æ‹©æ–°çš„èż‡æœŸæ—¶é—Žæˆ–çŠç”šé“ŸæŽ„æœ‰æ•ˆæœŸă€‚"), "exportLogs": MessageLookupByLibrary.simpleMessage("ćŻŒć‡șæ—„ćż—"), "exportYourData": MessageLookupByLibrary.simpleMessage("ćŻŒć‡șæ‚šçš„æ•°æź"), - "faceRecognition": - MessageLookupByLibrary.simpleMessage("Face recognition"), - "faceRecognitionIndexingDescription": MessageLookupByLibrary.simpleMessage( - "Please note that this will result in a higher bandwidth and battery usage until all items are indexed."), + "faceRecognition": MessageLookupByLibrary.simpleMessage("äșșè„žèŻ†ćˆ«"), "faces": MessageLookupByLibrary.simpleMessage("äșș脞"), "failedToApplyCode": MessageLookupByLibrary.simpleMessage("æ— æł•äœżç”šæ­€ä»Łç "), "failedToCancel": MessageLookupByLibrary.simpleMessage("ć–æ¶ˆć€±èŽ„"), @@ -624,18 +617,18 @@ class MessageLookup extends MessageLookupByLibrary { "fileSavedToGallery": MessageLookupByLibrary.simpleMessage("文件ć·Č保歘戰盾憌"), "fileTypes": MessageLookupByLibrary.simpleMessage("æ–‡ä»¶ç±»ćž‹"), "fileTypesAndNames": MessageLookupByLibrary.simpleMessage("æ–‡ä»¶ç±»ćž‹ć’Œćç§°"), - "filesBackedUpFromDevice": m22, - "filesBackedUpInAlbum": m23, + "filesBackedUpFromDevice": m23, + "filesBackedUpInAlbum": m24, "filesDeleted": MessageLookupByLibrary.simpleMessage("文件ć·Čćˆ é™€"), "filesSavedToGallery": MessageLookupByLibrary.simpleMessage("〚äžȘ文件ć·Č保歘戰盾憌"), + "findPeopleByName": MessageLookupByLibrary.simpleMessage("æŒ‰ćç§°ćż«é€ŸæŸ„æ‰Ÿäșș物"), "flip": MessageLookupByLibrary.simpleMessage("äžŠäž‹çż»èœŹ"), "forYourMemories": MessageLookupByLibrary.simpleMessage("äžșæ‚šçš„ć›žćż†"), "forgotPassword": MessageLookupByLibrary.simpleMessage("ćż˜èź°ćŻ†ç "), - "foundFaces": MessageLookupByLibrary.simpleMessage("Found faces"), + "foundFaces": MessageLookupByLibrary.simpleMessage("ć·Čæ‰Ÿćˆ°çš„äșș脞"), "freeStorageClaimed": MessageLookupByLibrary.simpleMessage("ć·Č鱆揖的慍èŽč歘悹"), - "freeStorageOnReferralSuccess": m24, - "freeStorageSpace": m25, + "freeStorageOnReferralSuccess": m25, "freeStorageUsable": MessageLookupByLibrary.simpleMessage("揯甹的慍èŽč歘悹"), "freeTrial": MessageLookupByLibrary.simpleMessage("慍èŽčèŻ•ç”š"), "freeTrialValidTill": m26, @@ -686,8 +679,8 @@ class MessageLookup extends MessageLookupByLibrary { "incorrectRecoveryKeyTitle": MessageLookupByLibrary.simpleMessage("äžæ­ŁçĄźçš„æąć€ćŻ†é’„"), "indexedItems": MessageLookupByLibrary.simpleMessage("ć·ČçŽąćŒ•éĄč盼"), - "indexingIsPaused": MessageLookupByLibrary.simpleMessage( - "Indexing is paused, will automatically resume when device is ready"), + "indexingIsPaused": + MessageLookupByLibrary.simpleMessage("çŽąćŒ•ć·Čæš‚ćœă€‚ćœ“èźŸć€‡ć‡†ć€‡ć°±ç»Șæ—¶ïŒŒćźƒć°†è‡ȘćŠšæąć€ă€‚"), "insecureDevice": MessageLookupByLibrary.simpleMessage("èźŸć€‡äžćź‰ć…š"), "installManually": MessageLookupByLibrary.simpleMessage("æ‰‹ćŠšćź‰èŁ…"), "invalidEmailAddress": @@ -779,8 +772,6 @@ class MessageLookup extends MessageLookupByLibrary { "lostDevice": MessageLookupByLibrary.simpleMessage("èźŸć€‡äžąć€±ïŒŸ"), "machineLearning": MessageLookupByLibrary.simpleMessage("æœș晹歩äč "), "magicSearch": MessageLookupByLibrary.simpleMessage("é­”æł•æœçŽą"), - "magicSearchDescription": MessageLookupByLibrary.simpleMessage( - "èŻ·æłšæ„ïŒŒćœšæ‰€æœ‰éĄčç›źćźŒæˆçŽąćŒ•äč‹ć‰ïŒŒèż™ć°†äœżç”šæ›Žé«˜çš„ćžŠćźœć’Œç””量。"), "manage": MessageLookupByLibrary.simpleMessage("缡理"), "manageDeviceStorage": MessageLookupByLibrary.simpleMessage("çźĄç†èźŸć€‡ć­˜ć‚š"), "manageFamily": MessageLookupByLibrary.simpleMessage("知理柶ćș­èźĄćˆ’"), @@ -795,6 +786,8 @@ class MessageLookup extends MessageLookupByLibrary { "matrix": MessageLookupByLibrary.simpleMessage("Matrix"), "memoryCount": m33, "merchandise": MessageLookupByLibrary.simpleMessage("敆擁"), + "mlIndexingDescription": MessageLookupByLibrary.simpleMessage( + "èŻ·æłšæ„ïŒŒæœș晹歩äč ć°†äœżç”šæ›Žé«˜çš„ćžŠćźœć’Œæ›Žć€šçš„ç””é‡ïŒŒç›Žćˆ°æ‰€æœ‰éĄčç›źéƒœèą«çŽąćŒ•äžșæ­ąă€‚"), "mobileWebDesktop": MessageLookupByLibrary.simpleMessage("移抚端, çœ‘éĄ”ç«Ż, æĄŒéąç«Ż"), "moderateStrength": MessageLookupByLibrary.simpleMessage("侭等"), @@ -880,6 +873,7 @@ class MessageLookup extends MessageLookupByLibrary { "paymentFailedTalkToProvider": m37, "pendingItems": MessageLookupByLibrary.simpleMessage("ćŸ…ć€„ç†éĄč盼"), "pendingSync": MessageLookupByLibrary.simpleMessage("æ­Łćœšç­‰ćŸ…ćŒæ­„"), + "people": MessageLookupByLibrary.simpleMessage("äșș物"), "peopleUsingYourCode": MessageLookupByLibrary.simpleMessage("äœżç”šæ‚šçš„ä»Łç çš„äșș"), "permDeleteWarning": MessageLookupByLibrary.simpleMessage("ć›žæ”¶ç«™äž­çš„æ‰€æœ‰éĄčç›źć°†èą«æ°žäč…ćˆ é™€\n\næ­€æ“äœœæ— æł•æ’€æ¶ˆ"), @@ -979,6 +973,7 @@ class MessageLookup extends MessageLookupByLibrary { "removeLink": MessageLookupByLibrary.simpleMessage("移陀铟掄"), "removeParticipant": MessageLookupByLibrary.simpleMessage("ç§»é™€ć‚äžŽè€…"), "removeParticipantBody": m43, + "removePersonLabel": MessageLookupByLibrary.simpleMessage("移陀äșș物标筟"), "removePublicLink": MessageLookupByLibrary.simpleMessage("ćˆ é™€ć…ŹćŒ€é“ŸæŽ„"), "removeShareItemsWarning": MessageLookupByLibrary.simpleMessage("æ‚šèŠćˆ é™€çš„æŸäș›éĄčç›źæ˜Żç”±ć…¶ä»–äșșæ·»ćŠ çš„ïŒŒæ‚šć°†æ— æł•èźżé—źćźƒä»Ź"), @@ -1025,7 +1020,7 @@ class MessageLookup extends MessageLookupByLibrary { "searchDatesEmptySection": MessageLookupByLibrary.simpleMessage("æŒ‰æ—„æœŸæœçŽąïŒŒæœˆä»œæˆ–ćčŽä»œ"), "searchFaceEmptySection": - MessageLookupByLibrary.simpleMessage("柄扟䞀äžȘäșș的所有照片"), + MessageLookupByLibrary.simpleMessage("ćŸ…çŽąćŒ•ćźŒæˆćŽïŒŒäșșç‰©ć°†æ˜Ÿç€șćœšæ­€ć€„"), "searchFileTypesAndNamesEmptySection": MessageLookupByLibrary.simpleMessage("æ–‡ä»¶ç±»ćž‹ć’Œćç§°"), "searchHint1": MessageLookupByLibrary.simpleMessage("ćœšèźŸć€‡äžŠćż«é€ŸæœçŽą"), diff --git a/mobile/lib/generated/l10n.dart b/mobile/lib/generated/l10n.dart index de8922161d..3bc5f8b9a7 100644 --- a/mobile/lib/generated/l10n.dart +++ b/mobile/lib/generated/l10n.dart @@ -2876,11 +2876,11 @@ class S { ); } - /// `Please note that this will result in a higher bandwidth and battery usage until all items are indexed.` - String get magicSearchDescription { + /// `Please note that machine learning will result in a higher bandwidth and battery usage until all items are indexed.` + String get mlIndexingDescription { return Intl.message( - 'Please note that this will result in a higher bandwidth and battery usage until all items are indexed.', - name: 'magicSearchDescription', + 'Please note that machine learning will result in a higher bandwidth and battery usage until all items are indexed.', + name: 'mlIndexingDescription', desc: '', args: [], ); @@ -4735,11 +4735,11 @@ class S { ); } - /// `The developer account we use to publish Ente on App Store has changed. Because of this, you will need to login again.\n\nOur apologies for the inconvenience, but this was unavoidable.` - String get devAccountChanged { + /// `Due to technical glitch, you have been logged out. Our apologies for the inconvenience.` + String get autoLogoutMessage { return Intl.message( - 'The developer account we use to publish Ente on App Store has changed. Because of this, you will need to login again.\n\nOur apologies for the inconvenience, but this was unavoidable.', - name: 'devAccountChanged', + 'Due to technical glitch, you have been logged out. Our apologies for the inconvenience.', + name: 'autoLogoutMessage', desc: '', args: [], ); @@ -6969,10 +6969,10 @@ class S { ); } - /// `Persons will be shown here once indexing is done` + /// `People will be shown here once indexing is done` String get searchFaceEmptySection { return Intl.message( - 'Persons will be shown here once indexing is done', + 'People will be shown here once indexing is done', name: 'searchFaceEmptySection', desc: '', args: [], @@ -7402,10 +7402,10 @@ class S { } /// `{freeAmount} {storageUnit} free` - String freeStorageSpace(Object freeAmount, Object storageUnit) { + String availableStorageSpace(Object freeAmount, Object storageUnit) { return Intl.message( '$freeAmount $storageUnit free', - name: 'freeStorageSpace', + name: 'availableStorageSpace', desc: '', args: [freeAmount, storageUnit], ); @@ -8764,16 +8764,6 @@ class S { ); } - /// `Please note that this will result in a higher bandwidth and battery usage until all items are indexed.` - String get faceRecognitionIndexingDescription { - return Intl.message( - 'Please note that this will result in a higher bandwidth and battery usage until all items are indexed.', - name: 'faceRecognitionIndexingDescription', - desc: '', - args: [], - ); - } - /// `Found faces` String get foundFaces { return Intl.message( @@ -8794,10 +8784,10 @@ class S { ); } - /// `Indexing is paused, will automatically resume when device is ready` + /// `Indexing is paused. It will automatically resume when device is ready.` String get indexingIsPaused { return Intl.message( - 'Indexing is paused, will automatically resume when device is ready', + 'Indexing is paused. It will automatically resume when device is ready.', name: 'indexingIsPaused', desc: '', args: [], diff --git a/mobile/lib/l10n/intl_de.arb b/mobile/lib/l10n/intl_de.arb index 913af46b6c..ca19df932a 100644 --- a/mobile/lib/l10n/intl_de.arb +++ b/mobile/lib/l10n/intl_de.arb @@ -23,7 +23,7 @@ "sendEmail": "E-Mail senden", "deleteRequestSLAText": "Deine Anfrage wird innerhalb von 72 Stunden bearbeitet.", "deleteEmailRequest": "Bitte sende eine E-Mail an account-deletion@ente.io von Deiner bei uns hinterlegten E-Mail-Adresse.", - "entePhotosPerm": "ente benötigt die Erlaubnis, deine Fotos aufzubewahren", + "entePhotosPerm": "", "ok": "Ok", "createAccount": "Konto erstellen", "createNewAccount": "Neues Konto erstellen", @@ -48,7 +48,7 @@ "sorry": "Entschuldigung", "noRecoveryKeyNoDecryption": "Aufgrund unseres Ende-zu-Ende-VerschlĂŒsselungsprotokolls können deine Daten nicht ohne dein Passwort oder deinen Wiederherstellungs-SchlĂŒssel entschlĂŒsselt werden", "verifyEmail": "E-Mail-Adresse verifizieren", - "toResetVerifyEmail": "Um Ihr Passwort zurĂŒckzusetzen, verifizieren Sie bitte zuerst Ihre E-Mail Adresse.", + "toResetVerifyEmail": "Um dein Passwort zurĂŒckzusetzen, verifiziere bitte zuerst deine E-Mail Adresse.", "checkInboxAndSpamFolder": "Bitte ĂŒberprĂŒfe deinen E-Mail-Posteingang (und Spam), um die Verifizierung abzuschließen", "tapToEnterCode": "Antippen, um den Code einzugeben", "resendEmail": "E-Mail erneut senden", @@ -143,7 +143,7 @@ "lostDevice": "GerĂ€t verloren?", "verifyingRecoveryKey": "Wiederherstellungs-SchlĂŒssel wird ĂŒberprĂŒft...", "recoveryKeyVerified": "Wiederherstellungs-SchlĂŒssel ĂŒberprĂŒft", - "recoveryKeySuccessBody": "Sehr gut! Ihr WiederherstellungsschlĂŒssel ist gĂŒltig. Vielen Dank fĂŒr die Verifizierung.\n\nBitte vergessen Sie nicht eine Kopie Ihres WiederherstellungsschlĂŒssels sicher aufzubewahren.", + "recoveryKeySuccessBody": "Sehr gut! Dein WiederherstellungsschlĂŒssel ist gĂŒltig. Vielen Dank fĂŒr die Verifizierung.\n\nBitte vergiss nicht eine Kopie des WiederherstellungsschlĂŒssels sicher aufzubewahren.", "invalidRecoveryKey": "Der von Ihnen eingegebene WiederherstellungsschlĂŒssel ist nicht gĂŒltig. Bitte stellen Sie sicher das aus 24 Wörtern zusammen gesetzt ist und jedes dieser Worte richtig geschrieben wurde.\n\nSollten Sie den Wiederherstellungscode eingegeben haben, stellen Sie bitte sicher, dass dieser 64 Worte lang ist und ebenfall richtig geschrieben wurde.", "invalidKey": "UngĂŒltiger SchlĂŒssel", "tryAgain": "Erneut versuchen", @@ -225,7 +225,7 @@ }, "description": "Number of participants in an album, including the album owner." }, - "collabLinkSectionDescription": "Erstelle einen Link, um anderen zu ermöglichen, Fotos in deinem geteilten Album hinzuzufĂŒgen und zu sehen - ohne dass diese ein Konto von ente.io oder die App benötigen. Ideal, um Fotos von Events zu sammeln.", + "collabLinkSectionDescription": "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.", "collectPhotos": "Fotos sammeln", "collaborativeLink": "Gemeinschaftlicher Link", "shareWithNonenteUsers": "Mit Nicht-Ente-Benutzern teilen", @@ -235,7 +235,7 @@ "linkHasExpired": "Link ist abgelaufen", "publicLinkEnabled": "Öffentlicher Link aktiviert", "shareALink": "Einen Link teilen", - "sharedAlbumSectionDescription": "Erstelle gemeinsame Alben mit anderen ente Benutzern, einschließlich solchen im kostenlosen Tarif.", + "sharedAlbumSectionDescription": "Erstelle gemeinsam mit anderen Ente-Nutzern geteilte Alben, inkl. Nutzern ohne Bezahltarif.", "shareWithPeopleSectionTitle": "{numberOfPeople, plural, =0 {Teile mit bestimmten Personen} =1 {Teilen mit 1 Person} other {Teilen mit {numberOfPeople} Personen}}", "@shareWithPeopleSectionTitle": { "placeholders": { @@ -259,12 +259,12 @@ }, "verificationId": "Verifizierungs-ID", "verifyEmailID": "Verifiziere {email}", - "emailNoEnteAccount": "{email} hat kein Ente-Konto.\n\nSenden Sie eine Einladung, um Fotos zu teilen.", + "emailNoEnteAccount": "{email} hat kein Ente-Konto.\n\nSende eine Einladung, um Fotos zu teilen.", "shareMyVerificationID": "Hier ist meine Verifizierungs-ID: {verificationID} fĂŒr ente.io.", "shareTextConfirmOthersVerificationID": "Hey, kannst du bestĂ€tigen, dass dies deine ente.io Verifizierungs-ID ist: {verificationID}", "somethingWentWrong": "Irgendetwas ging schief", "sendInvite": "Einladung senden", - "shareTextRecommendUsingEnte": "Lade ente herunter, damit wir einfach Fotos und Videos in höchster QualitĂ€t teilen können\n\nhttps://ente.io", + "shareTextRecommendUsingEnte": "Hol dir Ente, damit wir ganz einfach Fotos und Videos in OriginalqualitĂ€t teilen können\n\nhttps://ente.io", "done": "Fertig", "applyCodeTitle": "Code nutzen", "enterCodeDescription": "Gib den Code deines Freundes ein, damit sie beide kostenlosen Speicherplatz erhalten", @@ -281,7 +281,7 @@ "claimMore": "Mehr einlösen!", "theyAlsoGetXGb": "Diese erhalten auch {storageAmountInGB} GB", "freeStorageOnReferralSuccess": "{storageAmountInGB} GB jedes Mal, wenn sich jemand mit deinem Code fĂŒr einen bezahlten Tarif anmeldet", - "shareTextReferralCode": "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", + "shareTextReferralCode": "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", "claimFreeStorage": "Freien Speicher einlösen", "inviteYourFriends": "Lade deine Freunde ein", "failedToFetchReferralDetails": "Die Weiterempfehlungs-Details können nicht abgerufen werden. Bitte versuche es spĂ€ter erneut.", @@ -304,6 +304,7 @@ } }, "faq": "HĂ€ufig gestellte Fragen", + "help": "Hilfe", "oopsSomethingWentWrong": "Ups. Leider ist ein Fehler aufgetreten", "peopleUsingYourCode": "Leute, die deinen Code verwenden", "eligible": "zulĂ€ssig", @@ -333,7 +334,7 @@ "removeParticipantBody": "{userEmail} wird aus diesem geteilten Album entfernt\n\nAlle von ihnen hinzugefĂŒgte Fotos werden ebenfalls aus dem Album entfernt", "keepPhotos": "Fotos behalten", "deletePhotos": "Fotos löschen", - "inviteToEnte": "Zu ente einladen", + "inviteToEnte": "Zu Ente einladen", "removePublicLink": "Öffentlichen Link entfernen", "disableLinkMessage": "Der öffentliche Link zum Zugriff auf \"{albumName}\" wird entfernt.", "sharing": "Teilt...", @@ -349,10 +350,10 @@ "videoSmallCase": "Video", "photoSmallCase": "Foto", "singleFileDeleteHighlight": "Es wird aus allen Alben gelöscht.", - "singleFileInBothLocalAndRemote": "Dieses {fileType} existiert auf ente.io und deinem GerĂ€t.", - "singleFileInRemoteOnly": "Dieses {fileType} wird auf ente.io gelöscht.", + "singleFileInBothLocalAndRemote": "Diese Datei ist sowohl in Ente als auch auf deinem GerĂ€t.", + "singleFileInRemoteOnly": "Diese Datei wird von Ente gelöscht.", "singleFileDeleteFromDevice": "Dieses {fileType} wird von deinem GerĂ€t gelöscht.", - "deleteFromEnte": "Auf ente.io löschen", + "deleteFromEnte": "Von Ente löschen", "yesDelete": "Ja, löschen", "movedToTrash": "In den Papierkorb verschoben", "deleteFromDevice": "Vom GerĂ€t löschen", @@ -408,7 +409,7 @@ "manageDeviceStorage": "GerĂ€tespeicher verwalten", "machineLearning": "Maschinelles Lernen", "magicSearch": "Magische Suche", - "magicSearchDescription": "Bitte beachten Sie, dass dies mehr Bandbreite nutzt und zu einem höheren Akkuverbrauch fĂŒhrt, bis alle Elemente indiziert sind.", + "mlIndexingDescription": "Bitte beachten Sie, dass Machine Learning zu einem höheren Bandbreiten- und Batterieverbrauch fĂŒhrt, bis alle Elemente indiziert sind.", "loadingModel": "Lade Modelle herunter...", "waitingForWifi": "Warte auf WLAN...", "status": "Status", @@ -444,7 +445,7 @@ "backupOverMobileData": "Über mobile Daten sichern", "backupVideos": "Videos sichern", "disableAutoLock": "Automatische Sperre deaktivieren", - "deviceLockExplanation": "Das Sperren des GerĂ€tes verhindern, solange 'ente' im Vordergrund geöffnet ist und eine Sicherung lĂ€uft. \nDies wird fĂŒr gewöhnlich nicht benötigt, kann aber dabei helfen große Transfers schneller durchzufĂŒhren.", + "deviceLockExplanation": "Verhindern, dass der Bildschirm gesperrt wird, wĂ€hrend die App im Vordergrund ist und eine Sicherung lĂ€uft. Das ist normalerweise nicht notwendig, kann aber dabei helfen, große Uploads wie einen Erstimport schneller abzuschließen.", "about": "Allgemeine Informationen", "weAreOpenSource": "Unser Quellcode ist offen einsehbar!", "privacy": "Datenschutz", @@ -464,7 +465,7 @@ "authToInitiateAccountDeletion": "Bitte authentifizieren, um die Löschung des Kontos einzuleiten", "areYouSureYouWantToLogout": "Sind sie sicher, dass Sie sich abmelden wollen?", "yesLogout": "Ja, ausloggen", - "aNewVersionOfEnteIsAvailable": "Eine neuere Version von 'ente' ist verfĂŒgbar.", + "aNewVersionOfEnteIsAvailable": "Eine neue Version von Ente ist verfĂŒgbar.", "update": "Updaten", "installManually": "Manuell installieren", "criticalUpdateAvailable": "Kritisches Update ist verfĂŒgbar!", @@ -553,11 +554,11 @@ "systemTheme": "System", "freeTrial": "Kostenlose Testphase", "selectYourPlan": "WĂ€hle dein Abo aus", - "enteSubscriptionPitch": "ente sichert deine ErinnerungsstĂŒcke, sodass sie immer fĂŒr dich verfĂŒgbar sind, auch wenn du dein GerĂ€t verlieren solltest.", + "enteSubscriptionPitch": "Ente sichert deine Erinnerungen, sodass sie dir nie verloren gehen, selbst wenn du dein GerĂ€t verlierst.", "enteSubscriptionShareWithFamily": "Deine Familie kann zu deinem Abo hinzugefĂŒgt werden.", "currentUsageIs": "Aktuell genutzt werden ", "@currentUsageIs": { - "description": "This text is followed by storage usaged", + "description": "This text is followed by storage usage", "examples": { "0": "Current usage is 1.2 GB" }, @@ -619,7 +620,7 @@ "appleId": "Apple ID", "playstoreSubscription": "PlayStore Abo", "appstoreSubscription": "AppStore Abo", - "subAlreadyLinkedErrMessage": "Ihr {id} ist bereits mit einem anderen 'ente'-Konto verknĂŒpft.\nWenn Sie Ihre {id} mit diesem Konto verwenden möchten, kontaktieren Sie bitte unseren Support'", + "subAlreadyLinkedErrMessage": "Ihr {id} ist bereits mit einem anderen Ente-Konto verknĂŒpft.\nWenn Sie Ihre {id} mit diesem Konto verwenden möchten, kontaktieren Sie bitte unseren Support", "visitWebToManage": "Bitte rufen Sie \"web.ente.io\" auf um ihr Abo zu verwalten", "couldNotUpdateSubscription": "Abo konnte nicht aktualisiert werden", "pleaseContactSupportAndWeWillBeHappyToHelp": "Bitte kontaktieren Sie uns ĂŒber support@ente.io wo wir Ihnen gerne weiterhelfen.", @@ -640,7 +641,7 @@ "thankYou": "Vielen Dank", "failedToVerifyPaymentStatus": "ÜberprĂŒfung des Zahlungsstatus fehlgeschlagen", "pleaseWaitForSometimeBeforeRetrying": "Bitte warte kurz, bevor du es erneut versuchst", - "paymentFailedWithReason": "Leider ist deine Zahlung aus folgendem Grund fehlgeschlagen: {reason}", + "paymentFailedMessage": "Leider ist deine Zahlung fehlgeschlagen. Wende dich an unseren Support und wir helfen dir weiter!", "youAreOnAFamilyPlan": "Du bist im Familien-Tarif!", "contactFamilyAdmin": "Bitte kontaktiere {familyAdminEmail} um dein Abo zu verwalten", "leaveFamily": "Familienabo verlassen", @@ -664,9 +665,9 @@ "everywhere": "ĂŒberall", "androidIosWebDesktop": "Android, iOS, Web, Desktop", "mobileWebDesktop": "Mobil, Web, Desktop", - "newToEnte": "Neu bei ente", + "newToEnte": "Neu bei Ente", "pleaseLoginAgain": "Bitte logge dich erneut ein", - "devAccountChanged": "Das Entwicklerkonto, das wir verwenden, um ente im App Store zu veröffentlichen, hat sich geĂ€ndert. Aus diesem Grund musst du dich erneut anmelden.\n\nWir entschuldigen uns fĂŒr die Unannehmlichkeiten, aber das war unvermeidlich.", + "autoLogoutMessage": "Aufgrund technischer Störungen wurden Sie abgemeldet. Wir entschuldigen uns fĂŒr die Unannehmlichkeiten.", "yourSubscriptionHasExpired": "Dein Abonnement ist abgelaufen", "storageLimitExceeded": "Speichergrenze ĂŒberschritten", "upgrade": "Upgrade", @@ -677,12 +678,12 @@ }, "backupFailed": "Sicherung fehlgeschlagen", "couldNotBackUpTryLater": "Deine Daten konnten nicht gesichert werden.\nWir versuchen es spĂ€ter erneut.", - "enteCanEncryptAndPreserveFilesOnlyIfYouGrant": "ente kann Dateien nur verschlĂŒsselt sichern, wenn du uns darauf Zugriff gewĂ€hrst", + "enteCanEncryptAndPreserveFilesOnlyIfYouGrant": "Ente kann Dateien nur verschlĂŒsseln und sichern, wenn du den Zugriff darauf gewĂ€hrst", "pleaseGrantPermissions": "Bitte erteile die nötigen Berechtigungen", "grantPermission": "Zugriff gewĂ€hren", "privateSharing": "Privates Teilen", "shareOnlyWithThePeopleYouWant": "Teile mit ausgewĂ€hlten Personen", - "usePublicLinksForPeopleNotOnEnte": "Nutze öffentliche Links fĂŒr Personen ohne ente.io Konto", + "usePublicLinksForPeopleNotOnEnte": "Verwenden Sie öffentliche Links fĂŒr Personen, die kein Ente-Konto haben", "allowPeopleToAddPhotos": "Erlaube anderen das HinzufĂŒgen von Fotos", "shareAnAlbumNow": "Teile jetzt ein Album", "collectEventPhotos": "Gemeinsam Event-Fotos sammeln", @@ -694,7 +695,7 @@ }, "onDevice": "Auf dem GerĂ€t", "@onEnte": { - "description": "The text displayed above albums backed up to ente", + "description": "The text displayed above albums backed up to Ente", "type": "text" }, "onEnte": "Auf ente", @@ -740,7 +741,7 @@ "saveCollage": "Collage speichern", "collageSaved": "Collage in Galerie gespeichert", "collageLayout": "Layout", - "addToEnte": "Zu ente hinzufĂŒgen", + "addToEnte": "Zu Ente hinzufĂŒgen", "addToAlbum": "Zum Album hinzufĂŒgen", "delete": "Löschen", "hide": "Ausblenden", @@ -805,10 +806,10 @@ "photosAddedByYouWillBeRemovedFromTheAlbum": "Von dir hinzugefĂŒgte Fotos werden vom Album entfernt", "youveNoFilesInThisAlbumThatCanBeDeleted": "Du hast keine Dateien in diesem Album, die gelöscht werden können", "youDontHaveAnyArchivedItems": "Du hast keine archivierten Elemente.", - "ignoredFolderUploadReason": "Einige Dateien in diesem Album werden beim Upload ignoriert, weil sie zuvor auf ente gelöscht wurden.", + "ignoredFolderUploadReason": "Ein paar Dateien in diesem Album werden nicht hochgeladen, weil sie in der Vergangenheit schonmal aus Ente gelöscht wurden.", "resetIgnoredFiles": "Ignorierte Dateien zurĂŒcksetzen", - "deviceFilesAutoUploading": "Dateien, die zu diesem Album hinzugefĂŒgt werden, werden automatisch zu ente hochgeladen.", - "turnOnBackupForAutoUpload": "Aktiviere die Sicherung, um automatisch neu hinzugefĂŒgte Dateien dieses Ordners auf ente hochzuladen.", + "deviceFilesAutoUploading": "Dateien, die zu diesem Album hinzugefĂŒgt werden, werden automatisch zu Ente hochgeladen.", + "turnOnBackupForAutoUpload": "Aktiviere die Sicherung, um neue Dateien in diesem Ordner automatisch zu Ente hochzuladen.", "noHiddenPhotosOrVideos": "Keine versteckten Fotos oder Videos", "toHideAPhotoOrVideo": "Foto oder Video verstecken", "openTheItem": "‱ Element öffnen", @@ -834,6 +835,7 @@ "close": "Schließen", "setAs": "Festlegen als", "fileSavedToGallery": "Datei in Galerie gespeichert", + "filesSavedToGallery": "Dateien in Galerie gespeichert", "fileFailedToSaveToGallery": "Fehler beim Speichern der Datei in der Galerie", "download": "Herunterladen", "pressAndHoldToPlayVideo": "GedrĂŒckt halten, um Video abzuspielen", @@ -885,7 +887,7 @@ "@freeUpSpaceSaving": { "description": "Text to tell user how much space they can free up by deleting items from the device" }, - "freeUpAccessPostDelete": "Sie können immer noch {count, plural, one {darauf} other {auf sie}} auf ente zugreifen, solange Sie ein aktives Abonnement haben", + "freeUpAccessPostDelete": "Du kannst immernoch ĂŒber Ente {count, plural, one {darauf} other {auf sie}} zugreifen, solange du ein aktives Abo hast", "@freeUpAccessPostDelete": { "placeholders": { "count": { @@ -936,7 +938,7 @@ "renameFile": "Datei umbenennen", "enterFileName": "Dateinamen eingeben", "filesDeleted": "Dateien gelöscht", - "selectedFilesAreNotOnEnte": "AusgewĂ€hlte Dateien sind nicht auf ente", + "selectedFilesAreNotOnEnte": "AusgewĂ€hlte Dateien sind nicht auf Ente", "thisActionCannotBeUndone": "Diese Aktion kann nicht rĂŒckgĂ€ngig gemacht werden", "emptyTrash": "Papierkorb leeren?", "permDeleteWarning": "Alle Elemente im Papierkorb werden dauerhaft gelöscht\n\nDiese Aktion kann nicht rĂŒckgĂ€ngig gemacht werden", @@ -945,7 +947,7 @@ "permanentlyDeleteFromDevice": "EndgĂŒltig vom GerĂ€t löschen?", "someOfTheFilesYouAreTryingToDeleteAre": "Einige der Dateien, die Sie löschen möchten, sind nur auf Ihrem GerĂ€t verfĂŒgbar und können nicht wiederhergestellt werden, wenn sie gelöscht wurden", "theyWillBeDeletedFromAllAlbums": "Sie werden aus allen Alben gelöscht.", - "someItemsAreInBothEnteAndYourDevice": "Einige Elemente sind sowohl auf ente als auch auf Ihrem GerĂ€t.", + "someItemsAreInBothEnteAndYourDevice": "Einige Elemente sind sowohl auf Ente als auch auf deinem GerĂ€t.", "selectedItemsWillBeDeletedFromAllAlbumsAndMoved": "AusgewĂ€hlte Elemente werden aus allen Alben gelöscht und in den Papierkorb verschoben.", "theseItemsWillBeDeletedFromYourDevice": "Diese Elemente werden von deinem GerĂ€t gelöscht.", "itLooksLikeSomethingWentWrongPleaseRetryAfterSome": "Etwas ist schiefgelaufen. Bitte versuche es spĂ€ter noch einmal. Sollte der Fehler weiter bestehen, kontaktiere unser Supportteam.", @@ -985,7 +987,7 @@ "fileTypesAndNames": "Dateitypen und -namen", "location": "Standort", "moments": "Momente", - "searchFaceEmptySection": "Finde alle Foto von einer Person", + "searchFaceEmptySection": "Personen werden hier angezeigt, sobald die Indizierung abgeschlossen ist", "searchDatesEmptySection": "Suche nach Datum, Monat oder Jahr", "searchLocationEmptySection": "Gruppiere Fotos, die innerhalb des Radius eines bestimmten Fotos aufgenommen wurden", "searchPeopleEmptySection": "Laden Sie Personen ein, damit Sie geteilte Fotos hier einsehen können", @@ -1040,7 +1042,7 @@ "@storageUsageInfo": { "description": "Example: 1.2 GB of 2 GB used or 100 GB or 2TB used" }, - "freeStorageSpace": "{freeAmount} {storageUnit} kostenlos", + "availableStorageSpace": "{freeAmount} {storageUnit} frei", "appVersion": "Version: {versionValue}", "verifyIDLabel": "ÜberprĂŒfen", "fileInfoAddDescHint": "Beschreibung hinzufĂŒgen 
", @@ -1051,7 +1053,7 @@ }, "setRadius": "Radius festlegen", "familyPlanPortalTitle": "Familie", - "familyPlanOverview": "FĂŒgen Sie 5 Familienmitglieder zu Ihrem bestehenden Abo hinzu, ohne extra zu bezahlen.\n\nJedes Mitglied erhĂ€lt einen eigenen privaten Raum und kann die Dateien von anderen nicht sehen, wenn sie nicht freigegeben werden.\n\nFamilien-Abos sind fĂŒr Kunden verfĂŒgbar, die ein kostenpflichtiges ente Abonnement haben.\n\nMelden Sie sich jetzt an, um loszulegen!", + "familyPlanOverview": "FĂŒge kostenlos 5 Familienmitglieder zu deinem bestehenden Abo hinzu.\n\nJedes Mitglied bekommt seinen eigenen privaten Bereich und kann die Dateien der anderen nur sehen, wenn sie geteilt werden.\n\nFamilien-Abos stehen Nutzern mit einem Bezahltarif zur VerfĂŒgung.\n\nMelde dich jetzt an, um loszulegen!", "androidBiometricHint": "IdentitĂ€t verifizieren", "@androidBiometricHint": { "description": "Hint message advising the user how to authenticate with biometrics. It is used on Android side. Maximum 60 characters." @@ -1129,7 +1131,7 @@ "noAlbumsSharedByYouYet": "Noch keine Alben von dir geteilt", "sharedWithYou": "Mit dir geteilt", "sharedByYou": "Von dir geteilt", - "inviteYourFriendsToEnte": "Lade deine Freunde zu ente ein", + "inviteYourFriendsToEnte": "Lade deine Freunde zu Ente ein", "failedToDownloadVideo": "Herunterladen des Videos fehlgeschlagen", "hiding": "Verstecken...", "unhiding": "Einblenden...", @@ -1139,7 +1141,7 @@ "addToHiddenAlbum": "Zum versteckten Album hinzufĂŒgen", "moveToHiddenAlbum": "Zu verstecktem Album verschieben", "fileTypes": "Dateitypen", - "deleteConfirmDialogBody": "Dieses Konto ist mit anderen ente Apps verknĂŒpft, sofern du diese benutzt.\\n\\nDeine hochgeladenen Daten werden zur permanenten Löschung freigegeben. Dies gilt fĂŒr alle ente Apps.", + "deleteConfirmDialogBody": "Dieses Konto ist mit anderen Ente-Apps verknĂŒpft, falls du welche verwendest. Deine hochgeladenen Daten werden in allen Ente-Apps zur Löschung vorgemerkt und dein Konto wird endgĂŒltig gelöscht.", "hearUsWhereTitle": "Wie hast du von Ente erfahren? (optional)", "hearUsExplanation": "Wir tracken keine App-Installationen. Es wĂŒrde uns jedoch helfen, wenn du uns mitteilst, wie du von uns erfahren hast!", "viewAddOnButton": "Zeige Add-ons", @@ -1169,6 +1171,7 @@ } }, "faces": "Gesichter", + "people": "Personen", "contents": "Inhalte", "addNew": "HinzufĂŒgen", "@addNew": { @@ -1185,10 +1188,8 @@ "selectALocation": "Standort auswĂ€hlen", "selectALocationFirst": "WĂ€hle zuerst einen Standort", "changeLocationOfSelectedItems": "Standort der gewĂ€hlten Elemente Ă€ndern?", - "editsToLocationWillOnlyBeSeenWithinEnte": "Änderungen des Standorts werden nur in ente sichtbar sein", + "editsToLocationWillOnlyBeSeenWithinEnte": "Edits to location will only be seen within Ente", "cleanUncategorized": "Unkategorisiert leeren", - "addAName": "Add a name", - "findPeopleByName": "Find people quickly by searching by name", "cleanUncategorizedDescription": "Entferne alle Dateien von \"Unkategorisiert\" die in anderen Alben vorhanden sind", "waitingForVerification": "Warte auf BestĂ€tigung...", "passkey": "Passkey", @@ -1202,16 +1203,37 @@ "joinDiscord": "Discord beitreten", "locations": "Orte", "descriptions": "Beschreibungen", - "addViewers": "{count, plural, zero {Add viewer} one {Add viewer} other {Add viewers}}", - "addCollaborators": "{count, plural, zero {Add collaborator} one {Add collaborator} other {Add collaborators}}", - "longPressAnEmailToVerifyEndToEndEncryption": "Long press an email to verify end to end encryption.", - "createCollaborativeLink": "Create collaborative link", - "search": "Search", - "enterPersonName": "Enter person name", - "removePersonLabel": "Remove person label", - "faceRecognition": "Face recognition", - "faceRecognitionIndexingDescription": "Please note that this will result in a higher bandwidth and battery usage until all items are indexed.", - "foundFaces": "Found faces", - "clusteringProgress": "Clustering progress", - "indexingIsPaused": "Indexing is paused, will automatically resume when device is ready" + "addAName": "FĂŒge einen Namen hinzu", + "findPeopleByName": "Finde Personen schnell nach Namen", + "addViewers": "{count, plural, one {Betrachter} other {Betrachter}} hinzufĂŒgen", + "addCollaborators": "{count, plural, one {Teilnehmer} other {Teilnehmer}} hinzufĂŒgen", + "longPressAnEmailToVerifyEndToEndEncryption": "Lange auf eine E-Mail drĂŒcken, um die Ende-zu-Ende-VerschlĂŒsselung zu ĂŒberprĂŒfen.", + "developerSettingsWarning": "Bist du sicher, dass du Entwicklereinstellungen bearbeiten willst?", + "developerSettings": "Entwicklereinstellungen", + "serverEndpoint": "Server Endpunkt", + "invalidEndpoint": "UngĂŒltiger Endpunkt", + "invalidEndpointMessage": "Der eingegebene Endpunkt ist ungĂŒltig. Gib einen gĂŒltigen Endpunkt ein und versuch es nochmal.", + "endpointUpdatedMessage": "Endpunkt erfolgreich geĂ€ndert", + "customEndpoint": "Verbunden mit {endpoint}", + "createCollaborativeLink": "Gemeinschaftlichen Link erstellen", + "search": "Suche", + "enterPersonName": "Namen der Person eingeben", + "removePersonLabel": "Personenetikett entfernen", + "autoPairDesc": "Automatisches Verbinden funktioniert nur mit GerĂ€ten, die Chromecast unterstĂŒtzen.", + "manualPairDesc": "\"Mit PIN verbinden\" funktioniert mit jedem Bildschirm, auf dem du dein Album sehen möchtest.", + "connectToDevice": "Mit GerĂ€t verbinden", + "autoCastDialogBody": "VerfĂŒgbare Cast-GerĂ€te werden hier angezeigt.", + "autoCastiOSPermission": "Stelle sicher, dass die Ente-App auf das lokale Netzwerk zugreifen darf. Das kannst du in den Einstellungen unter \"Datenschutz\".", + "noDeviceFound": "Kein GerĂ€t gefunden", + "stopCastingTitle": "Übertragung beenden", + "stopCastingBody": "Möchtest du die Übertragung beenden?", + "castIPMismatchTitle": "Album konnte nicht auf den Bildschirm ĂŒbertragen werden", + "castIPMismatchBody": "Stelle sicher, dass du im selben Netzwerk bist wie der Fernseher.", + "pairingComplete": "Verbunden", + "autoPair": "Automatisch verbinden", + "pairWithPin": "Mit PIN verbinden", + "faceRecognition": "Gesichtserkennung", + "foundFaces": "Gesichter gefunden", + "clusteringProgress": "Fortschritt beim Clustering", + "indexingIsPaused": "Die Indizierung ist unterbrochen. Sie wird automatisch fortgesetzt, wenn das GerĂ€t bereit ist." } \ No newline at end of file diff --git a/mobile/lib/l10n/intl_en.arb b/mobile/lib/l10n/intl_en.arb index 08e794074b..43fa5d341e 100644 --- a/mobile/lib/l10n/intl_en.arb +++ b/mobile/lib/l10n/intl_en.arb @@ -409,7 +409,7 @@ "manageDeviceStorage": "Manage device storage", "machineLearning": "Machine learning", "magicSearch": "Magic search", - "magicSearchDescription": "Please note that this will result in a higher bandwidth and battery usage until all items are indexed.", + "mlIndexingDescription": "Please note that machine learning will result in a higher bandwidth and battery usage until all items are indexed.", "loadingModel": "Downloading models...", "waitingForWifi": "Waiting for WiFi...", "status": "Status", @@ -667,7 +667,7 @@ "mobileWebDesktop": "Mobile, Web, Desktop", "newToEnte": "New to Ente", "pleaseLoginAgain": "Please login again", - "devAccountChanged": "The developer account we use to publish Ente on App Store has changed. Because of this, you will need to login again.\n\nOur apologies for the inconvenience, but this was unavoidable.", + "autoLogoutMessage": "Due to technical glitch, you have been logged out. Our apologies for the inconvenience.", "yourSubscriptionHasExpired": "Your subscription has expired", "storageLimitExceeded": "Storage limit exceeded", "upgrade": "Upgrade", @@ -987,7 +987,7 @@ "fileTypesAndNames": "File types and names", "location": "Location", "moments": "Moments", - "searchFaceEmptySection": "Persons will be shown here once indexing is done", + "searchFaceEmptySection": "People will be shown here once indexing is done", "searchDatesEmptySection": "Search by a date, month or year", "searchLocationEmptySection": "Group photos that are taken within some radius of a photo", "searchPeopleEmptySection": "Invite people, and you'll see all photos shared by them here", @@ -1042,7 +1042,7 @@ "@storageUsageInfo": { "description": "Example: 1.2 GB of 2 GB used or 100 GB or 2TB used" }, - "freeStorageSpace": "{freeAmount} {storageUnit} free", + "availableStorageSpace": "{freeAmount} {storageUnit} free", "appVersion": "Version: {versionValue}", "verifyIDLabel": "Verify", "fileInfoAddDescHint": "Add a description...", @@ -1233,8 +1233,7 @@ "autoPair": "Auto pair", "pairWithPin": "Pair with PIN", "faceRecognition": "Face recognition", - "faceRecognitionIndexingDescription": "Please note that this will result in a higher bandwidth and battery usage until all items are indexed.", "foundFaces": "Found faces", "clusteringProgress": "Clustering progress", - "indexingIsPaused": "Indexing is paused, will automatically resume when device is ready" -} \ No newline at end of file + "indexingIsPaused": "Indexing is paused. It will automatically resume when device is ready." +} diff --git a/mobile/lib/l10n/intl_es.arb b/mobile/lib/l10n/intl_es.arb index 22acb2b333..a9ee0b4d9c 100644 --- a/mobile/lib/l10n/intl_es.arb +++ b/mobile/lib/l10n/intl_es.arb @@ -949,7 +949,6 @@ "@storageUsageInfo": { "description": "Example: 1.2 GB of 2 GB used or 100 GB or 2TB used" }, - "freeStorageSpace": "{freeAmount} {storageUnit} gratis", "appVersion": "VersiĂłn: {versionValue}", "verifyIDLabel": "Verificar", "fileInfoAddDescHint": "Añadir una descripciĂłn...", diff --git a/mobile/lib/l10n/intl_fr.arb b/mobile/lib/l10n/intl_fr.arb index 90c0ad80e6..44206d6347 100644 --- a/mobile/lib/l10n/intl_fr.arb +++ b/mobile/lib/l10n/intl_fr.arb @@ -1014,7 +1014,7 @@ "@storageUsageInfo": { "description": "Example: 1.2 GB of 2 GB used or 100 GB or 2TB used" }, - "freeStorageSpace": "{freeAmount} {storageUnit} libre", + "availableStorageSpace": "{freeAmount} {storageUnit} libre", "appVersion": "Version : {versionValue}", "verifyIDLabel": "VĂ©rifier", "fileInfoAddDescHint": "Ajouter une description...", diff --git a/mobile/lib/l10n/intl_it.arb b/mobile/lib/l10n/intl_it.arb index 071933ae5b..ddcaa3aac0 100644 --- a/mobile/lib/l10n/intl_it.arb +++ b/mobile/lib/l10n/intl_it.arb @@ -1004,7 +1004,7 @@ "@storageUsageInfo": { "description": "Example: 1.2 GB of 2 GB used or 100 GB or 2TB used" }, - "freeStorageSpace": "{freeAmount} {storageUnit} liberi", + "availableStorageSpace": "{freeAmount} {storageUnit} liberi", "appVersion": "Versione: {versionValue}", "verifyIDLabel": "Verifica", "fileInfoAddDescHint": "Aggiungi descrizione...", diff --git a/mobile/lib/l10n/intl_nl.arb b/mobile/lib/l10n/intl_nl.arb index f54f6b6043..968345316d 100644 --- a/mobile/lib/l10n/intl_nl.arb +++ b/mobile/lib/l10n/intl_nl.arb @@ -1042,7 +1042,7 @@ "@storageUsageInfo": { "description": "Example: 1.2 GB of 2 GB used or 100 GB or 2TB used" }, - "freeStorageSpace": "{freeAmount} {storageUnit} vrij", + "availableStorageSpace": "{freeAmount} {storageUnit} vrij", "appVersion": "Versie: {versionValue}", "verifyIDLabel": "VerifiĂ«ren", "fileInfoAddDescHint": "Voeg een beschrijving toe...", diff --git a/mobile/lib/l10n/intl_pt.arb b/mobile/lib/l10n/intl_pt.arb index 428dbf5fc2..5ac0904c99 100644 --- a/mobile/lib/l10n/intl_pt.arb +++ b/mobile/lib/l10n/intl_pt.arb @@ -24,13 +24,13 @@ "deleteRequestSLAText": "Sua solicitação serĂĄ processada em atĂ© 72 horas.", "deleteEmailRequest": "Por favor, envie um e-mail para account-deletion@ente.io a partir do seu endereço de e-mail registrado.", "entePhotosPerm": "Ente precisa de permissĂŁo para preservar suas fotos", - "ok": "Ok", + "ok": "OK", "createAccount": "Criar uma conta", "createNewAccount": "Criar nova conta", "password": "Senha", "confirmPassword": "Confirme sua senha", "activeSessions": "SessĂ”es ativas", - "oops": "Ops", + "oops": "Opa", "somethingWentWrongPleaseTryAgain": "Algo deu errado. Por favor, tente outra vez", "thisWillLogYouOutOfThisDevice": "Isso farĂĄ com que vocĂȘ saia deste dispositivo!", "thisWillLogYouOutOfTheFollowingDevice": "Isso farĂĄ com que vocĂȘ saia do seguinte dispositivo:", @@ -265,7 +265,7 @@ "somethingWentWrong": "Algo deu errado", "sendInvite": "Enviar convite", "shareTextRecommendUsingEnte": "Baixe o Ente para que possamos compartilhar facilmente fotos e vĂ­deos de qualidade original\n\nhttps://ente.io", - "done": "ConcluĂ­do", + "done": "Pronto", "applyCodeTitle": "Aplicar cĂłdigo", "enterCodeDescription": "Digite o cĂłdigo fornecido pelo seu amigo para reivindicar o armazenamento gratuito para vocĂȘs dois", "apply": "Aplicar", @@ -409,7 +409,7 @@ "manageDeviceStorage": "Gerenciar o armazenamento do dispositivo", "machineLearning": "Aprendizagem de mĂĄquina", "magicSearch": "Busca mĂĄgica", - "magicSearchDescription": "Por favor, note que isso resultarĂĄ em uma largura de banda maior e uso de bateria atĂ© que todos os itens sejam indexados.", + "mlIndexingDescription": "Por favor, note que isso resultarĂĄ em uma largura de banda maior e uso de bateria atĂ© que todos os itens sejam indexados.", "loadingModel": "Fazendo download de modelos...", "waitingForWifi": "Esperando por Wi-Fi...", "status": "Estado", @@ -667,7 +667,7 @@ "mobileWebDesktop": "Mobile, Web, Desktop", "newToEnte": "Novo no Ente", "pleaseLoginAgain": "Por favor, faça login novamente", - "devAccountChanged": "A conta de desenvolvedor que usamos para publicar o Ente na App Store foi alterada. Por esse motivo, vocĂȘ precisarĂĄ fazer entrar novamente.\n\nPedimos desculpas pelo inconveniente, mas isso era inevitĂĄvel.", + "autoLogoutMessage": "Devido a erros tĂ©cnicos, vocĂȘ foi desconectado. Pedimos desculpas pelo inconveniente.", "yourSubscriptionHasExpired": "A sua assinatura expirou", "storageLimitExceeded": "Limite de armazenamento excedido", "upgrade": "Aprimorar", @@ -948,7 +948,7 @@ "someOfTheFilesYouAreTryingToDeleteAre": "Alguns dos arquivos que vocĂȘ estĂĄ tentando excluir sĂł estĂŁo disponĂ­veis no seu dispositivo e nĂŁo podem ser recuperados se forem excluĂ­dos", "theyWillBeDeletedFromAllAlbums": "Ele serĂĄ excluĂ­do de todos os ĂĄlbuns.", "someItemsAreInBothEnteAndYourDevice": "Alguns itens estĂŁo tanto no Ente quanto no seu dispositivo.", - "selectedItemsWillBeDeletedFromAllAlbumsAndMoved": "Os itens selecionados serĂŁo excluĂ­dos de todos os ĂĄlbuns e movidos para o lixo.", + "selectedItemsWillBeDeletedFromAllAlbumsAndMoved": "Os itens selecionados serĂŁo excluĂ­dos de todos os ĂĄlbuns e movidos para a lixeira.", "theseItemsWillBeDeletedFromYourDevice": "Estes itens serĂŁo excluĂ­dos do seu dispositivo.", "itLooksLikeSomethingWentWrongPleaseRetryAfterSome": "Parece que algo deu errado. Por favor, tente novamente mais tarde. Se o erro persistir, entre em contato com nossa equipe de suporte.", "error": "Erro", @@ -1042,7 +1042,7 @@ "@storageUsageInfo": { "description": "Example: 1.2 GB of 2 GB used or 100 GB or 2TB used" }, - "freeStorageSpace": "{freeAmount} {storageUnit} livre", + "availableStorageSpace": "{freeAmount} {storageUnit} livre", "appVersion": "VersĂŁo: {versionValue}", "verifyIDLabel": "Verificar", "fileInfoAddDescHint": "Adicionar descrição...", @@ -1102,7 +1102,7 @@ "@iOSGoToSettingsDescription": { "description": "Message advising the user to go to the settings and configure Biometrics for their device. It shows in a dialog on iOS side." }, - "iOSOkButton": "Aceitar", + "iOSOkButton": "Tudo bem", "@iOSOkButton": { "description": "Message showed on a button that the user can click to leave the current dialog. It is used on iOS side. Maximum 30 characters." }, @@ -1233,8 +1233,7 @@ "autoPair": "Pareamento automĂĄtico", "pairWithPin": "Parear com PIN", "faceRecognition": "Reconhecimento facial", - "faceRecognitionIndexingDescription": "Por favor, note que isso resultarĂĄ em uma largura de banda maior e uso de bateria atĂ© que todos os itens sejam indexados.", "foundFaces": "Rostos encontrados", "clusteringProgress": "Progresso de agrupamento", - "indexingIsPaused": "Indexing is paused, will automatically resume when device is ready" + "indexingIsPaused": "A indexação estĂĄ pausada, serĂĄ retomada automaticamente quando o dispositivo estiver pronto." } \ No newline at end of file diff --git a/mobile/lib/l10n/intl_zh.arb b/mobile/lib/l10n/intl_zh.arb index 81fd22914a..456191ecaf 100644 --- a/mobile/lib/l10n/intl_zh.arb +++ b/mobile/lib/l10n/intl_zh.arb @@ -409,7 +409,7 @@ "manageDeviceStorage": "çźĄç†èźŸć€‡ć­˜ć‚š", "machineLearning": "æœș晹歩äč ", "magicSearch": "é­”æł•æœçŽą", - "magicSearchDescription": "èŻ·æłšæ„ïŒŒćœšæ‰€æœ‰éĄčç›źćźŒæˆçŽąćŒ•äč‹ć‰ïŒŒèż™ć°†äœżç”šæ›Žé«˜çš„ćžŠćźœć’Œç””量。", + "mlIndexingDescription": "èŻ·æłšæ„ïŒŒæœș晹歩äč ć°†äœżç”šæ›Žé«˜çš„ćžŠćźœć’Œæ›Žć€šçš„ç””é‡ïŒŒç›Žćˆ°æ‰€æœ‰éĄčç›źéƒœèą«çŽąćŒ•äžșæ­ąă€‚", "loadingModel": "æ­Łćœšäž‹èœœæšĄćž‹...", "waitingForWifi": "æ­Łćœšç­‰ćŸ… WiFi...", "status": "状态", @@ -569,7 +569,7 @@ "freeTrialValidTill": "慍èŽčèŻ•ç”šæœ‰æ•ˆæœŸè‡ł {endDate}", "validTill": "æœ‰æ•ˆæœŸè‡ł {endDate}", "addOnValidTill": "悚的 {storageAmount} æ’ä»¶æœ‰æ•ˆæœŸè‡ł {endDate}", - "playStoreFreeTrialValidTill": "慍èŽčèŻ•ç”šæœ‰æ•ˆæœŸè‡ł {endDate}。\næ‚šćŻä»„éšćŽèŽ­äč°ä»˜èŽčèźĄćˆ’ă€‚", + "playStoreFreeTrialValidTill": "慍èŽčèŻ•ç”šæœ‰æ•ˆæœŸè‡ł {endDate}。\nćœšæ­€äč‹ćŽæ‚šćŻä»„选择付èŽčèźĄćˆ’ă€‚", "subWillBeCancelledOn": "æ‚šçš„èźąé˜…ć°†äșŽ {endDate} ć–æ¶ˆ", "subscription": "èźąé˜…", "paymentDetails": "ä»˜æŹŸæ˜Žç»†", @@ -667,7 +667,7 @@ "mobileWebDesktop": "移抚端, çœ‘éĄ”ç«Ż, æĄŒéąç«Ż", "newToEnte": "ćˆæ„ Ente", "pleaseLoginAgain": "èŻ·é‡æ–°ç™»ćœ•", - "devAccountChanged": "æˆ‘ä»Źç”šäșŽćœš App Store 侊揑澃 Ente çš„ćŒ€ć‘è€…èŽŠæˆ·ć·Č曎æ”čă€‚ć› æ­€ïŒŒæ‚šéœ€èŠé‡æ–°ç™»ćœ•ă€‚\n\nćŻčäșŽç»™æ‚šćžŠæ„çš„äžäŸżïŒŒæˆ‘ä»Źæ·±èĄšæ­‰æ„ïŒŒäœ†èż™æ˜ŻäžćŻéżć…çš„ă€‚", + "autoLogoutMessage": "由äșŽæŠ€æœŻæ•…éšœïŒŒæ‚šć·Č退ć‡șç™»ćœ•ă€‚ćŻčäșŽç”±æ­€é€ æˆçš„äžäŸżïŒŒæˆ‘ä»Źæ·±èĄšæ­‰æ„ă€‚", "yourSubscriptionHasExpired": "æ‚šçš„èźąé˜…ć·Čèż‡æœŸ", "storageLimitExceeded": "ć·Č超ć‡șć­˜ć‚šé™ćˆ¶", "upgrade": "捇çș§", @@ -987,7 +987,7 @@ "fileTypesAndNames": "æ–‡ä»¶ç±»ćž‹ć’Œćç§°", "location": "ćœ°ç†äœçœź", "moments": "瞬问", - "searchFaceEmptySection": "柄扟䞀äžȘäșș的所有照片", + "searchFaceEmptySection": "ćŸ…çŽąćŒ•ćźŒæˆćŽïŒŒäșșç‰©ć°†æ˜Ÿç€șćœšæ­€ć€„", "searchDatesEmptySection": "æŒ‰æ—„æœŸæœçŽąïŒŒæœˆä»œæˆ–ćčŽä»œ", "searchLocationEmptySection": "ćœšç…§ç‰‡çš„äž€ćźšćŠćŸ„ć†…æ‹æ‘„çš„ć‡ ç»„ç…§ç‰‡", "searchPeopleEmptySection": "é‚€èŻ·ä»–äșșïŒŒæ‚šć°†ćœšæ­€çœ‹ćˆ°ä»–ä»Źćˆ†äș«çš„æ‰€æœ‰ç…§ç‰‡", @@ -1042,7 +1042,6 @@ "@storageUsageInfo": { "description": "Example: 1.2 GB of 2 GB used or 100 GB or 2TB used" }, - "freeStorageSpace": "{freeAmount} {storageUnit} ç©șé—Č", "appVersion": "ç‰ˆæœŹ: {versionValue}", "verifyIDLabel": "éȘŒè݁", "fileInfoAddDescHint": "æ·»ćŠ èŻŽæ˜Ž...", @@ -1171,6 +1170,7 @@ } }, "faces": "äșș脞", + "people": "äșș物", "contents": "憅ćźč", "addNew": "新ć»ș", "@addNew": { @@ -1196,14 +1196,14 @@ "verifyPasskey": "éȘŒèŻé€šèĄŒćŻ†é’„", "playOnTv": "ćœšç””è§†äžŠæ’­æ”Ÿç›žć†Œ", "pair": "配ćŻč", - "autoPair": "è‡Ș抹配ćŻč", - "pairWithPin": "甹 PIN 配ćŻč", "deviceNotFound": "æœȘć‘çŽ°èźŸć€‡", "castInstruction": "ćœšæ‚šèŠé…ćŻčçš„èźŸć€‡äžŠèźżé—ź cast.ente.io。\nèŸ“ć…„äž‹éąçš„ä»Łç ćłćŻćœšç””è§†äžŠæ’­æ”Ÿç›žć†Œă€‚", "deviceCodeHint": "èŸ“ć…„ä»Łç ", "joinDiscord": "抠慄 Discord", "locations": "äœçœź", "descriptions": "æèż°", + "addAName": "æ·»ćŠ äž€äžȘ損称", + "findPeopleByName": "æŒ‰ćç§°ćż«é€ŸæŸ„æ‰Ÿäșș物", "addViewers": "{count, plural, zero {æ·»ćŠ æŸ„çœ‹è€…} one {æ·»ćŠ æŸ„çœ‹è€…} other {æ·»ćŠ æŸ„çœ‹è€…}}", "addCollaborators": "{count, plural, zero {æ·»ćŠ ćäœœè€…} one {æ·»ćŠ ćäœœè€…} other {æ·»ćŠ ćäœœè€…}}", "longPressAnEmailToVerifyEndToEndEncryption": "é•żæŒ‰ç””ć­é‚źä»¶ä»„éȘŒèŻç«Żćˆ°ç«ŻćŠ ćŻ†ă€‚", @@ -1216,6 +1216,8 @@ "customEndpoint": "ć·ČèżžæŽ„è‡ł {endpoint}", "createCollaborativeLink": "戛ć»șćäœœé“ŸæŽ„", "search": "æœçŽą", + "enterPersonName": "èŸ“ć…„äșșç‰©ćç§°", + "removePersonLabel": "移陀äșș物标筟", "autoPairDesc": "è‡Ș抹配ćŻč仅适甚äșŽæ”ŻæŒ Chromecast çš„èźŸć€‡ă€‚", "manualPairDesc": "甹 PIN 码配ćŻč适甹äșŽæ‚šćžŒæœ›ćœšć…¶äžŠæŸ„çœ‹ç›žć†Œçš„ä»»äœ•ć±ćč•。", "connectToDevice": "èżžæŽ„ćˆ°èźŸć€‡", @@ -1227,9 +1229,10 @@ "castIPMismatchTitle": "æŠ•æ”Ÿç›žć†Œć€±èŽ„", "castIPMismatchBody": "èŻ·çĄźäżæ‚šçš„èźŸć€‡äžŽç””è§†ć€„äșŽćŒäž€çœ‘ç»œă€‚", "pairingComplete": "配ćŻčćźŒæˆ", - "faceRecognition": "Face recognition", - "faceRecognitionIndexingDescription": "Please note that this will result in a higher bandwidth and battery usage until all items are indexed.", - "foundFaces": "Found faces", - "clusteringProgress": "Clustering progress", - "indexingIsPaused": "Indexing is paused, will automatically resume when device is ready" + "autoPair": "è‡Ș抹配ćŻč", + "pairWithPin": "甹 PIN 配ćŻč", + "faceRecognition": "äșșè„žèŻ†ćˆ«", + "foundFaces": "ć·Čæ‰Ÿćˆ°çš„äșș脞", + "clusteringProgress": "èšç±»èż›ć±•", + "indexingIsPaused": "çŽąćŒ•ć·Čæš‚ćœă€‚ćœ“èźŸć€‡ć‡†ć€‡ć°±ç»Șæ—¶ïŒŒćźƒć°†è‡ȘćŠšæąć€ă€‚" } \ No newline at end of file diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart index 247ab9553f..5418afed89 100644 --- a/mobile/lib/main.dart +++ b/mobile/lib/main.dart @@ -181,76 +181,72 @@ void _headlessTaskHandler(HeadlessTask task) { } Future _init(bool isBackground, {String via = ''}) async { - bool initComplete = false; - Future.delayed(const Duration(seconds: 15), () { - if (!initComplete && !isBackground) { - sendLogsForInit( - "support@ente.io", - "Stuck on splash screen for >= 15 seconds", - null, - ); - } - }); - _isProcessRunning = true; - _logger.info("Initializing... inBG =$isBackground via: $via"); - final SharedPreferences preferences = await SharedPreferences.getInstance(); - - await _logFGHeartBeatInfo(); - unawaited(_scheduleHeartBeat(preferences, isBackground)); - AppLifecycleService.instance.init(preferences); - if (isBackground) { - AppLifecycleService.instance.onAppInBackground('init via: $via'); - } else { - AppLifecycleService.instance.onAppInForeground('init via: $via'); - } - // Start workers asynchronously. No need to wait for them to start - Computer.shared().turnOn(workersCount: 4).ignore(); - CryptoUtil.init(); - await Configuration.instance.init(); - await NetworkClient.instance.init(); - ServiceLocator.instance.init(preferences, NetworkClient.instance.enteDio); - await UserService.instance.init(); - await EntityService.instance.init(); - LocationService.instance.init(preferences); - - await UserRemoteFlagService.instance.init(); - await UpdateService.instance.init(); - BillingService.instance.init(); - await CollectionsService.instance.init(preferences); - FavoritesService.instance.initFav().ignore(); - await FileUploader.instance.init(preferences, isBackground); - await LocalSyncService.instance.init(preferences); - TrashSyncService.instance.init(preferences); - RemoteSyncService.instance.init(preferences); - await SyncService.instance.init(preferences); - MemoriesService.instance.init(preferences); - LocalSettings.instance.init(preferences); - LocalFileUpdateService.instance.init(preferences); - SearchService.instance.init(); - StorageBonusService.instance.init(preferences); - RemoteFileMLService.instance.init(preferences); - if (!isBackground && - Platform.isAndroid && - await HomeWidgetService.instance.countHomeWidgets() == 0) { - unawaited(HomeWidgetService.instance.initHomeWidget()); - } - - if (Platform.isIOS) { - // ignore: unawaited_futures - PushService.instance.init().then((_) { - FirebaseMessaging.onBackgroundMessage( - _firebaseMessagingBackgroundHandler, - ); + try { + bool initComplete = false; + Future.delayed(const Duration(seconds: 15), () { + if (!initComplete && !isBackground) { + sendLogsForInit( + "support@ente.io", + "Stuck on splash screen for >= 15 seconds", + null, + ); + } }); - } + _isProcessRunning = true; + _logger.info("Initializing... inBG =$isBackground via: $via"); + final SharedPreferences preferences = await SharedPreferences.getInstance(); - unawaited(SemanticSearchService.instance.init()); - MachineLearningController.instance.init(); - // Can not including existing tf/ml binaries as they are not being built - // from source. - // See https://gitlab.com/fdroid/fdroiddata/-/merge_requests/12671#note_1294346819 - if (!UpdateService.instance.isFdroidFlavor()) { - // unawaited(ObjectDetectionService.instance.init()); + await _logFGHeartBeatInfo(); + unawaited(_scheduleHeartBeat(preferences, isBackground)); + AppLifecycleService.instance.init(preferences); + if (isBackground) { + AppLifecycleService.instance.onAppInBackground('init via: $via'); + } else { + AppLifecycleService.instance.onAppInForeground('init via: $via'); + } + // Start workers asynchronously. No need to wait for them to start + Computer.shared().turnOn(workersCount: 4).ignore(); + CryptoUtil.init(); + await Configuration.instance.init(); + await NetworkClient.instance.init(); + ServiceLocator.instance.init(preferences, NetworkClient.instance.enteDio); + await UserService.instance.init(); + await EntityService.instance.init(); + LocationService.instance.init(preferences); + + await UserRemoteFlagService.instance.init(); + await UpdateService.instance.init(); + BillingService.instance.init(); + await CollectionsService.instance.init(preferences); + FavoritesService.instance.initFav().ignore(); + await FileUploader.instance.init(preferences, isBackground); + await LocalSyncService.instance.init(preferences); + TrashSyncService.instance.init(preferences); + RemoteSyncService.instance.init(preferences); + await SyncService.instance.init(preferences); + MemoriesService.instance.init(preferences); + LocalSettings.instance.init(preferences); + LocalFileUpdateService.instance.init(preferences); + SearchService.instance.init(); + StorageBonusService.instance.init(preferences); + RemoteFileMLService.instance.init(preferences); + if (!isBackground && + Platform.isAndroid && + await HomeWidgetService.instance.countHomeWidgets() == 0) { + unawaited(HomeWidgetService.instance.initHomeWidget()); + } + + if (Platform.isIOS) { + // ignore: unawaited_futures + PushService.instance.init().then((_) { + FirebaseMessaging.onBackgroundMessage( + _firebaseMessagingBackgroundHandler, + ); + }); + } + + unawaited(SemanticSearchService.instance.init()); + MachineLearningController.instance.init(); if (flagService.faceSearchEnabled) { unawaited(FaceMlService.instance.init()); } else { @@ -258,15 +254,18 @@ Future _init(bool isBackground, {String via = ''}) async { unawaited(LocalSettings.instance.toggleFaceIndexing()); } } - } - PersonService.init( - EntityService.instance, - FaceMLDataDB.instance, - preferences, - ); + PersonService.init( + EntityService.instance, + FaceMLDataDB.instance, + preferences, + ); - initComplete = true; - _logger.info("Initialization done"); + initComplete = true; + _logger.info("Initialization done"); + } catch (e, s) { + _logger.severe("Error in init", e, s); + rethrow; + } } Future _sync(String caller) async { diff --git a/mobile/lib/services/favorites_service.dart b/mobile/lib/services/favorites_service.dart index fef4a323a8..2f64e63d41 100644 --- a/mobile/lib/services/favorites_service.dart +++ b/mobile/lib/services/favorites_service.dart @@ -151,9 +151,7 @@ class FavoritesService { final collectionID = await _getOrCreateFavoriteCollectionID(); final List files = [file]; if (file.uploadedFileID == null) { - file.collectionID = collectionID; - await _filesDB.insert(file); - Bus.instance.fire(CollectionUpdatedEvent(collectionID, files, "addTFav")); + throw AssertionError("Can only favorite uploaded items"); } else { await _collectionsService.addOrCopyToCollection(collectionID, files); } diff --git a/mobile/lib/services/local_file_update_service.dart b/mobile/lib/services/local_file_update_service.dart index e00ac6c459..ce5a9080af 100644 --- a/mobile/lib/services/local_file_update_service.dart +++ b/mobile/lib/services/local_file_update_service.dart @@ -193,7 +193,7 @@ class LocalFileUpdateService { } else if (e.reason == InvalidReason.imageToLivePhotoTypeChanged) { fileType = FileType.livePhoto; } - final int count = await FilesDB.instance.markFilesForReUpload( + await FilesDB.instance.markFilesForReUpload( userID, file.localID!, file.title, @@ -202,8 +202,7 @@ class LocalFileUpdateService { file.modificationTime!, fileType, ); - _logger.fine('fileType changed for ${file.tag} to ${e.reason} for ' - '$count files'); + _logger.fine('fileType changed for ${file.tag} to ${e.reason} for '); } else { _logger.severe("failed to check hash: invalid file ${file.tag}", e); } diff --git a/mobile/lib/services/local_sync_service.dart b/mobile/lib/services/local_sync_service.dart index 93b3c94373..1915ac30c2 100644 --- a/mobile/lib/services/local_sync_service.dart +++ b/mobile/lib/services/local_sync_service.dart @@ -21,8 +21,8 @@ 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:shared_preferences/shared_preferences.dart'; -import 'package:sqflite/sqflite.dart'; import 'package:tuple/tuple.dart'; class LocalSyncService { @@ -184,7 +184,7 @@ class LocalSyncService { if (hasUnsyncedFiles) { await _db.insertMultiple( localDiffResult.uniqueLocalFiles!, - conflictAlgorithm: ConflictAlgorithm.ignore, + conflictAlgorithm: SqliteAsyncConflictAlgorithm.ignore, ); _logger.info( "Inserted ${localDiffResult.uniqueLocalFiles?.length} " @@ -321,7 +321,7 @@ class LocalSyncService { files.removeWhere((file) => existingLocalDs.contains(file.localID)); await _db.insertMultiple( files, - conflictAlgorithm: ConflictAlgorithm.ignore, + conflictAlgorithm: SqliteAsyncConflictAlgorithm.ignore, ); _logger.info('Inserted ${files.length} files'); Bus.instance.fire( diff --git a/mobile/lib/services/machine_learning/face_ml/face_clustering/cosine_distance.dart b/mobile/lib/services/machine_learning/face_ml/face_clustering/cosine_distance.dart index 0611a1d838..c081ef4520 100644 --- a/mobile/lib/services/machine_learning/face_ml/face_clustering/cosine_distance.dart +++ b/mobile/lib/services/machine_learning/face_ml/face_clustering/cosine_distance.dart @@ -5,17 +5,15 @@ import "package:ml_linalg/linalg.dart"; /// Calculates the cosine distance between two embeddings/vectors using SIMD from ml_linalg /// /// WARNING: This assumes both vectors are already normalized! +/// WARNING: For even more performance, consider calculating the logic below inline! +@pragma("vm:prefer-inline") double cosineDistanceSIMD(Vector vector1, Vector vector2) { - if (vector1.length != vector2.length) { - throw ArgumentError('Vectors must be the same length'); - } - return 1 - vector1.dot(vector2); } /// Calculates the cosine distance between two embeddings/vectors using SIMD from ml_linalg /// -/// WARNING: Only use when you're not sure if vectors are normalized. If you're sure they are, use [cosineDistanceSIMD] instead for better performance. +/// WARNING: Only use when you're not sure if vectors are normalized. If you're sure they are, use [cosineDistanceSIMD] instead for better performance, or inline for best performance. double cosineDistanceSIMDSafe(Vector vector1, Vector vector2) { if (vector1.length != vector2.length) { throw ArgumentError('Vectors must be the same length'); diff --git a/mobile/lib/services/machine_learning/face_ml/face_clustering/face_clustering_service.dart b/mobile/lib/services/machine_learning/face_ml/face_clustering/face_clustering_service.dart index 1a635b0f07..84b180eb8b 100644 --- a/mobile/lib/services/machine_learning/face_ml/face_clustering/face_clustering_service.dart +++ b/mobile/lib/services/machine_learning/face_ml/face_clustering/face_clustering_service.dart @@ -10,11 +10,9 @@ import "package:logging/logging.dart"; import "package:ml_linalg/dtype.dart"; import "package:ml_linalg/vector.dart"; import "package:photos/generated/protos/ente/common/vector.pb.dart"; -import 'package:photos/services/machine_learning/face_ml/face_clustering/cosine_distance.dart'; import "package:photos/services/machine_learning/face_ml/face_clustering/face_info_for_clustering.dart"; import "package:photos/services/machine_learning/face_ml/face_filtering/face_filtering_constants.dart"; import "package:photos/services/machine_learning/face_ml/face_ml_result.dart"; -import "package:simple_cluster/simple_cluster.dart"; import "package:synchronized/synchronized.dart"; class FaceInfo { @@ -22,7 +20,6 @@ class FaceInfo { final double? faceScore; final double? blurValue; final bool? badFace; - final List? embedding; final Vector? vEmbedding; int? clusterId; String? closestFaceId; @@ -33,27 +30,34 @@ class FaceInfo { this.faceScore, this.blurValue, this.badFace, - this.embedding, this.vEmbedding, this.clusterId, this.fileCreationTime, }); } -enum ClusterOperation { linearIncrementalClustering, dbscanClustering } +enum ClusterOperation { linearIncrementalClustering } class ClusteringResult { final Map newFaceIdToCluster; - final Map>? newClusterIdToFaceIds; - final Map? newClusterSummaries; + final Map> newClusterIdToFaceIds; + final Map newClusterSummaries; bool get isEmpty => newFaceIdToCluster.isEmpty; - + ClusteringResult({ required this.newFaceIdToCluster, - this.newClusterSummaries, - this.newClusterIdToFaceIds, + required this.newClusterSummaries, + required this.newClusterIdToFaceIds, }); + + factory ClusteringResult.empty() { + return ClusteringResult( + newFaceIdToCluster: {}, + newClusterIdToFaceIds: {}, + newClusterSummaries: {}, + ); + } } class FaceClusteringService { @@ -61,7 +65,7 @@ class FaceClusteringService { final _computer = Computer.shared(); Timer? _inactivityTimer; - final Duration _inactivityDuration = const Duration(minutes: 3); + final Duration _inactivityDuration = const Duration(minutes: 2); int _activeTasks = 0; final _initLock = Lock(); @@ -84,7 +88,7 @@ class FaceClusteringService { static final instance = FaceClusteringService._privateConstructor(); factory FaceClusteringService() => instance; - Future init() async { + Future _initIsolate() async { return _initLock.synchronized(() async { if (isSpawned) return; @@ -106,9 +110,9 @@ class FaceClusteringService { }); } - Future ensureSpawned() async { + Future _ensureSpawnedIsolate() async { if (!isSpawned) { - await init(); + await _initIsolate(); } } @@ -126,17 +130,12 @@ class FaceClusteringService { try { switch (function) { case ClusterOperation.linearIncrementalClustering: - final result = FaceClusteringService.runLinearClustering(args); - sendPort.send(result); - break; - case ClusterOperation.dbscanClustering: - final result = FaceClusteringService._runDbscanClustering(args); + final ClusteringResult result = _runLinearClustering(args); sendPort.send(result); break; } } catch (e, stackTrace) { - sendPort - .send({'error': e.toString(), 'stackTrace': stackTrace.toString()}); + sendPort.send({'error': e.toString(), 'stackTrace': stackTrace.toString()}); } }); } @@ -145,7 +144,7 @@ class FaceClusteringService { Future _runInIsolate( (ClusterOperation, Map) message, ) async { - await ensureSpawned(); + await _ensureSpawnedIsolate(); _resetInactivityTimer(); final completer = Completer(); final answerPort = ReceivePort(); @@ -185,13 +184,13 @@ class FaceClusteringService { _logger.info( 'Clustering Isolate has been inactive for ${_inactivityDuration.inSeconds} seconds with no tasks running. Killing isolate.', ); - dispose(); + _dispose(); } }); } /// Disposes the isolate worker. - void dispose() { + void _dispose() { if (!isSpawned) return; isSpawned = false; @@ -200,12 +199,10 @@ class FaceClusteringService { _inactivityTimer?.cancel(); } - /// Runs the clustering algorithm [runLinearClustering] on the given [input], in an isolate. + /// Runs the clustering algorithm [_runLinearClustering] on the given [input], in an isolate. /// /// Returns the clustering result, which is a list of clusters, where each cluster is a list of indices of the dataset. - /// - /// WARNING: Make sure to always input data in the same ordering, otherwise the clustering can less less deterministic. - Future predictLinear( + Future predictLinearIsolate( Set input, { Map? fileIDToCreationTime, double distanceThreshold = kRecommendedDistanceThreshold, @@ -234,7 +231,7 @@ class FaceClusteringService { final stopwatchClustering = Stopwatch()..start(); // final Map faceIdToCluster = // await _runLinearClusteringInComputer(input); - final ClusteringResult? faceIdToCluster = await _runInIsolate( + final ClusteringResult faceIdToCluster = await _runInIsolate( ( ClusterOperation.linearIncrementalClustering, { @@ -262,17 +259,59 @@ class FaceClusteringService { } } - /// Runs the clustering algorithm [runLinearClustering] on the given [input], in computer, without any dynamic thresholding - Future predictLinearComputer( + Future predictWithinClusterComputer( Map input, { Map? fileIDToCreationTime, + Map oldClusterSummaries = const {}, + double distanceThreshold = kRecommendedDistanceThreshold, + }) async { + _logger.info( + '`predictWithinClusterComputer` called with ${input.length} faces and distance threshold $distanceThreshold', + ); + try { + if (input.length < 500) { + final mergeThreshold = distanceThreshold; + _logger.info( + 'Running complete clustering on ${input.length} faces with distance threshold $mergeThreshold', + ); + final ClusteringResult clusterResult = await predictCompleteComputer( + input, + fileIDToCreationTime: fileIDToCreationTime, + oldClusterSummaries: oldClusterSummaries, + distanceThreshold: distanceThreshold - 0.08, + mergeThreshold: mergeThreshold, + ); + return clusterResult; + } else { + _logger.info( + 'Running linear clustering on ${input.length} faces with distance threshold $distanceThreshold', + ); + final ClusteringResult clusterResult = await predictLinearComputer( + input, + fileIDToCreationTime: fileIDToCreationTime, + oldClusterSummaries: oldClusterSummaries, + distanceThreshold: distanceThreshold, + ); + return clusterResult; + } + } catch (e, s) { + _logger.severe(e, s); + rethrow; + } + } + + /// Runs the clustering algorithm [_runLinearClustering] on the given [input], in computer, without any dynamic thresholding + Future predictLinearComputer( + Map input, { + Map? fileIDToCreationTime, + required Map oldClusterSummaries, double distanceThreshold = kRecommendedDistanceThreshold, }) async { if (input.isEmpty) { _logger.warning( "Linear Clustering dataset of embeddings is empty, returning empty list.", ); - return null; + return ClusteringResult.empty(); } // Clustering inside the isolate @@ -297,10 +336,11 @@ class FaceClusteringService { .toSet(); final startTime = DateTime.now(); final faceIdToCluster = await _computer.compute( - runLinearClustering, + _runLinearClustering, param: { "input": clusteringInput, "fileIDToCreationTime": fileIDToCreationTime, + "oldClusterSummaries": oldClusterSummaries, "distanceThreshold": distanceThreshold, "conservativeDistanceThreshold": distanceThreshold - 0.08, "useDynamicThreshold": false, @@ -318,12 +358,13 @@ class FaceClusteringService { } } - /// Runs the clustering algorithm [runCompleteClustering] on the given [input], in computer. + /// Runs the clustering algorithm [_runCompleteClustering] on the given [input], in computer. /// /// WARNING: Only use on small datasets, as it is not optimized for large datasets. Future predictCompleteComputer( Map input, { Map? fileIDToCreationTime, + required Map oldClusterSummaries, double distanceThreshold = kRecommendedDistanceThreshold, double mergeThreshold = 0.30, }) async { @@ -331,7 +372,7 @@ class FaceClusteringService { _logger.warning( "Complete Clustering dataset of embeddings is empty, returning empty list.", ); - return ClusteringResult(newFaceIdToCluster: {}); + return ClusteringResult.empty(); } // Clustering inside the isolate @@ -342,10 +383,11 @@ class FaceClusteringService { try { final startTime = DateTime.now(); final clusteringResult = await _computer.compute( - runCompleteClustering, + _runCompleteClustering, param: { "input": input, "fileIDToCreationTime": fileIDToCreationTime, + "oldClusterSummaries": oldClusterSummaries, "distanceThreshold": distanceThreshold, "mergeThreshold": mergeThreshold, }, @@ -361,638 +403,367 @@ class FaceClusteringService { rethrow; } } +} - Future predictWithinClusterComputer( - Map input, { - Map? fileIDToCreationTime, - double distanceThreshold = kRecommendedDistanceThreshold, - }) async { - _logger.info( - '`predictWithinClusterComputer` called with ${input.length} faces and distance threshold $distanceThreshold', - ); - try { - if (input.length < 500) { - final mergeThreshold = distanceThreshold; - _logger.info( - 'Running complete clustering on ${input.length} faces with distance threshold $mergeThreshold', - ); - final result = await predictCompleteComputer( - input, - fileIDToCreationTime: fileIDToCreationTime, - distanceThreshold: distanceThreshold - 0.08, - mergeThreshold: mergeThreshold, - ); - if (result.newFaceIdToCluster.isEmpty) return null; - return result; - } else { - _logger.info( - 'Running linear clustering on ${input.length} faces with distance threshold $distanceThreshold', - ); - final clusterResult = await predictLinearComputer( - input, - fileIDToCreationTime: fileIDToCreationTime, - distanceThreshold: distanceThreshold, - ); - return clusterResult; - } - } catch (e, s) { - _logger.severe(e, s); - rethrow; - } - } +ClusteringResult _runLinearClustering(Map args) { + // final input = args['input'] as Map; + final input = args['input'] as Set; + final fileIDToCreationTime = args['fileIDToCreationTime'] as Map?; + final distanceThreshold = args['distanceThreshold'] as double; + final conservativeDistanceThreshold = args['conservativeDistanceThreshold'] as double; + final useDynamicThreshold = args['useDynamicThreshold'] as bool; + final offset = args['offset'] as int?; + final oldClusterSummaries = args['oldClusterSummaries'] as Map?; - Future>> predictDbscan( - Map input, { - Map? fileIDToCreationTime, - double eps = 0.3, - int minPts = 5, - }) async { - if (input.isEmpty) { - _logger.warning( - "DBSCAN Clustering dataset of embeddings is empty, returning empty list.", - ); - return []; - } - if (isRunning) { - _logger.warning( - "DBSCAN Clustering is already running, returning empty list.", - ); - return []; - } + log( + "[ClusterIsolate] ${DateTime.now()} Copied to isolate ${input.length} faces", + ); - isRunning = true; - - // Clustering inside the isolate - _logger.info( - "Start DBSCAN clustering on ${input.length} embeddings inside computer isolate", - ); - final stopwatchClustering = Stopwatch()..start(); - // final Map faceIdToCluster = - // await _runLinearClusteringInComputer(input); - final List> clusterFaceIDs = await _runInIsolate( - ( - ClusterOperation.dbscanClustering, - { - 'input': input, - 'fileIDToCreationTime': fileIDToCreationTime, - 'eps': eps, - 'minPts': minPts, - } + // Organize everything into a list of FaceInfo objects + final List faceInfos = []; + for (final face in input) { + faceInfos.add( + FaceInfo( + faceID: face.faceID, + faceScore: face.faceScore, + blurValue: face.blurValue, + badFace: face.faceScore < kMinimumQualityFaceScore || + face.blurValue < kLaplacianSoftThreshold || + (face.blurValue < kLaplacianVerySoftThreshold && + face.faceScore < kMediumQualityFaceScore) || + face.isSideways, + vEmbedding: Vector.fromList( + EVector.fromBuffer(face.embeddingBytes).values, + dtype: DType.float32, + ), + clusterId: face.clusterId, + fileCreationTime: fileIDToCreationTime?[getFileIdFromFaceId(face.faceID)], ), ); - // return _runLinearClusteringInComputer(input); - _logger.info( - 'DBSCAN Clustering executed in ${stopwatchClustering.elapsed.inSeconds} seconds', - ); - - isRunning = false; - - return clusterFaceIDs; } - static ClusteringResult? runLinearClustering(Map args) { - // final input = args['input'] as Map; - final input = args['input'] as Set; - final fileIDToCreationTime = args['fileIDToCreationTime'] as Map?; - final distanceThreshold = args['distanceThreshold'] as double; - final conservativeDistanceThreshold = - args['conservativeDistanceThreshold'] as double; - final useDynamicThreshold = args['useDynamicThreshold'] as bool; - final offset = args['offset'] as int?; - final oldClusterSummaries = - args['oldClusterSummaries'] as Map?; + // Assert that the embeddings are normalized + for (final faceInfo in faceInfos) { + if (faceInfo.vEmbedding != null) { + final norm = faceInfo.vEmbedding!.norm(); + assert((norm - 1.0).abs() < 1e-5); + } + } - log( - "[ClusterIsolate] ${DateTime.now()} Copied to isolate ${input.length} faces", - ); + if (fileIDToCreationTime != null) { + _sortFaceInfosOnCreationTime(faceInfos); + } - // Organize everything into a list of FaceInfo objects - final List faceInfos = []; - for (final face in input) { - faceInfos.add( - FaceInfo( - faceID: face.faceID, - faceScore: face.faceScore, - blurValue: face.blurValue, - badFace: face.faceScore < kMinimumQualityFaceScore || - face.blurValue < kLaplacianSoftThreshold || - (face.blurValue < kLaplacianVerySoftThreshold && - face.faceScore < kMediumQualityFaceScore) || - face.isSideways, - vEmbedding: Vector.fromList( - EVector.fromBuffer(face.embeddingBytes).values, - dtype: DType.float32, - ), - clusterId: face.clusterId, - fileCreationTime: - fileIDToCreationTime?[getFileIdFromFaceId(face.faceID)], - ), - ); + // Sort the faceInfos such that the ones with null clusterId are at the end + final List facesWithClusterID = []; + final List facesWithoutClusterID = []; + for (final FaceInfo faceInfo in faceInfos) { + if (faceInfo.clusterId == null) { + facesWithoutClusterID.add(faceInfo); + } else { + facesWithClusterID.add(faceInfo); + } + } + final alreadyClusteredCount = facesWithClusterID.length; + final sortedFaceInfos = []; + sortedFaceInfos.addAll(facesWithClusterID); + sortedFaceInfos.addAll(facesWithoutClusterID); + + log( + "[ClusterIsolate] ${DateTime.now()} Clustering ${facesWithoutClusterID.length} new faces without clusterId, and $alreadyClusteredCount faces with clusterId", + ); + + // Make sure the first face has a clusterId + final int totalFaces = sortedFaceInfos.length; + int dynamicThresholdCount = 0; + + if (sortedFaceInfos.isEmpty) { + return ClusteringResult.empty(); + } + + // Start actual clustering + log( + "[ClusterIsolate] ${DateTime.now()} Processing $totalFaces faces in total in this round ${offset != null ? "on top of ${offset + facesWithClusterID.length} earlier processed faces" : ""}", + ); + // set current epoch time as clusterID + int clusterID = DateTime.now().microsecondsSinceEpoch; + if (facesWithClusterID.isEmpty) { + // assign a clusterID to the first face + sortedFaceInfos[0].clusterId = clusterID; + clusterID++; + } + final stopwatchClustering = Stopwatch()..start(); + for (int i = 1; i < totalFaces; i++) { + // Incremental clustering, so we can skip faces that already have a clusterId + if (sortedFaceInfos[i].clusterId != null) { + clusterID = max(clusterID, sortedFaceInfos[i].clusterId!); + continue; } - // Assert that the embeddings are normalized - for (final faceInfo in faceInfos) { - if (faceInfo.vEmbedding != null) { - final norm = faceInfo.vEmbedding!.norm(); - assert((norm - 1.0).abs() < 1e-5); - } - } - - if (fileIDToCreationTime != null) { - _sortFaceInfosOnCreationTime(faceInfos); - } - - // Sort the faceInfos such that the ones with null clusterId are at the end - final List facesWithClusterID = []; - final List facesWithoutClusterID = []; - for (final FaceInfo faceInfo in faceInfos) { - if (faceInfo.clusterId == null) { - facesWithoutClusterID.add(faceInfo); - } else { - facesWithClusterID.add(faceInfo); - } - } - final alreadyClusteredCount = facesWithClusterID.length; - final sortedFaceInfos = []; - sortedFaceInfos.addAll(facesWithClusterID); - sortedFaceInfos.addAll(facesWithoutClusterID); - - log( - "[ClusterIsolate] ${DateTime.now()} Clustering ${facesWithoutClusterID.length} new faces without clusterId, and $alreadyClusteredCount faces with clusterId", - ); - - // Make sure the first face has a clusterId - final int totalFaces = sortedFaceInfos.length; - int dynamicThresholdCount = 0; - - if (sortedFaceInfos.isEmpty) { - return null; - } - - // Start actual clustering - log( - "[ClusterIsolate] ${DateTime.now()} Processing $totalFaces faces in total in this round ${offset != null ? "on top of ${offset + facesWithClusterID.length} earlier processed faces" : ""}", - ); - // set current epoch time as clusterID - int clusterID = DateTime.now().microsecondsSinceEpoch; - if (facesWithClusterID.isEmpty) { - // assign a clusterID to the first face - sortedFaceInfos[0].clusterId = clusterID; - clusterID++; - } - final stopwatchClustering = Stopwatch()..start(); - for (int i = 1; i < totalFaces; i++) { - // Incremental clustering, so we can skip faces that already have a clusterId - if (sortedFaceInfos[i].clusterId != null) { - clusterID = max(clusterID, sortedFaceInfos[i].clusterId!); - continue; - } - - int closestIdx = -1; - double closestDistance = double.infinity; - late double thresholdValue; - if (useDynamicThreshold) { - thresholdValue = sortedFaceInfos[i].badFace! - ? conservativeDistanceThreshold - : distanceThreshold; - if (sortedFaceInfos[i].badFace!) dynamicThresholdCount++; - } else { - thresholdValue = distanceThreshold; - } - if (i % 250 == 0) { - log("[ClusterIsolate] ${DateTime.now()} Processed ${offset != null ? i + offset : i} faces"); - } - for (int j = i - 1; j >= 0; j--) { - late double distance; - if (sortedFaceInfos[i].vEmbedding != null) { - distance = cosineDistanceSIMD( - sortedFaceInfos[i].vEmbedding!, - sortedFaceInfos[j].vEmbedding!, - ); - } else { - distance = cosineDistForNormVectors( - sortedFaceInfos[i].embedding!, - sortedFaceInfos[j].embedding!, - ); - } - if (distance < closestDistance) { - if (sortedFaceInfos[j].badFace! && - distance > conservativeDistanceThreshold) { - continue; - } - closestDistance = distance; - closestIdx = j; - } - } - - if (closestDistance < thresholdValue) { - if (sortedFaceInfos[closestIdx].clusterId == null) { - // Ideally this should never happen, but just in case log it - log( - " [ClusterIsolate] [WARNING] ${DateTime.now()} Found new cluster $clusterID", - ); - clusterID++; - sortedFaceInfos[closestIdx].clusterId = clusterID; - } - sortedFaceInfos[i].clusterId = sortedFaceInfos[closestIdx].clusterId; - } else { - clusterID++; - sortedFaceInfos[i].clusterId = clusterID; - } - } - - // Finally, assign the new clusterId to the faces - final Map newFaceIdToCluster = {}; - final newClusteredFaceInfos = - sortedFaceInfos.sublist(alreadyClusteredCount); - for (final faceInfo in newClusteredFaceInfos) { - newFaceIdToCluster[faceInfo.faceID] = faceInfo.clusterId!; - } - - // Create a map of clusterId to faceIds - final Map> clusterIdToFaceIds = {}; - for (final entry in newFaceIdToCluster.entries) { - final clusterID = entry.value; - if (clusterIdToFaceIds.containsKey(clusterID)) { - clusterIdToFaceIds[clusterID]!.add(entry.key); - } else { - clusterIdToFaceIds[clusterID] = [entry.key]; - } - } - - stopwatchClustering.stop(); - log( - ' [ClusterIsolate] ${DateTime.now()} Clustering for ${sortedFaceInfos.length} embeddings executed in ${stopwatchClustering.elapsedMilliseconds}ms', - ); + int closestIdx = -1; + double closestDistance = double.infinity; + late double thresholdValue; if (useDynamicThreshold) { - log( - "[ClusterIsolate] ${DateTime.now()} Dynamic thresholding: $dynamicThresholdCount faces had a low face score or low blur clarity", - ); + thresholdValue = + sortedFaceInfos[i].badFace! ? conservativeDistanceThreshold : distanceThreshold; + if (sortedFaceInfos[i].badFace!) dynamicThresholdCount++; + } else { + thresholdValue = distanceThreshold; } - - // Now calculate the mean of the embeddings for each cluster and update the cluster summaries - Map? newClusterSummaries; - if (oldClusterSummaries != null) { - newClusterSummaries = FaceClusteringService.updateClusterSummaries( - oldSummary: oldClusterSummaries, - newFaceInfos: newClusteredFaceInfos, - ); + if (i % 250 == 0) { + log("[ClusterIsolate] ${DateTime.now()} Processed ${offset != null ? i + offset : i} faces"); } - - // analyze the results - // FaceClusteringService._analyzeClusterResults(sortedFaceInfos); - - return ClusteringResult( - newFaceIdToCluster: newFaceIdToCluster, - newClusterSummaries: newClusterSummaries, - newClusterIdToFaceIds: clusterIdToFaceIds, - ); - } - - static Map updateClusterSummaries({ - required Map oldSummary, - required List newFaceInfos, - }) { - final calcSummariesStart = DateTime.now(); - final Map> newClusterIdToFaceInfos = {}; - for (final faceInfo in newFaceInfos) { - if (newClusterIdToFaceInfos.containsKey(faceInfo.clusterId!)) { - newClusterIdToFaceInfos[faceInfo.clusterId!]!.add(faceInfo); - } else { - newClusterIdToFaceInfos[faceInfo.clusterId!] = [faceInfo]; - } - } - - final Map newClusterSummaries = {}; - for (final clusterId in newClusterIdToFaceInfos.keys) { - final List newEmbeddings = newClusterIdToFaceInfos[clusterId]! - .map((faceInfo) => faceInfo.vEmbedding!) - .toList(); - final newCount = newEmbeddings.length; - if (oldSummary.containsKey(clusterId)) { - final oldMean = Vector.fromList( - EVector.fromBuffer(oldSummary[clusterId]!.$1).values, - dtype: DType.float32, - ); - final oldCount = oldSummary[clusterId]!.$2; - final oldEmbeddings = oldMean * oldCount; - newEmbeddings.add(oldEmbeddings); - final newMeanVector = - newEmbeddings.reduce((a, b) => a + b) / (oldCount + newCount); - final newMeanVectorNormalized = newMeanVector / newMeanVector.norm(); - newClusterSummaries[clusterId] = ( - EVector(values: newMeanVectorNormalized.toList()).writeToBuffer(), - oldCount + newCount - ); - } else { - final newMeanVector = newEmbeddings.reduce((a, b) => a + b); - final newMeanVectorNormalized = newMeanVector / newMeanVector.norm(); - newClusterSummaries[clusterId] = ( - EVector(values: newMeanVectorNormalized.toList()).writeToBuffer(), - newCount - ); - } - } - log( - "[ClusterIsolate] ${DateTime.now()} Calculated cluster summaries in ${DateTime.now().difference(calcSummariesStart).inMilliseconds}ms", - ); - - return newClusterSummaries; - } - - static void _analyzeClusterResults(List sortedFaceInfos) { - if (!kDebugMode) return; - final stopwatch = Stopwatch()..start(); - - final Map faceIdToCluster = {}; - for (final faceInfo in sortedFaceInfos) { - faceIdToCluster[faceInfo.faceID] = faceInfo.clusterId!; - } - - // Find faceIDs that are part of a cluster which is larger than 5 and are new faceIDs - final Map clusterIdToSize = {}; - faceIdToCluster.forEach((key, value) { - if (clusterIdToSize.containsKey(value)) { - clusterIdToSize[value] = clusterIdToSize[value]! + 1; - } else { - clusterIdToSize[value] = 1; - } - }); - - // print top 10 cluster ids and their sizes based on the internal cluster id - final clusterIds = faceIdToCluster.values.toSet(); - final clusterSizes = clusterIds.map((clusterId) { - return faceIdToCluster.values.where((id) => id == clusterId).length; - }).toList(); - clusterSizes.sort(); - // find clusters whose size is greater than 1 - int oneClusterCount = 0; - int moreThan5Count = 0; - int moreThan10Count = 0; - int moreThan20Count = 0; - int moreThan50Count = 0; - int moreThan100Count = 0; - - for (int i = 0; i < clusterSizes.length; i++) { - if (clusterSizes[i] > 100) { - moreThan100Count++; - } else if (clusterSizes[i] > 50) { - moreThan50Count++; - } else if (clusterSizes[i] > 20) { - moreThan20Count++; - } else if (clusterSizes[i] > 10) { - moreThan10Count++; - } else if (clusterSizes[i] > 5) { - moreThan5Count++; - } else if (clusterSizes[i] == 1) { - oneClusterCount++; - } - } - - // print the metrics - log( - "[ClusterIsolate] Total clusters ${clusterIds.length}: \n oneClusterCount $oneClusterCount \n moreThan5Count $moreThan5Count \n moreThan10Count $moreThan10Count \n moreThan20Count $moreThan20Count \n moreThan50Count $moreThan50Count \n moreThan100Count $moreThan100Count", - ); - stopwatch.stop(); - log( - "[ClusterIsolate] Clustering additional analysis took ${stopwatch.elapsedMilliseconds} ms", - ); - } - - static ClusteringResult runCompleteClustering(Map args) { - final input = args['input'] as Map; - final fileIDToCreationTime = args['fileIDToCreationTime'] as Map?; - final distanceThreshold = args['distanceThreshold'] as double; - final mergeThreshold = args['mergeThreshold'] as double; - - log( - "[CompleteClustering] ${DateTime.now()} Copied to isolate ${input.length} faces for clustering", - ); - - // Organize everything into a list of FaceInfo objects - final List faceInfos = []; - for (final entry in input.entries) { - faceInfos.add( - FaceInfo( - faceID: entry.key, - vEmbedding: Vector.fromList( - EVector.fromBuffer(entry.value).values, - dtype: DType.float32, - ), - fileCreationTime: - fileIDToCreationTime?[getFileIdFromFaceId(entry.key)], - ), - ); - } - - if (fileIDToCreationTime != null) { - _sortFaceInfosOnCreationTime(faceInfos); - } - - if (faceInfos.isEmpty) { - ClusteringResult(newFaceIdToCluster: {}); - } - final int totalFaces = faceInfos.length; - - // Start actual clustering - log( - "[CompleteClustering] ${DateTime.now()} Processing $totalFaces faces in one single round of complete clustering", - ); - - // set current epoch time as clusterID - int clusterID = DateTime.now().microsecondsSinceEpoch; - - // Start actual clustering - final Map newFaceIdToCluster = {}; - final stopwatchClustering = Stopwatch()..start(); - for (int i = 0; i < totalFaces; i++) { - if ((i + 1) % 250 == 0) { - log("[CompleteClustering] ${DateTime.now()} Processed ${i + 1} faces"); - } - if (faceInfos[i].clusterId != null) continue; - int closestIdx = -1; - double closestDistance = double.infinity; - for (int j = 0; j < totalFaces; j++) { - if (i == j) continue; - final double distance = cosineDistanceSIMD( - faceInfos[i].vEmbedding!, - faceInfos[j].vEmbedding!, - ); - if (distance < closestDistance) { - closestDistance = distance; - closestIdx = j; + // WARNING: The loop below is now O(n^2) so be very careful with anything you put in there! + for (int j = i - 1; j >= 0; j--) { + final double distance = + 1 - sortedFaceInfos[i].vEmbedding!.dot(sortedFaceInfos[j].vEmbedding!); + if (distance < closestDistance) { + if (sortedFaceInfos[j].badFace! && distance > conservativeDistanceThreshold) { + continue; } + closestDistance = distance; + closestIdx = j; } + } - if (closestDistance < distanceThreshold) { - if (faceInfos[closestIdx].clusterId == null) { - clusterID++; - faceInfos[closestIdx].clusterId = clusterID; - } - faceInfos[i].clusterId = faceInfos[closestIdx].clusterId!; - } else { + if (closestDistance < thresholdValue) { + if (sortedFaceInfos[closestIdx].clusterId == null) { + // Ideally this should never happen, but just in case log it + log( + " [ClusterIsolate] [WARNING] ${DateTime.now()} Found new cluster $clusterID", + ); clusterID++; - faceInfos[i].clusterId = clusterID; + sortedFaceInfos[closestIdx].clusterId = clusterID; } + sortedFaceInfos[i].clusterId = sortedFaceInfos[closestIdx].clusterId; + } else { + clusterID++; + sortedFaceInfos[i].clusterId = clusterID; } + } - // Now calculate the mean of the embeddings for each cluster - final Map> clusterIdToFaceInfos = {}; - for (final faceInfo in faceInfos) { - if (clusterIdToFaceInfos.containsKey(faceInfo.clusterId)) { - clusterIdToFaceInfos[faceInfo.clusterId]!.add(faceInfo); - } else { - clusterIdToFaceInfos[faceInfo.clusterId!] = [faceInfo]; - } - } - final Map clusterIdToMeanEmbeddingAndWeight = {}; - for (final clusterId in clusterIdToFaceInfos.keys) { - final List embeddings = clusterIdToFaceInfos[clusterId]! - .map((faceInfo) => faceInfo.vEmbedding!) - .toList(); - final count = clusterIdToFaceInfos[clusterId]!.length; - final Vector meanEmbedding = embeddings.reduce((a, b) => a + b) / count; - final Vector meanEmbeddingNormalized = - meanEmbedding / meanEmbedding.norm(); - clusterIdToMeanEmbeddingAndWeight[clusterId] = - (meanEmbeddingNormalized, count); + // Finally, assign the new clusterId to the faces + final Map newFaceIdToCluster = {}; + final newClusteredFaceInfos = sortedFaceInfos.sublist(alreadyClusteredCount); + for (final faceInfo in newClusteredFaceInfos) { + newFaceIdToCluster[faceInfo.faceID] = faceInfo.clusterId!; + } + + // Create a map of clusterId to faceIds + final Map> clusterIdToFaceIds = {}; + for (final entry in newFaceIdToCluster.entries) { + final clusterID = entry.value; + if (clusterIdToFaceIds.containsKey(clusterID)) { + clusterIdToFaceIds[clusterID]!.add(entry.key); + } else { + clusterIdToFaceIds[clusterID] = [entry.key]; } + } - // Now merge the clusters that are close to each other, based on mean embedding - final List<(int, int)> mergedClustersList = []; - final List clusterIds = - clusterIdToMeanEmbeddingAndWeight.keys.toList(); - log(' [CompleteClustering] ${DateTime.now()} ${clusterIds.length} clusters found, now checking for merges'); - while (true) { - if (clusterIds.length < 2) break; - double distance = double.infinity; - (int, int) clusterIDsToMerge = (-1, -1); - for (int i = 0; i < clusterIds.length; i++) { - for (int j = 0; j < clusterIds.length; j++) { - if (i == j) continue; - final double newDistance = cosineDistanceSIMD( - clusterIdToMeanEmbeddingAndWeight[clusterIds[i]]!.$1, - clusterIdToMeanEmbeddingAndWeight[clusterIds[j]]!.$1, - ); - if (newDistance < distance) { - distance = newDistance; - clusterIDsToMerge = (clusterIds[i], clusterIds[j]); - } - } - } - if (distance < mergeThreshold) { - mergedClustersList.add(clusterIDsToMerge); - final clusterID1 = clusterIDsToMerge.$1; - final clusterID2 = clusterIDsToMerge.$2; - final mean1 = clusterIdToMeanEmbeddingAndWeight[clusterID1]!.$1; - final mean2 = clusterIdToMeanEmbeddingAndWeight[clusterID2]!.$1; - final count1 = clusterIdToMeanEmbeddingAndWeight[clusterID1]!.$2; - final count2 = clusterIdToMeanEmbeddingAndWeight[clusterID2]!.$2; - final weight1 = count1 / (count1 + count2); - final weight2 = count2 / (count1 + count2); - final weightedMean = mean1 * weight1 + mean2 * weight2; - final weightedMeanNormalized = weightedMean / weightedMean.norm(); - clusterIdToMeanEmbeddingAndWeight[clusterID1] = ( - weightedMeanNormalized, - count1 + count2, - ); - clusterIdToMeanEmbeddingAndWeight.remove(clusterID2); - clusterIds.remove(clusterID2); - } else { - break; - } - } - log(' [CompleteClustering] ${DateTime.now()} ${mergedClustersList.length} clusters merged'); - - // Now assign the new clusterId to the faces - for (final faceInfo in faceInfos) { - for (final mergedClusters in mergedClustersList) { - if (faceInfo.clusterId == mergedClusters.$2) { - faceInfo.clusterId = mergedClusters.$1; - } - } - } - - // Finally, assign the new clusterId to the faces - for (final faceInfo in faceInfos) { - newFaceIdToCluster[faceInfo.faceID] = faceInfo.clusterId!; - } - - final Map> clusterIdToFaceIds = {}; - for (final entry in newFaceIdToCluster.entries) { - final clusterID = entry.value; - if (clusterIdToFaceIds.containsKey(clusterID)) { - clusterIdToFaceIds[clusterID]!.add(entry.key); - } else { - clusterIdToFaceIds[clusterID] = [entry.key]; - } - } - - final newClusterSummaries = FaceClusteringService.updateClusterSummaries( - oldSummary: {}, - newFaceInfos: faceInfos, - ); - - stopwatchClustering.stop(); + stopwatchClustering.stop(); + log( + ' [ClusterIsolate] ${DateTime.now()} Clustering for ${sortedFaceInfos.length} embeddings executed in ${stopwatchClustering.elapsedMilliseconds}ms', + ); + if (useDynamicThreshold) { log( - ' [CompleteClustering] ${DateTime.now()} Clustering for ${faceInfos.length} embeddings executed in ${stopwatchClustering.elapsedMilliseconds}ms', - ); - - return ClusteringResult( - newFaceIdToCluster: newFaceIdToCluster, - newClusterSummaries: newClusterSummaries, - newClusterIdToFaceIds: clusterIdToFaceIds, + "[ClusterIsolate] ${DateTime.now()} Dynamic thresholding: $dynamicThresholdCount faces had a low face score or low blur clarity", ); } - static List> _runDbscanClustering(Map args) { - final input = args['input'] as Map; - final fileIDToCreationTime = args['fileIDToCreationTime'] as Map?; - final eps = args['eps'] as double; - final minPts = args['minPts'] as int; - - log( - "[ClusterIsolate] ${DateTime.now()} Copied to isolate ${input.length} faces", + // Now calculate the mean of the embeddings for each cluster and update the cluster summaries + final newClusterSummaries = _updateClusterSummaries( + newFaceInfos: newClusteredFaceInfos, + oldSummary: oldClusterSummaries, ); - final DBSCAN dbscan = DBSCAN( - epsilon: eps, - minPoints: minPts, - distanceMeasure: cosineDistForNormVectors, - ); + // analyze the results + // FaceClusteringService._analyzeClusterResults(sortedFaceInfos); - // Organize everything into a list of FaceInfo objects - final List faceInfos = []; - for (final entry in input.entries) { - faceInfos.add( - FaceInfo( - faceID: entry.key, - embedding: EVector.fromBuffer(entry.value).values, - fileCreationTime: - fileIDToCreationTime?[getFileIdFromFaceId(entry.key)], + return ClusteringResult( + newFaceIdToCluster: newFaceIdToCluster, + newClusterSummaries: newClusterSummaries, + newClusterIdToFaceIds: clusterIdToFaceIds, + ); +} + +ClusteringResult _runCompleteClustering(Map args) { + final input = args['input'] as Map; + final fileIDToCreationTime = args['fileIDToCreationTime'] as Map?; + final distanceThreshold = args['distanceThreshold'] as double; + final mergeThreshold = args['mergeThreshold'] as double; + final oldClusterSummaries = args['oldClusterSummaries'] as Map?; + + log( + "[CompleteClustering] ${DateTime.now()} Copied to isolate ${input.length} faces for clustering", + ); + + // Organize everything into a list of FaceInfo objects + final List faceInfos = []; + for (final entry in input.entries) { + faceInfos.add( + FaceInfo( + faceID: entry.key, + vEmbedding: Vector.fromList( + EVector.fromBuffer(entry.value).values, + dtype: DType.float32, ), - ); - } - - if (fileIDToCreationTime != null) { - _sortFaceInfosOnCreationTime(faceInfos); - } - - // Get the embeddings - final List> embeddings = - faceInfos.map((faceInfo) => faceInfo.embedding!).toList(); - - // Run the DBSCAN clustering - final List> clusterOutput = dbscan.run(embeddings); - // final List> clusteredFaceInfos = clusterOutput - // .map((cluster) => cluster.map((idx) => faceInfos[idx]).toList()) - // .toList(); - final List> clusteredFaceIDs = clusterOutput - .map((cluster) => cluster.map((idx) => faceInfos[idx].faceID).toList()) - .toList(); - - return clusteredFaceIDs; + fileCreationTime: fileIDToCreationTime?[getFileIdFromFaceId(entry.key)], + ), + ); } + + if (fileIDToCreationTime != null) { + _sortFaceInfosOnCreationTime(faceInfos); + } + + if (faceInfos.isEmpty) { + ClusteringResult.empty(); + } + final int totalFaces = faceInfos.length; + + // Start actual clustering + log( + "[CompleteClustering] ${DateTime.now()} Processing $totalFaces faces in one single round of complete clustering", + ); + + // set current epoch time as clusterID + int clusterID = DateTime.now().microsecondsSinceEpoch; + + // Start actual clustering + final Map newFaceIdToCluster = {}; + final stopwatchClustering = Stopwatch()..start(); + for (int i = 0; i < totalFaces; i++) { + if ((i + 1) % 250 == 0) { + log("[CompleteClustering] ${DateTime.now()} Processed ${i + 1} faces"); + } + if (faceInfos[i].clusterId != null) continue; + int closestIdx = -1; + double closestDistance = double.infinity; + for (int j = 0; j < totalFaces; j++) { + if (i == j) continue; + final double distance = 1 - faceInfos[i].vEmbedding!.dot(faceInfos[j].vEmbedding!); + if (distance < closestDistance) { + closestDistance = distance; + closestIdx = j; + } + } + + if (closestDistance < distanceThreshold) { + if (faceInfos[closestIdx].clusterId == null) { + clusterID++; + faceInfos[closestIdx].clusterId = clusterID; + } + faceInfos[i].clusterId = faceInfos[closestIdx].clusterId!; + } else { + clusterID++; + faceInfos[i].clusterId = clusterID; + } + } + + // Now calculate the mean of the embeddings for each cluster + final Map> clusterIdToFaceInfos = {}; + for (final faceInfo in faceInfos) { + if (clusterIdToFaceInfos.containsKey(faceInfo.clusterId)) { + clusterIdToFaceInfos[faceInfo.clusterId]!.add(faceInfo); + } else { + clusterIdToFaceInfos[faceInfo.clusterId!] = [faceInfo]; + } + } + final Map clusterIdToMeanEmbeddingAndWeight = {}; + for (final clusterId in clusterIdToFaceInfos.keys) { + final List embeddings = + clusterIdToFaceInfos[clusterId]!.map((faceInfo) => faceInfo.vEmbedding!).toList(); + final count = clusterIdToFaceInfos[clusterId]!.length; + final Vector meanEmbedding = embeddings.reduce((a, b) => a + b) / count; + final Vector meanEmbeddingNormalized = meanEmbedding / meanEmbedding.norm(); + clusterIdToMeanEmbeddingAndWeight[clusterId] = (meanEmbeddingNormalized, count); + } + + // Now merge the clusters that are close to each other, based on mean embedding + final List<(int, int)> mergedClustersList = []; + final List clusterIds = clusterIdToMeanEmbeddingAndWeight.keys.toList(); + log(' [CompleteClustering] ${DateTime.now()} ${clusterIds.length} clusters found, now checking for merges'); + while (true) { + if (clusterIds.length < 2) break; + double distance = double.infinity; + (int, int) clusterIDsToMerge = (-1, -1); + for (int i = 0; i < clusterIds.length; i++) { + for (int j = 0; j < clusterIds.length; j++) { + if (i == j) continue; + final double newDistance = 1 - + clusterIdToMeanEmbeddingAndWeight[clusterIds[i]]! + .$1 + .dot(clusterIdToMeanEmbeddingAndWeight[clusterIds[j]]!.$1); + if (newDistance < distance) { + distance = newDistance; + clusterIDsToMerge = (clusterIds[i], clusterIds[j]); + } + } + } + if (distance < mergeThreshold) { + mergedClustersList.add(clusterIDsToMerge); + final clusterID1 = clusterIDsToMerge.$1; + final clusterID2 = clusterIDsToMerge.$2; + final mean1 = clusterIdToMeanEmbeddingAndWeight[clusterID1]!.$1; + final mean2 = clusterIdToMeanEmbeddingAndWeight[clusterID2]!.$1; + final count1 = clusterIdToMeanEmbeddingAndWeight[clusterID1]!.$2; + final count2 = clusterIdToMeanEmbeddingAndWeight[clusterID2]!.$2; + final weight1 = count1 / (count1 + count2); + final weight2 = count2 / (count1 + count2); + final weightedMean = mean1 * weight1 + mean2 * weight2; + final weightedMeanNormalized = weightedMean / weightedMean.norm(); + clusterIdToMeanEmbeddingAndWeight[clusterID1] = ( + weightedMeanNormalized, + count1 + count2, + ); + clusterIdToMeanEmbeddingAndWeight.remove(clusterID2); + clusterIds.remove(clusterID2); + } else { + break; + } + } + log(' [CompleteClustering] ${DateTime.now()} ${mergedClustersList.length} clusters merged'); + + // Now assign the new clusterId to the faces + for (final faceInfo in faceInfos) { + for (final mergedClusters in mergedClustersList) { + if (faceInfo.clusterId == mergedClusters.$2) { + faceInfo.clusterId = mergedClusters.$1; + } + } + } + + // Finally, assign the new clusterId to the faces + for (final faceInfo in faceInfos) { + newFaceIdToCluster[faceInfo.faceID] = faceInfo.clusterId!; + } + + final Map> clusterIdToFaceIds = {}; + for (final entry in newFaceIdToCluster.entries) { + final clusterID = entry.value; + if (clusterIdToFaceIds.containsKey(clusterID)) { + clusterIdToFaceIds[clusterID]!.add(entry.key); + } else { + clusterIdToFaceIds[clusterID] = [entry.key]; + } + } + + // Now calculate the mean of the embeddings for each cluster and update the cluster summaries + final newClusterSummaries = _updateClusterSummaries( + newFaceInfos: faceInfos, + oldSummary: oldClusterSummaries, + ); + + stopwatchClustering.stop(); + log( + ' [CompleteClustering] ${DateTime.now()} Clustering for ${faceInfos.length} embeddings executed in ${stopwatchClustering.elapsedMilliseconds}ms', + ); + + return ClusteringResult( + newFaceIdToCluster: newFaceIdToCluster, + newClusterSummaries: newClusterSummaries, + newClusterIdToFaceIds: clusterIdToFaceIds, + ); } /// Sort the faceInfos based on fileCreationTime, in descending order, so newest faces are first @@ -1011,3 +782,107 @@ void _sortFaceInfosOnCreationTime( } }); } + +Map _updateClusterSummaries({ + required List newFaceInfos, + Map? oldSummary, +}) { + final calcSummariesStart = DateTime.now(); + final Map> newClusterIdToFaceInfos = {}; + for (final faceInfo in newFaceInfos) { + if (newClusterIdToFaceInfos.containsKey(faceInfo.clusterId!)) { + newClusterIdToFaceInfos[faceInfo.clusterId!]!.add(faceInfo); + } else { + newClusterIdToFaceInfos[faceInfo.clusterId!] = [faceInfo]; + } + } + + final Map newClusterSummaries = {}; + for (final clusterId in newClusterIdToFaceInfos.keys) { + final List newEmbeddings = + newClusterIdToFaceInfos[clusterId]!.map((faceInfo) => faceInfo.vEmbedding!).toList(); + final newCount = newEmbeddings.length; + if (oldSummary != null && oldSummary.containsKey(clusterId)) { + final oldMean = Vector.fromList( + EVector.fromBuffer(oldSummary[clusterId]!.$1).values, + dtype: DType.float32, + ); + final oldCount = oldSummary[clusterId]!.$2; + final oldEmbeddings = oldMean * oldCount; + newEmbeddings.add(oldEmbeddings); + final newMeanVector = newEmbeddings.reduce((a, b) => a + b) / (oldCount + newCount); + final newMeanVectorNormalized = newMeanVector / newMeanVector.norm(); + newClusterSummaries[clusterId] = + (EVector(values: newMeanVectorNormalized.toList()).writeToBuffer(), oldCount + newCount); + } else { + final newMeanVector = newEmbeddings.reduce((a, b) => a + b); + final newMeanVectorNormalized = newMeanVector / newMeanVector.norm(); + newClusterSummaries[clusterId] = + (EVector(values: newMeanVectorNormalized.toList()).writeToBuffer(), newCount); + } + } + log( + "[ClusterIsolate] ${DateTime.now()} Calculated cluster summaries in ${DateTime.now().difference(calcSummariesStart).inMilliseconds}ms", + ); + + return newClusterSummaries; +} + +void _analyzeClusterResults(List sortedFaceInfos) { + if (!kDebugMode) return; + final stopwatch = Stopwatch()..start(); + + final Map faceIdToCluster = {}; + for (final faceInfo in sortedFaceInfos) { + faceIdToCluster[faceInfo.faceID] = faceInfo.clusterId!; + } + + // Find faceIDs that are part of a cluster which is larger than 5 and are new faceIDs + final Map clusterIdToSize = {}; + faceIdToCluster.forEach((key, value) { + if (clusterIdToSize.containsKey(value)) { + clusterIdToSize[value] = clusterIdToSize[value]! + 1; + } else { + clusterIdToSize[value] = 1; + } + }); + + // print top 10 cluster ids and their sizes based on the internal cluster id + final clusterIds = faceIdToCluster.values.toSet(); + final clusterSizes = clusterIds.map((clusterId) { + return faceIdToCluster.values.where((id) => id == clusterId).length; + }).toList(); + clusterSizes.sort(); + // find clusters whose size is greater than 1 + int oneClusterCount = 0; + int moreThan5Count = 0; + int moreThan10Count = 0; + int moreThan20Count = 0; + int moreThan50Count = 0; + int moreThan100Count = 0; + + for (int i = 0; i < clusterSizes.length; i++) { + if (clusterSizes[i] > 100) { + moreThan100Count++; + } else if (clusterSizes[i] > 50) { + moreThan50Count++; + } else if (clusterSizes[i] > 20) { + moreThan20Count++; + } else if (clusterSizes[i] > 10) { + moreThan10Count++; + } else if (clusterSizes[i] > 5) { + moreThan5Count++; + } else if (clusterSizes[i] == 1) { + oneClusterCount++; + } + } + + // print the metrics + log( + "[ClusterIsolate] Total clusters ${clusterIds.length}: \n oneClusterCount $oneClusterCount \n moreThan5Count $moreThan5Count \n moreThan10Count $moreThan10Count \n moreThan20Count $moreThan20Count \n moreThan50Count $moreThan50Count \n moreThan100Count $moreThan100Count", + ); + stopwatch.stop(); + log( + "[ClusterIsolate] Clustering additional analysis took ${stopwatch.elapsedMilliseconds} ms", + ); +} diff --git a/mobile/lib/services/machine_learning/face_ml/face_filtering/face_filtering_constants.dart b/mobile/lib/services/machine_learning/face_ml/face_filtering/face_filtering_constants.dart index b0f954f8f9..64ca82dd97 100644 --- a/mobile/lib/services/machine_learning/face_ml/face_filtering/face_filtering_constants.dart +++ b/mobile/lib/services/machine_learning/face_ml/face_filtering/face_filtering_constants.dart @@ -17,4 +17,4 @@ const kHighQualityFaceScore = 0.90; const kMinFaceDetectionScore = FaceDetectionService.kMinScoreSigmoidThreshold; /// The minimum cluster size for displaying a cluster in the UI -const kMinimumClusterSizeSearchResult = 20; +const kMinimumClusterSizeSearchResult = 10; diff --git a/mobile/lib/services/machine_learning/face_ml/face_ml_service.dart b/mobile/lib/services/machine_learning/face_ml/face_ml_service.dart index bbe719dbe1..87a707995c 100644 --- a/mobile/lib/services/machine_learning/face_ml/face_ml_service.dart +++ b/mobile/lib/services/machine_learning/face_ml/face_ml_service.dart @@ -43,6 +43,7 @@ import 'package:photos/services/machine_learning/face_ml/face_ml_result.dart'; import "package:photos/services/machine_learning/face_ml/person/person_service.dart"; import 'package:photos/services/machine_learning/file_ml/file_ml.dart'; import 'package:photos/services/machine_learning/file_ml/remote_fileml_service.dart'; +import "package:photos/services/machine_learning/machine_learning_controller.dart"; import "package:photos/services/search_service.dart"; import "package:photos/utils/file_util.dart"; import 'package:photos/utils/image_ml_isolate.dart'; @@ -90,7 +91,10 @@ class FaceMlService { bool isInitialized = false; late String client; + bool get showClusteringIsHappening => _showClusteringIsHappening; + bool debugIndexingDisabled = false; + bool _showClusteringIsHappening = false; bool _mlControllerStatus = false; bool _isIndexingOrClusteringRunning = false; bool _shouldPauseIndexingAndClustering = false; @@ -99,7 +103,7 @@ class FaceMlService { final int _fileDownloadLimit = 5; final int _embeddingFetchLimit = 200; - final int _kForceClusteringFaceCount = 4000; + final int _kForceClusteringFaceCount = 8000; Future init({bool initializeImageMlIsolate = false}) async { if (LocalSettings.instance.isFaceIndexingEnabled == false) { @@ -163,9 +167,15 @@ class FaceMlService { pauseIndexingAndClustering(); } }); + if (Platform.isIOS && MachineLearningController.instance.isDeviceHealthy) { + _logger.info("Starting face indexing and clustering on iOS from init"); + unawaited(indexAndClusterAll()); + } _listenIndexOnDiffSync(); _listenOnPeopleChangedSync(); + + _logger.info('init done'); }); } @@ -262,8 +272,7 @@ class FaceMlService { switch (function) { case FaceMlOperation.analyzeImage: final time = DateTime.now(); - final FaceMlResult result = - await FaceMlService.analyzeImageSync(args); + final FaceMlResult result = await FaceMlService.analyzeImageSync(args); dev.log( "`analyzeImageSync` function executed in ${DateTime.now().difference(time).inMilliseconds} ms", ); @@ -276,8 +285,7 @@ class FaceMlService { error: e, stackTrace: stackTrace, ); - sendPort - .send({'error': e.toString(), 'stackTrace': stackTrace.toString()}); + sendPort.send({'error': e.toString(), 'stackTrace': stackTrace.toString()}); } }); } @@ -350,6 +358,7 @@ class FaceMlService { _isSyncing = true; if (forceSync) { await PersonService.instance.reconcileClusters(); + Bus.instance.fire(PeopleChangedEvent()); _shouldSyncPeople = false; } _isSyncing = false; @@ -360,8 +369,7 @@ class FaceMlService { await sync(forceSync: _shouldSyncPeople); - final int unclusteredFacesCount = - await FaceMLDataDB.instance.getUnclusteredFaceCount(); + final int unclusteredFacesCount = await FaceMLDataDB.instance.getUnclusteredFaceCount(); if (unclusteredFacesCount > _kForceClusteringFaceCount) { _logger.info( "There are $unclusteredFacesCount unclustered faces, doing clustering first", @@ -388,13 +396,10 @@ class FaceMlService { _isIndexingOrClusteringRunning = true; _logger.info('starting image indexing'); - final w = (kDebugMode ? EnteWatch('prepare indexing files') : null) - ?..start(); - final Map alreadyIndexedFiles = - await FaceMLDataDB.instance.getIndexedFileIds(); + final w = (kDebugMode ? EnteWatch('prepare indexing files') : null)?..start(); + final Map alreadyIndexedFiles = await FaceMLDataDB.instance.getIndexedFileIds(); w?.log('getIndexedFileIds'); - final List enteFiles = - await SearchService.instance.getAllFiles(); + final List enteFiles = await SearchService.instance.getAllFiles(); w?.log('getAllFiles'); // Make sure the image conversion isolate is spawned @@ -421,8 +426,7 @@ class FaceMlService { } } w?.log('sifting through all normal files'); - final List hiddenFiles = - await SearchService.instance.getHiddenFiles(); + final List hiddenFiles = await SearchService.instance.getHiddenFiles(); w?.log('getHiddenFiles: ${hiddenFiles.length} hidden files'); for (final EnteFile enteFile in hiddenFiles) { if (_skipAnalysisEnteFile(enteFile, alreadyIndexedFiles)) { @@ -438,8 +442,7 @@ class FaceMlService { sortedBylocalID.addAll(filesWithoutLocalID); sortedBylocalID.addAll(hiddenFilesToIndex); w?.log('preparing all files to index'); - final List> chunks = - sortedBylocalID.chunks(_embeddingFetchLimit); + final List> chunks = sortedBylocalID.chunks(_embeddingFetchLimit); int fetchedCount = 0; outerLoop: for (final chunk in chunks) { @@ -447,15 +450,13 @@ class FaceMlService { if (LocalSettings.instance.remoteFetchEnabled) { try { - final Set fileIds = - {}; // if there are duplicates here server returns 400 + final Set fileIds = {}; // if there are duplicates here server returns 400 // Try to find embeddings on the remote server for (final f in chunk) { fileIds.add(f.uploadedFileID!); } _logger.info('starting remote fetch for ${fileIds.length} files'); - final res = - await RemoteFileMLService.instance.getFilessEmbedding(fileIds); + final res = await RemoteFileMLService.instance.getFilessEmbedding(fileIds); _logger.info('fetched ${res.mlData.length} embeddings'); fetchedCount += res.mlData.length; final List faces = []; @@ -477,8 +478,7 @@ class FaceMlService { faces.add(f); } } - remoteFileIdToVersion[fileMl.fileID] = - fileMl.faceEmbedding.version; + remoteFileIdToVersion[fileMl.fileID] = fileMl.faceEmbedding.version; } if (res.noEmbeddingFileIDs.isNotEmpty) { _logger.info( @@ -495,8 +495,7 @@ class FaceMlService { for (final entry in remoteFileIdToVersion.entries) { alreadyIndexedFiles[entry.key] = entry.value; } - _logger - .info('already indexed files ${remoteFileIdToVersion.length}'); + _logger.info('already indexed files ${remoteFileIdToVersion.length}'); } catch (e, s) { _logger.severe("err while getting files embeddings", e, s); if (retryFetchCount < 1000) { @@ -572,12 +571,16 @@ class FaceMlService { _isIndexingOrClusteringRunning = true; final clusterAllImagesTime = DateTime.now(); + _logger.info('Pulling remote feedback before actually clustering'); + await PersonService.instance.fetchRemoteClusterFeedback(); + try { + _showClusteringIsHappening = true; + // Get a sense of the total number of faces in the database - final int totalFaces = await FaceMLDataDB.instance - .getTotalFaceCount(minFaceScore: minFaceScore); - final fileIDToCreationTime = - await FilesDB.instance.getFileIDToCreationTime(); + final int totalFaces = + await FaceMLDataDB.instance.getTotalFaceCount(minFaceScore: minFaceScore); + final fileIDToCreationTime = await FilesDB.instance.getFileIDToCreationTime(); final startEmbeddingFetch = DateTime.now(); // read all embeddings final result = await FaceMLDataDB.instance.getFaceInfoForClustering( @@ -595,8 +598,7 @@ class FaceMlService { } // sort the embeddings based on file creation time, newest first allFaceInfoForClustering.sort((b, a) { - return fileIDToCreationTime[a.fileID]! - .compareTo(fileIDToCreationTime[b.fileID]!); + return fileIDToCreationTime[a.fileID]!.compareTo(fileIDToCreationTime[b.fileID]!); }); _logger.info( 'Getting and sorting embeddings took ${DateTime.now().difference(startEmbeddingFetch).inMilliseconds} ms for ${allFaceInfoForClustering.length} embeddings' @@ -608,7 +610,7 @@ class FaceMlService { await FaceMLDataDB.instance.getAllClusterSummary(); if (clusterInBuckets) { - const int bucketSize = 20000; + const int bucketSize = 10000; const int offsetIncrement = 7500; int offset = 0; int bucket = 1; @@ -639,8 +641,20 @@ class FaceMlService { min(offset + bucketSize, allFaceInfoForClustering.length), ); - final clusteringResult = - await FaceClusteringService.instance.predictLinear( + if (faceInfoForClustering.every((face) => face.clusterId != null)) { + _logger.info('Everything in bucket $bucket is already clustered'); + if (offset + bucketSize >= totalFaces) { + _logger.info('All faces clustered'); + break; + } else { + _logger.info('Skipping to next bucket'); + offset += offsetIncrement; + bucket++; + continue; + } + } + + final clusteringResult = await FaceClusteringService.instance.predictLinearIsolate( faceInfoForClustering.toSet(), fileIDToCreationTime: fileIDToCreationTime, offset: offset, @@ -651,16 +665,13 @@ class FaceMlService { return; } - await FaceMLDataDB.instance - .updateFaceIdToClusterId(clusteringResult.newFaceIdToCluster); - await FaceMLDataDB.instance - .clusterSummaryUpdate(clusteringResult.newClusterSummaries!); + await FaceMLDataDB.instance.updateFaceIdToClusterId(clusteringResult.newFaceIdToCluster); + await FaceMLDataDB.instance.clusterSummaryUpdate(clusteringResult.newClusterSummaries); + Bus.instance.fire(PeopleChangedEvent()); for (final faceInfo in faceInfoForClustering) { - faceInfo.clusterId ??= - clusteringResult.newFaceIdToCluster[faceInfo.faceID]; + faceInfo.clusterId ??= clusteringResult.newFaceIdToCluster[faceInfo.faceID]; } - for (final clusterUpdate - in clusteringResult.newClusterSummaries!.entries) { + for (final clusterUpdate in clusteringResult.newClusterSummaries.entries) { oldClusterSummaries[clusterUpdate.key] = clusterUpdate.value; } _logger.info( @@ -676,8 +687,7 @@ class FaceMlService { } else { final clusterStartTime = DateTime.now(); // Cluster the embeddings using the linear clustering algorithm, returning a map from faceID to clusterID - final clusteringResult = - await FaceClusteringService.instance.predictLinear( + final clusteringResult = await FaceClusteringService.instance.predictLinearIsolate( allFaceInfoForClustering.toSet(), fileIDToCreationTime: fileIDToCreationTime, oldClusterSummaries: oldClusterSummaries, @@ -695,19 +705,18 @@ class FaceMlService { _logger.info( 'Updating ${clusteringResult.newFaceIdToCluster.length} FaceIDs with clusterIDs in the DB', ); - await FaceMLDataDB.instance - .updateFaceIdToClusterId(clusteringResult.newFaceIdToCluster); - await FaceMLDataDB.instance - .clusterSummaryUpdate(clusteringResult.newClusterSummaries!); + await FaceMLDataDB.instance.updateFaceIdToClusterId(clusteringResult.newFaceIdToCluster); + await FaceMLDataDB.instance.clusterSummaryUpdate(clusteringResult.newClusterSummaries); + Bus.instance.fire(PeopleChangedEvent()); _logger.info('Done updating FaceIDs with clusterIDs in the DB, in ' '${DateTime.now().difference(clusterDoneTime).inSeconds} seconds'); } - Bus.instance.fire(PeopleChangedEvent()); _logger.info('clusterAllImages() finished, in ' '${DateTime.now().difference(clusterAllImagesTime).inSeconds} seconds'); } catch (e, s) { _logger.severe("`clusterAllImages` failed", e, s); } finally { + _showClusteringIsHappening = false; _isIndexingOrClusteringRunning = false; _shouldPauseIndexingAndClustering = false; } @@ -730,8 +739,7 @@ class FaceMlService { allLandmarksEqual = false; break; } - if (face.detection.landmarks - .any((landmark) => landmark.x != landmark.y)) { + if (face.detection.landmarks.any((landmark) => landmark.x != landmark.y)) { allLandmarksEqual = false; break; } @@ -740,10 +748,7 @@ class FaceMlService { debugPrint("Discarding remote embedding for fileID ${fileMl.fileID} " "because landmarks are equal"); debugPrint( - fileMl.faceEmbedding.faces - .map((e) => e.detection.landmarks.toString()) - .toList() - .toString(), + fileMl.faceEmbedding.faces.map((e) => e.detection.landmarks.toString()).toList().toString(), ); return true; } @@ -781,11 +786,9 @@ class FaceMlService { Face.empty(result.fileId, error: result.errorOccured), ); } else { - if (result.decodedImageSize.width == -1 || - result.decodedImageSize.height == -1) { - _logger - .severe("decodedImageSize is not stored correctly for image with " - "ID: ${enteFile.uploadedFileID}"); + if (result.decodedImageSize.width == -1 || result.decodedImageSize.height == -1) { + _logger.severe("decodedImageSize is not stored correctly for image with " + "ID: ${enteFile.uploadedFileID}"); _logger.info( "Using aligned image size for image with ID: ${enteFile.uploadedFileID}. This size is ${result.decodedImageSize.width}x${result.decodedImageSize.height} compared to size of ${enteFile.width}x${enteFile.height} in the metadata", ); @@ -857,7 +860,7 @@ class FaceMlService { return true; } catch (e, s) { _logger.severe( - "Failed to analyze using FaceML for image with ID: ${enteFile.uploadedFileID}", + "Failed to analyze using FaceML for image with ID: ${enteFile.uploadedFileID}. Not storing any faces, which means it will be automatically retried later.", e, s, ); @@ -870,12 +873,11 @@ class FaceMlService { _checkEnteFileForID(enteFile); await ensureInitialized(); - final String? filePath = - await _getImagePathForML(enteFile, typeOfData: FileDataForML.fileData); + final String? filePath = await _getImagePathForML(enteFile, typeOfData: FileDataForML.fileData); if (filePath == null) { _logger.severe( - "Failed to get any data for enteFile with uploadedFileID ${enteFile.uploadedFileID}", + "Failed to get any data for enteFile with uploadedFileID ${enteFile.uploadedFileID} since its file path is null", ); throw CouldNotRetrieveAnyFileData(); } @@ -890,10 +892,8 @@ class FaceMlService { { "enteFileID": enteFile.uploadedFileID ?? -1, "filePath": filePath, - "faceDetectionAddress": - FaceDetectionService.instance.sessionAddress, - "faceEmbeddingAddress": - FaceEmbeddingService.instance.sessionAddress, + "faceDetectionAddress": FaceDetectionService.instance.sessionAddress, + "faceEmbeddingAddress": FaceEmbeddingService.instance.sessionAddress, } ), ) as String?; @@ -947,8 +947,7 @@ class FaceMlService { stopwatch.reset(); // Get the faces - final List faceDetectionResult = - await FaceMlService.detectFacesSync( + final List faceDetectionResult = await FaceMlService.detectFacesSync( image, imgByteData, faceDetectionAddress, @@ -969,8 +968,7 @@ class FaceMlService { stopwatch.reset(); // Align the faces - final Float32List faceAlignmentResult = - await FaceMlService.alignFacesSync( + final Float32List faceAlignmentResult = await FaceMlService.alignFacesSync( image, imgByteData, faceDetectionResult, @@ -1016,17 +1014,29 @@ class FaceMlService { File? file; if (enteFile.fileType == FileType.video) { try { - file = await getThumbnailForUploadedFile(enteFile); + file = await getThumbnailForUploadedFile(enteFile); } on PlatformException catch (e, s) { - _logger.severe("Could not get thumbnail for $enteFile due to PlatformException", e, s); + _logger.severe( + "Could not get thumbnail for $enteFile due to PlatformException", + e, + s, + ); throw ThumbnailRetrievalException(e.toString(), s); } } else { - file = await getFile(enteFile, isOrigin: true); - // TODO: This is returning null for Pragadees for all files, so something is wrong here! + try { + file = await getFile(enteFile, isOrigin: true); + } catch (e, s) { + _logger.severe( + "Could not get file for $enteFile", + e, + s, + ); + } } if (file == null) { - _logger.warning("Could not get file for $enteFile"); + _logger + .warning("Could not get file for $enteFile of type ${enteFile.fileType.toString()}"); imagePath = null; break; } @@ -1078,8 +1088,7 @@ class FaceMlService { }) async { try { // Get the bounding boxes of the faces - final (List faces, dataSize) = - await FaceDetectionService.predictSync( + final (List faces, dataSize) = await FaceDetectionService.predictSync( image, imageByteData, interpreterAddress, @@ -1178,19 +1187,18 @@ class FaceMlService { /// Checks if the ente file to be analyzed actually can be analyzed: it must be uploaded and in the correct format. void _checkEnteFileForID(EnteFile enteFile) { if (_skipAnalysisEnteFile(enteFile, {})) { - _logger.warning( - '''Skipped analysis of image with enteFile, it might be the wrong format or has no uploadedFileID, or MLController doesn't allow it to run. + final String logString = + '''Skipped analysis of image with enteFile, it might be the wrong format or has no uploadedFileID, or MLController doesn't allow it to run. enteFile: ${enteFile.toString()} - ''', - ); + '''; + _logger.warning(logString); _logStatus(); - throw CouldNotRetrieveAnyFileData(); + throw GeneralFaceMlException(logString); } } bool _skipAnalysisEnteFile(EnteFile enteFile, Map indexedFileIds) { - if (_isIndexingOrClusteringRunning == false || - _mlControllerStatus == false) { + if (_isIndexingOrClusteringRunning == false || _mlControllerStatus == false) { return true; } // Skip if the file is not uploaded or not owned by the user @@ -1204,8 +1212,7 @@ class FaceMlService { // Skip if the file is already analyzed with the latest ml version final id = enteFile.uploadedFileID!; - return indexedFileIds.containsKey(id) && - indexedFileIds[id]! >= faceMlVersion; + return indexedFileIds.containsKey(id) && indexedFileIds[id]! >= faceMlVersion; } bool _cannotRunMLFunction({String function = ""}) { diff --git a/mobile/lib/services/machine_learning/face_ml/feedback/cluster_feedback.dart b/mobile/lib/services/machine_learning/face_ml/feedback/cluster_feedback.dart index 8567e88685..6ca2c33dc9 100644 --- a/mobile/lib/services/machine_learning/face_ml/feedback/cluster_feedback.dart +++ b/mobile/lib/services/machine_learning/face_ml/feedback/cluster_feedback.dart @@ -13,7 +13,6 @@ import "package:photos/face/db.dart"; import "package:photos/face/model/person.dart"; import "package:photos/generated/protos/ente/common/vector.pb.dart"; import "package:photos/models/file/file.dart"; -import "package:photos/services/machine_learning/face_ml/face_clustering/cosine_distance.dart"; import "package:photos/services/machine_learning/face_ml/face_clustering/face_clustering_service.dart"; import "package:photos/services/machine_learning/face_ml/face_filtering/face_filtering_constants.dart"; import "package:photos/services/machine_learning/face_ml/face_ml_result.dart"; @@ -118,7 +117,11 @@ class ClusterFeedbackService { final sortingStartTime = DateTime.now(); if (extremeFilesFirst) { - await _sortSuggestionsOnDistanceToPerson(person, finalSuggestions); + try { + await _sortSuggestionsOnDistanceToPerson(person, finalSuggestions); + } catch (e, s) { + _logger.severe("Error in sorting suggestions", e, s); + } } _logger.info( 'getSuggestionForPerson post-processing suggestions took ${DateTime.now().difference(findSuggestionsTime).inMilliseconds} ms, of which sorting took ${DateTime.now().difference(sortingStartTime).inMilliseconds} ms and getting files took ${getFilesTime.difference(findSuggestionsTime).inMilliseconds} ms', @@ -157,7 +160,8 @@ class ClusterFeedbackService { fileIDToCreationTime: fileIDToCreationTime, distanceThreshold: 0.20, ); - if (clusterResult == null || clusterResult.isEmpty) { + if (clusterResult.isEmpty) { + _logger.warning('No clusters found or something went wrong'); return; } final newFaceIdToClusterID = clusterResult.newFaceIdToCluster; @@ -165,7 +169,7 @@ class ClusterFeedbackService { // Update the deleted faces await FaceMLDataDB.instance.forceUpdateClusterIds(newFaceIdToClusterID); await FaceMLDataDB.instance - .clusterSummaryUpdate(clusterResult.newClusterSummaries!); + .clusterSummaryUpdate(clusterResult.newClusterSummaries); // Make sure the deleted faces don't get suggested in the future final notClusterIdToPersonId = {}; @@ -209,7 +213,7 @@ class ClusterFeedbackService { fileIDToCreationTime: fileIDToCreationTime, distanceThreshold: 0.20, ); - if (clusterResult == null || clusterResult.isEmpty) { + if (clusterResult.isEmpty) { return; } final newFaceIdToClusterID = clusterResult.newFaceIdToCluster; @@ -217,7 +221,7 @@ class ClusterFeedbackService { // Update the deleted faces await FaceMLDataDB.instance.forceUpdateClusterIds(newFaceIdToClusterID); await FaceMLDataDB.instance - .clusterSummaryUpdate(clusterResult.newClusterSummaries!); + .clusterSummaryUpdate(clusterResult.newClusterSummaries); Bus.instance.fire( PeopleChangedEvent( @@ -345,9 +349,7 @@ class ClusterFeedbackService { distanceThreshold: 0.22, ); - if (clusterResult == null || - clusterResult.newClusterIdToFaceIds == null || - clusterResult.isEmpty) { + if (clusterResult.isEmpty) { _logger.warning( '[CheckMixedClusters] Clustering did not seem to work for cluster $clusterID of size ${allClusterToFaceCount[clusterID]}', ); @@ -355,7 +357,7 @@ class ClusterFeedbackService { } final newClusterIdToCount = - clusterResult.newClusterIdToFaceIds!.map((key, value) { + clusterResult.newClusterIdToFaceIds.map((key, value) { return MapEntry(key, value.length); }); final amountOfNewClusters = newClusterIdToCount.length; @@ -424,6 +426,11 @@ class ClusterFeedbackService { final embeddings = await faceMlDb.getFaceEmbeddingMapForFaces(faceIDs); + if (embeddings.isEmpty) { + _logger.warning('No embeddings found for cluster $clusterID'); + return ClusteringResult.empty(); + } + final fileIDToCreationTime = await FilesDB.instance.getFileIDToCreationTime(); @@ -434,13 +441,13 @@ class ClusterFeedbackService { distanceThreshold: 0.22, ); - if (clusterResult == null || clusterResult.newClusterIdToFaceIds == null || clusterResult.isEmpty) { + if (clusterResult.isEmpty) { _logger.warning('No clusters found or something went wrong'); - return ClusteringResult(newFaceIdToCluster: {}); + return ClusteringResult.empty(); } final clusterIdToCount = - clusterResult.newClusterIdToFaceIds!.map((key, value) { + clusterResult.newClusterIdToFaceIds.map((key, value) { return MapEntry(key, value.length); }); final amountOfNewClusters = clusterIdToCount.length; @@ -452,7 +459,7 @@ class ClusterFeedbackService { if (kDebugMode) { final Set allClusteredFaceIDsSet = {}; for (final List value - in clusterResult.newClusterIdToFaceIds!.values) { + in clusterResult.newClusterIdToFaceIds.values) { allClusteredFaceIDsSet.addAll(value); } assert((originalFaceIDsSet.difference(allClusteredFaceIDsSet)).isEmpty); @@ -537,8 +544,7 @@ class ClusterFeedbackService { EVector.fromBuffer(clusterSummary.$1).values, dtype: DType.float32, ); - final bigClustersMeanDistance = - cosineDistanceSIMD(biggestMean, currentMean); + final bigClustersMeanDistance = 1 - biggestMean.dot(currentMean); _logger.info( "Mean distance between biggest cluster and current cluster: $bigClustersMeanDistance", ); @@ -595,8 +601,7 @@ class ClusterFeedbackService { final List trueDistances = []; for (final biggestEmbedding in biggestSampledEmbeddings) { for (final currentEmbedding in currentSampledEmbeddings) { - distances - .add(cosineDistanceSIMD(biggestEmbedding, currentEmbedding)); + distances.add(1 - biggestEmbedding.dot(currentEmbedding)); trueDistances.add( biggestEmbedding.distanceTo( currentEmbedding, @@ -686,7 +691,7 @@ class ClusterFeedbackService { clusterAvgBigClusters, personClusters, ignoredClusters, - (minimumSize == 100) ? goodMeanDistance + 0.15 : goodMeanDistance, + goodMeanDistance, ); w?.log( 'Calculate suggestions using mean for ${clusterAvgBigClusters.length} clusters of min size $minimumSize', @@ -789,7 +794,7 @@ class ClusterFeedbackService { final List distances = []; for (final otherEmbedding in sampledOtherEmbeddings) { for (final embedding in sampledEmbeddings) { - distances.add(cosineDistanceSIMD(embedding, otherEmbedding)); + distances.add(1 - embedding.dot(otherEmbedding)); } } distances.sort(); @@ -1041,11 +1046,25 @@ class ClusterFeedbackService { await faceMlDb.getClusterToClusterSummary(personClusters); final clusterSummaryCallTime = DateTime.now(); + // remove personClusters that don't have any summary + for (final clusterID in personClusters.toSet()) { + if (!personClusterToSummary.containsKey(clusterID)) { + _logger.warning('missing summary for $clusterID'); + personClusters.remove(clusterID); + } + } + if (personClusters.isEmpty) { + _logger.warning('No person clusters with summary found'); + return; + } + // Calculate the avg embedding of the person final w = (kDebugMode ? EnteWatch('sortSuggestions') : null)?..start(); - final personEmbeddingsCount = personClusters - .map((e) => personClusterToSummary[e]!.$2) - .reduce((a, b) => a + b); + int personEmbeddingsCount = 0; + for (final clusterID in personClusters) { + personEmbeddingsCount += personClusterToSummary[clusterID]!.$2; + } + Vector personAvg = Vector.filled(192, 0); for (final personClusterID in personClusters) { final personClusterBlob = personClusterToSummary[personClusterID]!.$1; @@ -1086,7 +1105,7 @@ class ClusterFeedbackService { final fileIdToDistanceMap = {}; for (final entry in faceIdToVectorMap.entries) { fileIdToDistanceMap[getFileIdFromFaceId(entry.key)] = - cosineDistanceSIMD(personAvg, entry.value); + 1 - personAvg.dot(entry.value); } w?.log('calculated distances for cluster $clusterID'); suggestion.filesInCluster.sort((b, a) { @@ -1141,7 +1160,7 @@ List<(int, double)> _calcSuggestionsMean(Map args) { continue; } final Vector avg = clusterAvg[personCluster]!; - final distance = cosineDistanceSIMD(avg, otherAvg); + final distance = 1 - avg.dot(otherAvg); comparisons++; if (distance < maxClusterDistance) { if (minDistance == null || distance < minDistance) { 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 7517d057d5..682deaff0c 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 @@ -73,7 +73,7 @@ class PersonService { Future reconcileClusters() async { final EnteWatch? w = kDebugMode ? EnteWatch("reconcileClusters") : null; w?.start(); - await storeRemoteFeedback(); + await fetchRemoteClusterFeedback(); w?.log("Stored remote feedback"); final dbPersonClusterInfo = await faceMLDataDB.getPersonToClusterIdToFaceIds(); @@ -225,7 +225,7 @@ class PersonService { Bus.instance.fire(PeopleChangedEvent()); } - Future storeRemoteFeedback() async { + Future fetchRemoteClusterFeedback() async { await entityService.syncEntities(); final entities = await entityService.getEntities(EntityType.person); entities.sort((a, b) => a.updatedAt.compareTo(b.updatedAt)); diff --git a/mobile/lib/services/machine_learning/machine_learning_controller.dart b/mobile/lib/services/machine_learning/machine_learning_controller.dart index 1b70ea48d8..7268032ca4 100644 --- a/mobile/lib/services/machine_learning/machine_learning_controller.dart +++ b/mobile/lib/services/machine_learning/machine_learning_controller.dart @@ -4,7 +4,6 @@ import "dart:io"; import "package:battery_info/battery_info_plugin.dart"; import "package:battery_info/model/android_battery_info.dart"; import "package:battery_info/model/iso_battery_info.dart"; -import "package:flutter/foundation.dart" show kDebugMode; import "package:logging/logging.dart"; import "package:photos/core/event_bus.dart"; import "package:photos/events/machine_learning_control_event.dart"; @@ -19,18 +18,19 @@ class MachineLearningController { static const kMaximumTemperature = 42; // 42 degree celsius static const kMinimumBatteryLevel = 20; // 20% - static const kDefaultInteractionTimeout = - kDebugMode ? Duration(seconds: 3) : Duration(seconds: 5); + static const kDefaultInteractionTimeout = Duration(seconds: 15); static const kUnhealthyStates = ["over_heat", "over_voltage", "dead"]; bool _isDeviceHealthy = true; bool _isUserInteracting = true; bool _canRunML = false; + bool mlInteractionOverride = false; late Timer _userInteractionTimer; bool get isDeviceHealthy => _isDeviceHealthy; void init() { + _logger.info('init called'); if (Platform.isAndroid) { _startInteractionTimer(); BatteryInfoPlugin() @@ -47,6 +47,7 @@ class MachineLearningController { }); } _fireControlEvent(); + _logger.info('init done'); } void onUserInteraction() { @@ -61,13 +62,23 @@ class MachineLearningController { _resetTimer(); } + bool _canRunGivenUserInteraction() { + return (Platform.isIOS ? true : !_isUserInteracting) || + mlInteractionOverride; + } + + void forceOverrideML({required bool turnOn}) { + _logger.info("Forcing to turn on ML: $turnOn"); + mlInteractionOverride = turnOn; + _fireControlEvent(); + } + void _fireControlEvent() { - final shouldRunML = - _isDeviceHealthy && (Platform.isAndroid ? !_isUserInteracting : true); + final shouldRunML = _isDeviceHealthy && _canRunGivenUserInteraction(); if (shouldRunML != _canRunML) { _canRunML = shouldRunML; _logger.info( - "Firing event with $shouldRunML, device health: $_isDeviceHealthy and user interaction: $_isUserInteracting", + "Firing event: $shouldRunML (device health: $_isDeviceHealthy, user interaction: $_isUserInteracting, mlInteractionOverride: $mlInteractionOverride)", ); Bus.instance.fire(MachineLearningControlEvent(shouldRunML)); } diff --git a/mobile/lib/services/object_detection/models/predictions.dart b/mobile/lib/services/object_detection/models/predictions.dart deleted file mode 100644 index 4957fd8cb8..0000000000 --- a/mobile/lib/services/object_detection/models/predictions.dart +++ /dev/null @@ -1,14 +0,0 @@ -import "package:photos/services/object_detection/models/recognition.dart"; -import "package:photos/services/object_detection/models/stats.dart"; - -class Predictions { - final List? recognitions; - final Stats? stats; - final Object? error; - - Predictions( - this.recognitions, - this.stats, { - this.error, - }); -} diff --git a/mobile/lib/services/object_detection/models/recognition.dart b/mobile/lib/services/object_detection/models/recognition.dart deleted file mode 100644 index 3a76c2d565..0000000000 --- a/mobile/lib/services/object_detection/models/recognition.dart +++ /dev/null @@ -1,18 +0,0 @@ -/// Represents the recognition output from the model -class Recognition { - /// Index of the result - final int id; - - /// Label of the result - final String label; - - /// Confidence [0.0, 1.0] - final double score; - - Recognition(this.id, this.label, this.score); - - @override - String toString() { - return 'Recognition(id: $id, label: $label, score: $score)'; - } -} diff --git a/mobile/lib/services/object_detection/models/stats.dart b/mobile/lib/services/object_detection/models/stats.dart deleted file mode 100644 index 6397f42428..0000000000 --- a/mobile/lib/services/object_detection/models/stats.dart +++ /dev/null @@ -1,27 +0,0 @@ -/// Bundles different elapsed times -class Stats { - /// Total time taken in the isolate where the inference runs - final int totalPredictTime; - - /// [totalPredictTime] + communication overhead time - /// between main isolate and another isolate - final int totalElapsedTime; - - /// Time for which inference runs - final int inferenceTime; - - /// Time taken to pre-process the image - final int preProcessingTime; - - Stats( - this.totalPredictTime, - this.totalElapsedTime, - this.inferenceTime, - this.preProcessingTime, - ); - - @override - String toString() { - return 'Stats{totalPredictTime: $totalPredictTime, totalElapsedTime: $totalElapsedTime, inferenceTime: $inferenceTime, preProcessingTime: $preProcessingTime}'; - } -} diff --git a/mobile/lib/services/object_detection/object_detection_service.dart b/mobile/lib/services/object_detection/object_detection_service.dart deleted file mode 100644 index 360747f952..0000000000 --- a/mobile/lib/services/object_detection/object_detection_service.dart +++ /dev/null @@ -1,157 +0,0 @@ -// import "dart:isolate"; -// import "dart:math"; -// import "dart:typed_data"; - -// import "package:logging/logging.dart"; -// import "package:photos/services/object_detection/models/predictions.dart"; -// import 'package:photos/services/object_detection/models/recognition.dart'; -// import 'package:photos/services/object_detection/tflite/cocossd_classifier.dart'; -// import "package:photos/services/object_detection/tflite/mobilenet_classifier.dart"; -// import "package:photos/services/object_detection/tflite/scene_classifier.dart"; -// import "package:photos/services/object_detection/utils/isolate_utils.dart"; - -// class ObjectDetectionService { -// static const scoreThreshold = 0.35; - -// final _logger = Logger("ObjectDetectionService"); - -// late CocoSSDClassifier _objectClassifier; -// late MobileNetClassifier _mobileNetClassifier; -// late SceneClassifier _sceneClassifier; - -// late IsolateUtils _isolateUtils; - -// ObjectDetectionService._privateConstructor(); -// bool inInitiated = false; - -// Future init() async { -// _isolateUtils = IsolateUtils(); -// await _isolateUtils.start(); -// try { -// _objectClassifier = CocoSSDClassifier(); -// } catch (e, s) { -// _logger.severe("Could not initialize cocossd", e, s); -// } -// try { -// _mobileNetClassifier = MobileNetClassifier(); -// } catch (e, s) { -// _logger.severe("Could not initialize mobilenet", e, s); -// } -// try { -// _sceneClassifier = SceneClassifier(); -// } catch (e, s) { -// _logger.severe("Could not initialize sceneclassifier", e, s); -// } -// inInitiated = true; -// } - -// static ObjectDetectionService instance = -// ObjectDetectionService._privateConstructor(); - -// Future> predict(Uint8List bytes) async { -// try { -// if (!inInitiated) { -// return Future.error("ObjectDetectionService init is not completed"); -// } -// final results = {}; -// final methods = [_getObjects, _getMobileNetResults, _getSceneResults]; - -// for (var method in methods) { -// final methodResults = await method(bytes); -// methodResults.forEach((key, value) { -// results.update( -// key, -// (existingValue) => max(existingValue, value), -// ifAbsent: () => value, -// ); -// }); -// } -// return results; -// } catch (e, s) { -// _logger.severe(e, s); -// rethrow; -// } -// } - -// Future> _getObjects(Uint8List bytes) async { -// try { -// final isolateData = IsolateData( -// bytes, -// _objectClassifier.interpreter.address, -// _objectClassifier.labels, -// ClassifierType.cocossd, -// ); -// return _getPredictions(isolateData); -// } catch (e, s) { -// _logger.severe("Could not run cocossd", e, s); -// } -// return {}; -// } - -// Future> _getMobileNetResults(Uint8List bytes) async { -// try { -// final isolateData = IsolateData( -// bytes, -// _mobileNetClassifier.interpreter.address, -// _mobileNetClassifier.labels, -// ClassifierType.mobilenet, -// ); -// return _getPredictions(isolateData); -// } catch (e, s) { -// _logger.severe("Could not run mobilenet", e, s); -// } -// return {}; -// } - -// Future> _getSceneResults(Uint8List bytes) async { -// try { -// final isolateData = IsolateData( -// bytes, -// _sceneClassifier.interpreter.address, -// _sceneClassifier.labels, -// ClassifierType.scenes, -// ); -// return _getPredictions(isolateData); -// } catch (e, s) { -// _logger.severe("Could not run scene detection", e, s); -// } -// return {}; -// } - -// Future> _getPredictions(IsolateData isolateData) async { -// final predictions = await _inference(isolateData); -// final Map results = {}; - -// if (predictions.error == null) { -// for (final Recognition result in predictions.recognitions!) { -// if (result.score > scoreThreshold) { -// // Update the result score only if it's higher than the current score -// if (!results.containsKey(result.label) || -// results[result.label]! < result.score) { -// results[result.label] = result.score; -// } -// } -// } - -// _logger.info( -// "Time taken for ${isolateData.type}: ${predictions.stats!.totalElapsedTime}ms", -// ); -// } else { -// _logger.severe( -// "Error while fetching predictions for ${isolateData.type}", -// predictions.error, -// ); -// } - -// return results; -// } - -// /// Runs inference in another isolate -// Future _inference(IsolateData isolateData) async { -// final responsePort = ReceivePort(); -// _isolateUtils.sendPort.send( -// isolateData..responsePort = responsePort.sendPort, -// ); -// return await responsePort.first; -// } -// } diff --git a/mobile/lib/services/object_detection/tflite/classifier.dart b/mobile/lib/services/object_detection/tflite/classifier.dart deleted file mode 100644 index 6a989b2792..0000000000 --- a/mobile/lib/services/object_detection/tflite/classifier.dart +++ /dev/null @@ -1,89 +0,0 @@ -// import "dart:math"; - -// import 'package:image/image.dart' as image_lib; -// import "package:logging/logging.dart"; -// import "package:photos/services/object_detection/models/predictions.dart"; -// import "package:tflite_flutter/tflite_flutter.dart"; -// import "package:tflite_flutter_helper/tflite_flutter_helper.dart"; - -// abstract class Classifier { -// // Path to the model -// String get modelPath; - -// // Path to the labels -// String get labelPath; - -// // Input size expected by the model (for eg. width = height = 224) -// int get inputSize; - -// // Logger implementation for the specific classifier -// Logger get logger; - -// Predictions? predict(image_lib.Image image); - -// /// Instance of Interpreter -// late Interpreter _interpreter; - -// /// Labels file loaded as list -// late List _labels; - -// /// Shapes of output tensors -// late List> _outputShapes; - -// /// Types of output tensors -// late List _outputTypes; - -// /// Gets the interpreter instance -// Interpreter get interpreter => _interpreter; - -// /// Gets the loaded labels -// List get labels => _labels; - -// /// Gets the output shapes -// List> get outputShapes => _outputShapes; - -// /// Gets the output types -// List get outputTypes => _outputTypes; - -// /// Loads interpreter from asset -// void loadModel(Interpreter? interpreter) async { -// try { -// _interpreter = interpreter ?? -// await Interpreter.fromAsset( -// modelPath, -// options: InterpreterOptions()..threads = 4, -// ); -// final outputTensors = _interpreter.getOutputTensors(); -// _outputShapes = []; -// _outputTypes = []; -// for (var tensor in outputTensors) { -// _outputShapes.add(tensor.shape); -// _outputTypes.add(tensor.type); -// } -// logger.info("Interpreter initialized"); -// } catch (e, s) { -// logger.severe("Error while creating interpreter", e, s); -// } -// } - -// /// Loads labels from assets -// void loadLabels(List? labels) async { -// try { -// _labels = labels ?? await FileUtil.loadLabels(labelPath); -// logger.info("Labels initialized"); -// } catch (e, s) { -// logger.severe("Error while loading labels", e, s); -// } -// } - -// /// Pre-process the image -// TensorImage getProcessedImage(TensorImage inputImage) { -// final padSize = max(inputImage.height, inputImage.width); -// final imageProcessor = ImageProcessorBuilder() -// .add(ResizeWithCropOrPadOp(padSize, padSize)) -// .add(ResizeOp(inputSize, inputSize, ResizeMethod.BILINEAR)) -// .build(); -// inputImage = imageProcessor.process(inputImage); -// return inputImage; -// } -// } diff --git a/mobile/lib/services/object_detection/tflite/cocossd_classifier.dart b/mobile/lib/services/object_detection/tflite/cocossd_classifier.dart deleted file mode 100644 index 246d704982..0000000000 --- a/mobile/lib/services/object_detection/tflite/cocossd_classifier.dart +++ /dev/null @@ -1,115 +0,0 @@ -// import 'dart:math'; - -// import 'package:image/image.dart' as image_lib; -// import "package:logging/logging.dart"; -// import 'package:photos/services/object_detection/models/predictions.dart'; -// import 'package:photos/services/object_detection/models/recognition.dart'; -// import "package:photos/services/object_detection/models/stats.dart"; -// import "package:photos/services/object_detection/tflite/classifier.dart"; -// import "package:tflite_flutter/tflite_flutter.dart"; -// import "package:tflite_flutter_helper/tflite_flutter_helper.dart"; - -// /// Classifier -// class CocoSSDClassifier extends Classifier { -// static final _logger = Logger("CocoSSDClassifier"); -// static const double threshold = 0.4; - -// @override -// String get modelPath => "models/cocossd/model.tflite"; - -// @override -// String get labelPath => "assets/models/cocossd/labels.txt"; - -// @override -// int get inputSize => 300; - -// @override -// Logger get logger => _logger; - -// static const int numResults = 10; - -// CocoSSDClassifier({ -// Interpreter? interpreter, -// List? labels, -// }) { -// loadModel(interpreter); -// loadLabels(labels); -// } - -// @override -// Predictions? predict(image_lib.Image image) { -// final predictStartTime = DateTime.now().millisecondsSinceEpoch; - -// final preProcessStart = DateTime.now().millisecondsSinceEpoch; - -// // Create TensorImage from image -// TensorImage inputImage = TensorImage.fromImage(image); - -// // Pre-process TensorImage -// inputImage = getProcessedImage(inputImage); - -// final preProcessElapsedTime = -// DateTime.now().millisecondsSinceEpoch - preProcessStart; - -// // TensorBuffers for output tensors -// final outputLocations = TensorBufferFloat(outputShapes[0]); -// final outputClasses = TensorBufferFloat(outputShapes[1]); -// final outputScores = TensorBufferFloat(outputShapes[2]); -// final numLocations = TensorBufferFloat(outputShapes[3]); - -// // Inputs object for runForMultipleInputs -// // Use [TensorImage.buffer] or [TensorBuffer.buffer] to pass by reference -// final inputs = [inputImage.buffer]; - -// // Outputs map -// final outputs = { -// 0: outputLocations.buffer, -// 1: outputClasses.buffer, -// 2: outputScores.buffer, -// 3: numLocations.buffer, -// }; - -// final inferenceTimeStart = DateTime.now().millisecondsSinceEpoch; - -// // run inference -// interpreter.runForMultipleInputs(inputs, outputs); - -// final inferenceTimeElapsed = -// DateTime.now().millisecondsSinceEpoch - inferenceTimeStart; - -// // Maximum number of results to show -// final resultsCount = min(numResults, numLocations.getIntValue(0)); - -// // Using labelOffset = 1 as ??? at index 0 -// const labelOffset = 1; - -// final recognitions = []; - -// for (int i = 0; i < resultsCount; i++) { -// // Prediction score -// final score = outputScores.getDoubleValue(i); - -// // Label string -// final labelIndex = outputClasses.getIntValue(i) + labelOffset; -// final label = labels.elementAt(labelIndex); - -// if (score > threshold) { -// recognitions.add( -// Recognition(i, label, score), -// ); -// } -// } - -// final predictElapsedTime = -// DateTime.now().millisecondsSinceEpoch - predictStartTime; -// return Predictions( -// recognitions, -// Stats( -// predictElapsedTime, -// predictElapsedTime, -// inferenceTimeElapsed, -// preProcessElapsedTime, -// ), -// ); -// } -// } diff --git a/mobile/lib/services/object_detection/tflite/mobilenet_classifier.dart b/mobile/lib/services/object_detection/tflite/mobilenet_classifier.dart deleted file mode 100644 index d3cadd1a61..0000000000 --- a/mobile/lib/services/object_detection/tflite/mobilenet_classifier.dart +++ /dev/null @@ -1,83 +0,0 @@ -// import 'package:image/image.dart' as image_lib; -// import "package:logging/logging.dart"; -// import 'package:photos/services/object_detection/models/predictions.dart'; -// import 'package:photos/services/object_detection/models/recognition.dart'; -// import "package:photos/services/object_detection/models/stats.dart"; -// import "package:photos/services/object_detection/tflite/classifier.dart"; -// import "package:tflite_flutter/tflite_flutter.dart"; -// import "package:tflite_flutter_helper/tflite_flutter_helper.dart"; - -// // Source: https://tfhub.dev/tensorflow/lite-model/mobilenet_v1_1.0_224/1/default/1 -// class MobileNetClassifier extends Classifier { -// static final _logger = Logger("MobileNetClassifier"); -// static const double threshold = 0.4; - -// @override -// String get modelPath => "models/mobilenet/mobilenet_v1_1.0_224_quant.tflite"; - -// @override -// String get labelPath => -// "assets/models/mobilenet/labels_mobilenet_quant_v1_224.txt"; - -// @override -// int get inputSize => 224; - -// @override -// Logger get logger => _logger; - -// MobileNetClassifier({ -// Interpreter? interpreter, -// List? labels, -// }) { -// loadModel(interpreter); -// loadLabels(labels); -// } - -// @override -// Predictions? predict(image_lib.Image image) { -// final predictStartTime = DateTime.now().millisecondsSinceEpoch; - -// final preProcessStart = DateTime.now().millisecondsSinceEpoch; - -// // Create TensorImage from image -// TensorImage inputImage = TensorImage.fromImage(image); - -// // Pre-process TensorImage -// inputImage = getProcessedImage(inputImage); - -// final preProcessElapsedTime = -// DateTime.now().millisecondsSinceEpoch - preProcessStart; - -// // TensorBuffers for output tensors -// final output = TensorBufferUint8(outputShapes[0]); -// final inferenceTimeStart = DateTime.now().millisecondsSinceEpoch; -// // run inference -// interpreter.run(inputImage.buffer, output.buffer); - -// final inferenceTimeElapsed = -// DateTime.now().millisecondsSinceEpoch - inferenceTimeStart; - -// final recognitions = []; -// for (int i = 0; i < labels.length; i++) { -// final score = output.getDoubleValue(i) / 255; -// final label = labels.elementAt(i); -// if (score >= threshold) { -// recognitions.add( -// Recognition(i, label, score), -// ); -// } -// } - -// final predictElapsedTime = -// DateTime.now().millisecondsSinceEpoch - predictStartTime; -// return Predictions( -// recognitions, -// Stats( -// predictElapsedTime, -// predictElapsedTime, -// inferenceTimeElapsed, -// preProcessElapsedTime, -// ), -// ); -// } -// } diff --git a/mobile/lib/services/object_detection/tflite/scene_classifier.dart b/mobile/lib/services/object_detection/tflite/scene_classifier.dart deleted file mode 100644 index 1c1db6ce8f..0000000000 --- a/mobile/lib/services/object_detection/tflite/scene_classifier.dart +++ /dev/null @@ -1,88 +0,0 @@ -// import "package:flutter/foundation.dart"; -// import 'package:image/image.dart' as image_lib; -// import "package:logging/logging.dart"; -// import 'package:photos/services/object_detection/models/predictions.dart'; -// import 'package:photos/services/object_detection/models/recognition.dart'; -// import "package:photos/services/object_detection/models/stats.dart"; -// import "package:photos/services/object_detection/tflite/classifier.dart"; -// import "package:tflite_flutter/tflite_flutter.dart"; -// import "package:tflite_flutter_helper/tflite_flutter_helper.dart"; - -// // Source: https://tfhub.dev/sayannath/lite-model/image-scene/1 -// class SceneClassifier extends Classifier { -// static final _logger = Logger("SceneClassifier"); -// static const double threshold = 0.35; - -// @override -// String get modelPath => "models/scenes/model.tflite"; - -// @override -// String get labelPath => "assets/models/scenes/labels.txt"; - -// @override -// int get inputSize => 224; - -// @override -// Logger get logger => _logger; - -// SceneClassifier({ -// Interpreter? interpreter, -// List? labels, -// }) { -// loadModel(interpreter); -// loadLabels(labels); -// } - -// @override -// Predictions? predict(image_lib.Image image) { -// final predictStartTime = DateTime.now().millisecondsSinceEpoch; - -// final preProcessStart = DateTime.now().millisecondsSinceEpoch; - -// // Create TensorImage from image -// TensorImage inputImage = TensorImage.fromImage(image); - -// // Pre-process TensorImage -// inputImage = getProcessedImage(inputImage); -// final list = inputImage.getTensorBuffer().getDoubleList(); -// final input = list.reshape([1, inputSize, inputSize, 3]); - -// final preProcessElapsedTime = -// DateTime.now().millisecondsSinceEpoch - preProcessStart; - -// final output = TensorBufferFloat(outputShapes[0]); - -// final inferenceTimeStart = DateTime.now().millisecondsSinceEpoch; -// interpreter.run(input, output.buffer); -// final inferenceTimeElapsed = -// DateTime.now().millisecondsSinceEpoch - inferenceTimeStart; - -// final recognitions = []; -// for (int i = 0; i < labels.length; i++) { -// final score = output.getDoubleValue(i); -// final label = labels.elementAt(i); -// if (score >= threshold) { -// recognitions.add( -// Recognition(i, label, score), -// ); -// } else if (kDebugMode && score > 0.2) { -// debugPrint("scenePrediction score $label is below threshold: $score"); -// } -// } -// debugPrint( -// "Total lables ${labels.length} + reccg ${recognitions.map((e) => e.label).toSet()}", -// ); - -// final predictElapsedTime = -// DateTime.now().millisecondsSinceEpoch - predictStartTime; -// return Predictions( -// recognitions, -// Stats( -// predictElapsedTime, -// predictElapsedTime, -// inferenceTimeElapsed, -// preProcessElapsedTime, -// ), -// ); -// } -// } diff --git a/mobile/lib/services/object_detection/utils/isolate_utils.dart b/mobile/lib/services/object_detection/utils/isolate_utils.dart deleted file mode 100644 index c5c46e8636..0000000000 --- a/mobile/lib/services/object_detection/utils/isolate_utils.dart +++ /dev/null @@ -1,88 +0,0 @@ -// import 'dart:isolate'; -// import "dart:typed_data"; - -// import 'package:image/image.dart' as imgLib; -// import "package:photos/services/object_detection/models/predictions.dart"; -// import "package:photos/services/object_detection/tflite/classifier.dart"; -// import 'package:photos/services/object_detection/tflite/cocossd_classifier.dart'; -// import "package:photos/services/object_detection/tflite/mobilenet_classifier.dart"; -// import "package:photos/services/object_detection/tflite/scene_classifier.dart"; -// import 'package:tflite_flutter/tflite_flutter.dart'; - -// /// Manages separate Isolate instance for inference -// class IsolateUtils { -// static const String debugName = "InferenceIsolate"; - -// late SendPort _sendPort; -// final _receivePort = ReceivePort(); - -// SendPort get sendPort => _sendPort; - -// Future start() async { -// await Isolate.spawn( -// entryPoint, -// _receivePort.sendPort, -// debugName: debugName, -// ); - -// _sendPort = await _receivePort.first; -// } - -// static void entryPoint(SendPort sendPort) async { -// final port = ReceivePort(); -// sendPort.send(port.sendPort); - -// await for (final IsolateData isolateData in port) { -// final classifier = _getClassifier(isolateData); -// final image = imgLib.decodeImage(isolateData.input); -// try { -// final results = classifier.predict(image!); -// isolateData.responsePort.send(results); -// } catch (e) { -// isolateData.responsePort.send(Predictions(null, null, error: e)); -// } -// } -// } - -// static Classifier _getClassifier(IsolateData isolateData) { -// final interpreter = Interpreter.fromAddress(isolateData.interpreterAddress); -// if (isolateData.type == ClassifierType.cocossd) { -// return CocoSSDClassifier( -// interpreter: interpreter, -// labels: isolateData.labels, -// ); -// } else if (isolateData.type == ClassifierType.mobilenet) { -// return MobileNetClassifier( -// interpreter: interpreter, -// labels: isolateData.labels, -// ); -// } else { -// return SceneClassifier( -// interpreter: interpreter, -// labels: isolateData.labels, -// ); -// } -// } -// } - -// /// Bundles data to pass between Isolate -// class IsolateData { -// Uint8List input; -// int interpreterAddress; -// List labels; -// ClassifierType type; -// late SendPort responsePort; - -// IsolateData( -// this.input, -// this.interpreterAddress, -// this.labels, -// this.type, -// ); -// } - -// enum ClassifierType { -// cocossd, -// mobilenet, -// scenes, -// } diff --git a/mobile/lib/ui/home/landing_page_widget.dart b/mobile/lib/ui/home/landing_page_widget.dart index 11f043b8b4..16604cfca9 100644 --- a/mobile/lib/ui/home/landing_page_widget.dart +++ b/mobile/lib/ui/home/landing_page_widget.dart @@ -288,7 +288,7 @@ class _LandingPageWidgetState extends State { final result = await showDialogWidget( context: context, title: S.of(context).pleaseLoginAgain, - body: S.of(context).devAccountChanged, + body: S.of(context).autoLogoutMessage, buttons: const [ ButtonWidget( buttonType: ButtonType.neutral, diff --git a/mobile/lib/ui/map/map_marker.dart b/mobile/lib/ui/map/map_marker.dart index 0009370be1..3dc026697a 100644 --- a/mobile/lib/ui/map/map_marker.dart +++ b/mobile/lib/ui/map/map_marker.dart @@ -7,20 +7,19 @@ import "package:photos/ui/map/marker_image.dart"; Marker mapMarker( ImageMarker imageMarker, - String key, { + ValueKey key, { Size markerSize = MapView.defaultMarkerSize, }) { return Marker( - //-6.5 is for taking in the height of the MarkerPointer - anchorPos: AnchorPos.exactly(Anchor(markerSize.height / 2, -6.5)), - key: Key(key), + alignment: Alignment.topCenter, + key: key, width: markerSize.width, height: markerSize.height, point: LatLng( imageMarker.latitude, imageMarker.longitude, ), - builder: (context) => MarkerImage( + child: MarkerImage( file: imageMarker.imageFile, seperator: (MapView.defaultMarkerSize.height + 10) - (MapView.defaultMarkerSize.height - markerSize.height), diff --git a/mobile/lib/ui/map/map_screen.dart b/mobile/lib/ui/map/map_screen.dart index adb2d590d9..78120bd839 100644 --- a/mobile/lib/ui/map/map_screen.dart +++ b/mobile/lib/ui/map/map_screen.dart @@ -114,7 +114,7 @@ class _MapScreenState extends State { ); Timer(Duration(milliseconds: debounceDuration), () { - calculateVisibleMarkers(mapController.bounds!); + calculateVisibleMarkers(mapController.camera.visibleBounds); setState(() { isLoading = false; }); diff --git a/mobile/lib/ui/map/map_view.dart b/mobile/lib/ui/map/map_view.dart index 15b5c1d8b9..d4de022197 100644 --- a/mobile/lib/ui/map/map_view.dart +++ b/mobile/lib/ui/map/map_view.dart @@ -4,8 +4,8 @@ import "package:flutter_map_marker_cluster/flutter_map_marker_cluster.dart"; import "package:latlong2/latlong.dart"; import "package:photos/ui/map/image_marker.dart"; import "package:photos/ui/map/map_button.dart"; -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_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"; @@ -60,11 +60,6 @@ class _MapViewState extends State { _markers = _buildMakers(); } - @override - void dispose() { - super.dispose(); - } - void onChange(LatLngBounds bounds) { _debouncer.run( () async { @@ -85,55 +80,44 @@ class _MapViewState extends State { widget.onTap!.call(); } : null, - center: widget.center, + initialCenter: widget.center, minZoom: widget.minZoom, maxZoom: widget.maxZoom, - enableMultiFingerGestureRace: true, - zoom: widget.initialZoom, - maxBounds: LatLngBounds( - const LatLng(-90, -180), - const LatLng(90, 180), + interactionOptions: InteractionOptions( + flags: widget.interactiveFlags, + enableMultiFingerGestureRace: true, + ), + initialZoom: widget.initialZoom, + cameraConstraint: CameraConstraint.contain( + bounds: LatLngBounds( + const LatLng(-90, -180), + const LatLng(90, 180), + ), ), onPositionChanged: (position, hasGesture) { if (position.bounds != null) { onChange(position.bounds!); } }, - interactiveFlags: widget.interactiveFlags, ), - nonRotatedChildren: [ - Padding( - padding: EdgeInsets.only( - bottom: widget.bottomSheetDraggableAreaHeight, - ), - child: OSMFranceTileAttributes( - options: widget.mapAttributionOptions, - ), - ), - ], children: [ const OSMFranceTileLayer(), MarkerClusterLayerWidget( options: MarkerClusterLayerOptions( - anchorPos: AnchorPos.align(AnchorAlign.top), + alignment: Alignment.topCenter, maxClusterRadius: 100, showPolygon: false, size: widget.markerSize, - fitBoundsOptions: const FitBoundsOptions( - padding: EdgeInsets.all(80), - ), + padding: const EdgeInsets.all(80), markers: _markers, onClusterTap: (_) { - onChange(widget.controller.bounds!); + onChange(widget.controller.camera.visibleBounds); }, builder: (context, List markers) { - final index = int.parse( - markers.first.key - .toString() - .replaceAll(RegExp(r'[^0-9]'), ''), - ); - final String clusterKey = - 'map-badge-$index-len-${markers.length}'; + final valueKey = markers.first.key as ValueKey; + final index = valueKey.value as int; + + final clusterKey = 'map-badge-$index-len-${markers.length}'; return Stack( key: ValueKey(clusterKey), @@ -148,6 +132,14 @@ class _MapViewState extends State { }, ), ), + Padding( + padding: EdgeInsets.only( + bottom: widget.bottomSheetDraggableAreaHeight, + ), + child: OSMFranceTileAttributes( + options: widget.mapAttributionOptions, + ), + ), ], ), widget.showControls @@ -175,8 +167,8 @@ class _MapViewState extends State { icon: Icons.add, onPressed: () { widget.controller.move( - widget.controller.center, - widget.controller.zoom + 1, + widget.controller.camera.center, + widget.controller.camera.zoom + 1, ); }, heroTag: 'zoom-in', @@ -185,8 +177,8 @@ class _MapViewState extends State { icon: Icons.remove, onPressed: () { widget.controller.move( - widget.controller.center, - widget.controller.zoom - 1, + widget.controller.camera.center, + widget.controller.camera.zoom - 1, ); }, heroTag: 'zoom-out', @@ -204,7 +196,7 @@ class _MapViewState extends State { final imageMarker = widget.imageMarkers[index]; return mapMarker( imageMarker, - index.toString(), + ValueKey(index), markerSize: widget.markerSize, ); }); diff --git a/mobile/lib/ui/map/tile/attribution/map_attribution.dart b/mobile/lib/ui/map/tile/attribution/map_attribution.dart index e00e1a3e64..7ae8c6bc86 100644 --- a/mobile/lib/ui/map/tile/attribution/map_attribution.dart +++ b/mobile/lib/ui/map/tile/attribution/map_attribution.dart @@ -3,7 +3,7 @@ import "dart:async"; import "package:flutter/material.dart"; -import "package:flutter_map/plugin_api.dart"; +import "package:flutter_map/flutter_map.dart"; import "package:photos/extensions/list.dart"; import "package:photos/theme/colors.dart"; import "package:photos/theme/ente_theme.dart"; @@ -194,10 +194,8 @@ class MapAttributionWidgetState extends State { context, () { setState(() => popupExpanded = true); - mapEventSubscription = FlutterMapState.of(context) - .mapController - .mapEventStream - .listen((e) { + mapEventSubscription = + MapController().mapEventStream.listen((e) { setState(() => popupExpanded = false); mapEventSubscription?.cancel(); }); diff --git a/mobile/lib/ui/settings/debug/face_debug_section_widget.dart b/mobile/lib/ui/settings/debug/face_debug_section_widget.dart index 376793769f..844f71c01b 100644 --- a/mobile/lib/ui/settings/debug/face_debug_section_widget.dart +++ b/mobile/lib/ui/settings/debug/face_debug_section_widget.dart @@ -193,7 +193,7 @@ class _FaceDebugSectionWidgetState extends State { trailingIconIsMuted: true, onTap: () async { try { - await PersonService.instance.storeRemoteFeedback(); + await PersonService.instance.fetchRemoteClusterFeedback(); FaceMlService.instance.debugIndexingDisabled = false; await FaceMlService.instance .clusterAllImages(clusterInBuckets: true); diff --git a/mobile/lib/ui/settings/machine_learning_settings_page.dart b/mobile/lib/ui/settings/machine_learning_settings_page.dart index 0ea1588a0e..62ee7a1c0b 100644 --- a/mobile/lib/ui/settings/machine_learning_settings_page.dart +++ b/mobile/lib/ui/settings/machine_learning_settings_page.dart @@ -58,6 +58,7 @@ class _MachineLearningSettingsPageState }); _fetchState(); _wakeLock.enable(); + MachineLearningController.instance.forceOverrideML(turnOn: true); } void _fetchState() { @@ -69,6 +70,7 @@ class _MachineLearningSettingsPageState super.dispose(); _eventSubscription.cancel(); _wakeLock.disable(); + MachineLearningController.instance.forceOverrideML(turnOn: false); } @override @@ -89,12 +91,27 @@ class _MachineLearningSettingsPageState iconButtonType: IconButtonType.secondary, onTap: () { Navigator.pop(context); - Navigator.pop(context); - Navigator.pop(context); + if (Navigator.canPop(context)) Navigator.pop(context); + if (Navigator.canPop(context)) Navigator.pop(context); }, ), ], ), + SliverList( + delegate: SliverChildBuilderDelegate( + (delegateBuildContext, index) => Padding( + padding: const EdgeInsets.only(left: 16, right: 16), + child: Text( + S.of(context).mlIndexingDescription, + textAlign: TextAlign.left, + style: getEnteTextTheme(context) + .mini + .copyWith(color: getEnteColorScheme(context).textMuted), + ), + ), + childCount: 1, + ), + ), SliverList( delegate: SliverChildBuilderDelegate( (delegateBuildContext, index) { @@ -154,12 +171,6 @@ class _MachineLearningSettingsPageState alignCaptionedTextToLeft: true, isGestureDetectorDisabled: true, ), - const SizedBox( - height: 4, - ), - MenuSectionDescriptionWidget( - content: S.of(context).magicSearchDescription, - ), const SizedBox( height: 12, ), @@ -225,12 +236,6 @@ class _MachineLearningSettingsPageState alignCaptionedTextToLeft: true, isGestureDetectorDisabled: true, ), - const SizedBox( - height: 4, - ), - MenuSectionDescriptionWidget( - content: S.of(context).faceRecognitionIndexingDescription, - ), const SizedBox( height: 12, ), @@ -535,15 +540,19 @@ class FaceRecognitionStatusWidgetState title: S.of(context).clusteringProgress, ), trailingWidget: Text( - "${clusteringPercentage.toStringAsFixed(0)}%", + FaceMlService.instance.showClusteringIsHappening + ? "currently running" + : "${clusteringPercentage.toStringAsFixed(0)}%", style: Theme.of(context).textTheme.bodySmall, ), singleBorderRadius: 8, alignCaptionedTextToLeft: true, isGestureDetectorDisabled: true, key: ValueKey( - "clustering_progress_" + - clusteringPercentage.toStringAsFixed(0), + FaceMlService.instance.showClusteringIsHappening + ? "currently running" + : "clustering_progress_" + + clusteringPercentage.toStringAsFixed(0), ), ), ], diff --git a/mobile/lib/ui/settings/storage_card_widget.dart b/mobile/lib/ui/settings/storage_card_widget.dart index 0d8924c836..3554c6fa47 100644 --- a/mobile/lib/ui/settings/storage_card_widget.dart +++ b/mobile/lib/ui/settings/storage_card_widget.dart @@ -272,7 +272,7 @@ class _StorageCardWidgetState extends State { ) : const SizedBox.shrink(), Text( - S.of(context).freeStorageSpace(freeSpace, freeSpaceUnit), + S.of(context).availableStorageSpace(freeSpace, freeSpaceUnit), style: getEnteTextTheme(context) .mini .copyWith(color: textFaintDark), diff --git a/mobile/lib/ui/tools/editor/image_editor_page.dart b/mobile/lib/ui/tools/editor/image_editor_page.dart index 4830df9523..5314d9ca86 100644 --- a/mobile/lib/ui/tools/editor/image_editor_page.dart +++ b/mobile/lib/ui/tools/editor/image_editor_page.dart @@ -371,7 +371,7 @@ class _ImageEditorPageState extends State { ); } } - newFile.generatedID = await FilesDB.instance.insert(newFile); + newFile.generatedID = await FilesDB.instance.insertAndGetId(newFile); Bus.instance.fire(LocalPhotosUpdatedEvent([newFile], source: "editSave")); unawaited(SyncService.instance.sync()); showShortToast(context, S.of(context).editsSaved); diff --git a/mobile/lib/ui/viewer/file/file_details_widget.dart b/mobile/lib/ui/viewer/file/file_details_widget.dart index d87a806cc4..2423ee77c8 100644 --- a/mobile/lib/ui/viewer/file/file_details_widget.dart +++ b/mobile/lib/ui/viewer/file/file_details_widget.dart @@ -11,7 +11,6 @@ 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/services/file_magic_service.dart"; -import "package:photos/services/update_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"; @@ -26,6 +25,7 @@ import "package:photos/ui/viewer/file_details/faces_item_widget.dart"; import "package:photos/ui/viewer/file_details/file_properties_item_widget.dart"; import "package:photos/ui/viewer/file_details/location_tags_widget.dart"; import "package:photos/utils/exif_util.dart"; +import "package:photos/utils/local_settings.dart"; class FileDetailsWidget extends StatefulWidget { final EnteFile file; @@ -230,9 +230,8 @@ class _FileDetailsWidgetState extends State { ]); } - if (!UpdateService.instance.isFdroidFlavor()) { + if (LocalSettings.instance.isFaceIndexingEnabled) { fileDetailsTiles.addAll([ - // ObjectsItemWidget(file), FacesItemWidget(file), const FileDetailsDivider(), ]); diff --git a/mobile/lib/ui/viewer/file_details/face_widget.dart b/mobile/lib/ui/viewer/file_details/face_widget.dart index 1ec7a2eb2d..67d2368d71 100644 --- a/mobile/lib/ui/viewer/file_details/face_widget.dart +++ b/mobile/lib/ui/viewer/file_details/face_widget.dart @@ -293,7 +293,7 @@ class _FaceWidgetState extends State { } } - Future?> getFaceCrop() async { + Future?> getFaceCrop({int fetchAttempt = 1}) async { try { final Uint8List? cachedFace = faceCropCache.get(widget.face.faceID); if (cachedFace != null) { @@ -326,6 +326,10 @@ class _FaceWidgetState extends State { error: e, stackTrace: s, ); + resetPool(fullFile: true); + if (fetchAttempt <= retryLimit) { + return getFaceCrop(fetchAttempt: fetchAttempt + 1); + } return null; } } diff --git a/mobile/lib/ui/viewer/file_details/faces_item_widget.dart b/mobile/lib/ui/viewer/file_details/faces_item_widget.dart index ed2fb0f12e..cb22e53b82 100644 --- a/mobile/lib/ui/viewer/file_details/faces_item_widget.dart +++ b/mobile/lib/ui/viewer/file_details/faces_item_widget.dart @@ -173,7 +173,9 @@ class _FacesItemWidgetState extends State { } Future?> getRelevantFaceCrops( - Iterable faces, + Iterable faces, { + int fetchAttempt = 1, + } ) async { try { final faceIdToCrop = {}; @@ -223,6 +225,10 @@ class _FacesItemWidgetState extends State { error: e, stackTrace: s, ); + resetPool(fullFile: true); + if(fetchAttempt <= retryLimit) { + return getRelevantFaceCrops(faces, fetchAttempt: fetchAttempt + 1); + } return null; } } diff --git a/mobile/lib/ui/viewer/location/location_screen.dart b/mobile/lib/ui/viewer/location/location_screen.dart index 374d70cb9f..55975dd3fa 100644 --- a/mobile/lib/ui/viewer/location/location_screen.dart +++ b/mobile/lib/ui/viewer/location/location_screen.dart @@ -146,6 +146,8 @@ class _LocationGalleryWidgetState extends State { late final StreamSubscription _filesUpdateEvent; @override void initState() { + super.initState(); + final collectionsToHide = CollectionsService.instance.archivedOrHiddenCollectionIds(); fileLoadResult = FilesDB.instance @@ -179,8 +181,6 @@ class _LocationGalleryWidgetState extends State { }); galleryHeaderWidget = const GalleryHeaderWidget(); - - super.initState(); } @override diff --git a/mobile/lib/ui/viewer/people/add_person_action_sheet.dart b/mobile/lib/ui/viewer/people/add_person_action_sheet.dart index 7a0c3a4713..eb5b3e4b32 100644 --- a/mobile/lib/ui/viewer/people/add_person_action_sheet.dart +++ b/mobile/lib/ui/viewer/people/add_person_action_sheet.dart @@ -2,6 +2,7 @@ import "dart:async"; import "dart:developer"; import "dart:math" as math; +import "package:flutter/foundation.dart"; import 'package:flutter/material.dart'; import "package:logging/logging.dart"; import 'package:modal_bottom_sheet/modal_bottom_sheet.dart'; @@ -48,14 +49,14 @@ Future showAssignPersonAction( BuildContext context, { required int clusterID, PersonActionType actionType = PersonActionType.assignPerson, - bool showOptionToCreateNewAlbum = true, + bool showOptionToAddNewPerson = true, }) { return showBarModalBottomSheet( context: context, builder: (context) { return PersonActionSheet( actionType: actionType, - showOptionToCreateNewPerson: showOptionToCreateNewAlbum, + showOptionToCreateNewPerson: showOptionToAddNewPerson, cluserID: clusterID, ); }, @@ -182,12 +183,21 @@ class _PersonActionSheetState extends State { child: Padding( padding: const EdgeInsets.fromLTRB(16, 24, 4, 0), child: FutureBuilder>( - future: _getPersons(), + future: _getPersonsWithRecentFile(), builder: (context, snapshot) { if (snapshot.hasError) { log("Error: ${snapshot.error} ${snapshot.stackTrace}}"); //Need to show an error on the UI here - return const SizedBox.shrink(); + if (kDebugMode) { + return Column( + children: [ + Text('${snapshot.error}'), + Text('${snapshot.stackTrace}'), + ], + ); + } else { + return const SizedBox.shrink(); + } } else if (snapshot.hasData) { final persons = snapshot.data!; final searchResults = _searchQuery.isNotEmpty @@ -199,6 +209,10 @@ class _PersonActionSheetState extends State { ) .toList() : persons; + // sort searchResults alphabetically by name + searchResults.sort( + (a, b) => a.$1.data.name.compareTo(b.$1.data.name), + ); final shouldShowAddPerson = widget.showOptionToCreateNewPerson && (_searchQuery.isEmpty || searchResults.isEmpty); @@ -303,7 +317,7 @@ class _PersonActionSheetState extends State { } } - Future> _getPersons({ + Future> _getPersonsWithRecentFile({ bool excludeHidden = true, }) async { final persons = await PersonService.instance.getPersons(); @@ -317,6 +331,12 @@ class _PersonActionSheetState extends State { person.remoteID, ); final files = clustersToFiles.values.expand((e) => e).toList(); + if (files.isEmpty) { + debugPrint( + "Person ${kDebugMode ? person.data.name : person.remoteID} has no files", + ); + continue; + } personAndFileID.add((person, files.first)); } return personAndFileID; diff --git a/mobile/lib/ui/viewer/people/cluster_app_bar.dart b/mobile/lib/ui/viewer/people/cluster_app_bar.dart index 0896d06896..e2b3c10824 100644 --- a/mobile/lib/ui/viewer/people/cluster_app_bar.dart +++ b/mobile/lib/ui/viewer/people/cluster_app_bar.dart @@ -3,7 +3,6 @@ import 'dart:async'; import "package:flutter/foundation.dart"; import 'package:flutter/material.dart'; import 'package:logging/logging.dart'; -import "package:ml_linalg/linalg.dart"; import 'package:photos/core/configuration.dart'; import 'package:photos/core/event_bus.dart'; import "package:photos/db/files_db.dart"; @@ -11,12 +10,10 @@ import "package:photos/events/people_changed_event.dart"; import 'package:photos/events/subscription_purchased_event.dart'; import "package:photos/face/db.dart"; import "package:photos/face/model/person.dart"; -import "package:photos/generated/protos/ente/common/vector.pb.dart"; 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/collections_service.dart'; -import "package:photos/services/machine_learning/face_ml/face_clustering/cosine_distance.dart"; import "package:photos/services/machine_learning/face_ml/face_ml_result.dart"; import "package:photos/services/machine_learning/face_ml/feedback/cluster_feedback.dart"; import 'package:photos/ui/actions/collection/collection_sharing_actions.dart'; @@ -97,7 +94,7 @@ class _AppBarWidgetState extends State { maxLines: 2, overflow: TextOverflow.ellipsis, ), - actions: kDebugMode ? _getDefaultActions(context) : null, + actions: _getDefaultActions(context), ); } @@ -160,58 +157,6 @@ class _AppBarWidgetState extends State { return actions; } - @Deprecated( - 'Used for debugging an issue with conflicts on cluster IDs, resolved now', - ) - Future _validateCluster(BuildContext context) async { - _logger.info('_validateCluster called'); - final faceMlDb = FaceMLDataDB.instance; - - final faceIDs = await faceMlDb.getFaceIDsForCluster(widget.clusterID); - final fileIDs = faceIDs.map((e) => getFileIdFromFaceId(e)).toList(); - - final embeddingsBlobs = await faceMlDb.getFaceEmbeddingMapForFile(fileIDs); - embeddingsBlobs.removeWhere((key, value) => !faceIDs.contains(key)); - final embeddings = embeddingsBlobs - .map((key, value) => MapEntry(key, EVector.fromBuffer(value).values)); - - for (final MapEntry> embedding in embeddings.entries) { - double closestDistance = double.infinity; - double closestDistance32 = double.infinity; - double closestDistance64 = double.infinity; - String? closestFaceID; - for (final MapEntry> otherEmbedding - in embeddings.entries) { - if (embedding.key == otherEmbedding.key) { - continue; - } - final distance64 = cosineDistanceSIMD( - Vector.fromList(embedding.value, dtype: DType.float64), - Vector.fromList(otherEmbedding.value, dtype: DType.float64), - ); - final distance32 = cosineDistanceSIMD( - Vector.fromList(embedding.value, dtype: DType.float32), - Vector.fromList(otherEmbedding.value, dtype: DType.float32), - ); - final distance = cosineDistForNormVectors( - embedding.value, - otherEmbedding.value, - ); - if (distance < closestDistance) { - closestDistance = distance; - closestDistance32 = distance32; - closestDistance64 = distance64; - closestFaceID = otherEmbedding.key; - } - } - if (closestDistance > 0.3) { - _logger.severe( - "Face ${embedding.key} is similar to $closestFaceID with distance $closestDistance, and float32 distance $closestDistance32, and float64 distance $closestDistance64", - ); - } - } - } - Future _onIgnoredClusterClicked(BuildContext context) async { await showChoiceDialog( context, @@ -246,14 +191,14 @@ class _AppBarWidgetState extends State { final breakupResult = await ClusterFeedbackService.instance .breakUpCluster(widget.clusterID); final Map> newClusterIDToFaceIDs = - breakupResult.newClusterIdToFaceIds!; + breakupResult.newClusterIdToFaceIds; final Map newFaceIdToClusterID = breakupResult.newFaceIdToCluster; // Update to delete the old clusters and save the new clusters await FaceMLDataDB.instance.deleteClusterSummary(widget.clusterID); await FaceMLDataDB.instance - .clusterSummaryUpdate(breakupResult.newClusterSummaries!); + .clusterSummaryUpdate(breakupResult.newClusterSummaries); await FaceMLDataDB.instance .updateFaceIdToClusterId(newFaceIdToClusterID); @@ -309,7 +254,7 @@ class _AppBarWidgetState extends State { await ClusterFeedbackService.instance.breakUpCluster(widget.clusterID); final Map> newClusterIDToFaceIDs = - breakupResult.newClusterIdToFaceIds!; + breakupResult.newClusterIdToFaceIds; final allFileIDs = newClusterIDToFaceIDs.values .expand((e) => e) diff --git a/mobile/lib/ui/viewer/people/cluster_page.dart b/mobile/lib/ui/viewer/people/cluster_page.dart index f6b720f023..285804f543 100644 --- a/mobile/lib/ui/viewer/people/cluster_page.dart +++ b/mobile/lib/ui/viewer/people/cluster_page.dart @@ -57,8 +57,7 @@ class _ClusterPageState extends State { late final StreamSubscription _filesUpdatedEvent; late final StreamSubscription _peopleChangedEvent; - bool get showNamingBanner => - (!userDismissedNamingBanner && widget.showNamingBanner); + bool get showNamingBanner => (!userDismissedNamingBanner && widget.showNamingBanner); bool userDismissedNamingBanner = false; @@ -67,8 +66,7 @@ class _ClusterPageState extends State { super.initState(); ClusterFeedbackService.setLastViewedClusterID(widget.clusterID); files = widget.searchResult; - _filesUpdatedEvent = - Bus.instance.on().listen((event) { + _filesUpdatedEvent = Bus.instance.on().listen((event) { if (event.type == EventType.deletedFromDevice || event.type == EventType.deletedFromEverywhere || event.type == EventType.deletedFromRemote || @@ -100,6 +98,9 @@ class _ClusterPageState extends State { void dispose() { _filesUpdatedEvent.cancel(); _peopleChangedEvent.cancel(); + if (ClusterFeedbackService.lastViewedClusterID == widget.clusterID) { + ClusterFeedbackService.resetLastViewedClusterID(); + } super.dispose(); } @@ -110,8 +111,7 @@ class _ClusterPageState extends State { final result = files .where( (file) => - file.creationTime! >= creationStartTime && - file.creationTime! <= creationEndTime, + file.creationTime! >= creationStartTime && file.creationTime! <= creationEndTime, ) .toList(); return Future.value( @@ -161,43 +161,44 @@ class _ClusterPageState extends State { ), ), showNamingBanner - ? Dismissible( - key: const Key("namingBanner"), - direction: DismissDirection.horizontal, - onDismissed: (direction) { - setState(() { - userDismissedNamingBanner = true; - }); - }, - child: PeopleBanner( - type: PeopleBannerType.addName, - faceWidget: PersonFaceWidget( - files.first, - clusterID: widget.clusterID, - ), - actionIcon: Icons.add_outlined, - text: S.of(context).addAName, - subText: S.of(context).findPeopleByName, - onTap: () async { - if (widget.personID == null) { - final result = await showAssignPersonAction( - context, - clusterID: widget.clusterID, - ); - if (result != null && - result is (PersonEntity, EnteFile)) { - Navigator.pop(context); - // ignore: unawaited_futures - routeToPage(context, PeoplePage(person: result.$1)); - } else if (result != null && result is PersonEntity) { - Navigator.pop(context); - // ignore: unawaited_futures - routeToPage(context, PeoplePage(person: result)); - } - } else { - showShortToast(context, "No personID or clusterID"); - } + ? SafeArea( + child: Dismissible( + key: const Key("namingBanner"), + direction: DismissDirection.horizontal, + onDismissed: (direction) { + setState(() { + userDismissedNamingBanner = true; + }); }, + child: PeopleBanner( + type: PeopleBannerType.addName, + faceWidget: PersonFaceWidget( + files.first, + clusterID: widget.clusterID, + ), + actionIcon: Icons.add_outlined, + text: S.of(context).addAName, + subText: S.of(context).findPeopleByName, + onTap: () async { + if (widget.personID == null) { + final result = await showAssignPersonAction( + context, + clusterID: widget.clusterID, + ); + if (result != null && result is (PersonEntity, EnteFile)) { + Navigator.pop(context); + // ignore: unawaited_futures + routeToPage(context, PeoplePage(person: result.$1)); + } else if (result != null && result is PersonEntity) { + Navigator.pop(context); + // ignore: unawaited_futures + routeToPage(context, PeoplePage(person: result)); + } + } else { + showShortToast(context, "No personID or clusterID"); + } + }, + ), ), ) : const SizedBox.shrink(), diff --git a/mobile/lib/ui/viewer/people/people_app_bar.dart b/mobile/lib/ui/viewer/people/people_app_bar.dart index d53059327f..828dff6bfd 100644 --- a/mobile/lib/ui/viewer/people/people_app_bar.dart +++ b/mobile/lib/ui/viewer/people/people_app_bar.dart @@ -18,7 +18,6 @@ import 'package:photos/ui/actions/collection/collection_sharing_actions.dart'; import "package:photos/ui/viewer/people/add_person_action_sheet.dart"; import "package:photos/ui/viewer/people/people_page.dart"; import "package:photos/ui/viewer/people/person_cluster_suggestion.dart"; -import 'package:photos/ui/viewer/people/person_clusters_page.dart'; import "package:photos/utils/dialog_util.dart"; import "package:photos/utils/navigation_util.dart"; @@ -46,8 +45,7 @@ enum PeoplePopupAction { rename, setCover, removeLabel, - viewPhotos, - confirmPhotos, + reviewSuggestions, unignore, } @@ -69,8 +67,7 @@ class _AppBarWidgetState extends State { }; collectionActions = CollectionActions(CollectionsService.instance); widget.selectedFiles.addListener(_selectedFilesListener); - _userAuthEventSubscription = - Bus.instance.on().listen((event) { + _userAuthEventSubscription = Bus.instance.on().listen((event) { setState(() {}); }); _appBarTitle = widget.title; @@ -91,8 +88,7 @@ class _AppBarWidgetState extends State { centerTitle: false, title: Text( _appBarTitle!, - style: - Theme.of(context).textTheme.headlineSmall!.copyWith(fontSize: 16), + style: Theme.of(context).textTheme.headlineSmall!.copyWith(fontSize: 16), maxLines: 2, overflow: TextOverflow.ellipsis, ), @@ -116,8 +112,7 @@ class _AppBarWidgetState extends State { } try { - await PersonService.instance - .updateAttributes(widget.person.remoteID, name: text); + await PersonService.instance.updateAttributes(widget.person.remoteID, name: text); if (mounted) { _appBarTitle = text; setState(() {}); @@ -137,8 +132,7 @@ class _AppBarWidgetState extends State { List _getDefaultActions(BuildContext context) { final List actions = []; // If the user has selected files, don't show any actions - if (widget.selectedFiles.files.isNotEmpty || - !Configuration.instance.hasConfiguredAccount()) { + if (widget.selectedFiles.files.isNotEmpty || !Configuration.instance.hasConfiguredAccount()) { return actions; } @@ -185,19 +179,7 @@ class _AppBarWidgetState extends State { ), ), const PopupMenuItem( - value: PeoplePopupAction.viewPhotos, - child: Row( - children: [ - Icon(Icons.view_array_outlined), - Padding( - padding: EdgeInsets.all(8), - ), - Text('View confirmed photos'), - ], - ), - ), - const PopupMenuItem( - value: PeoplePopupAction.confirmPhotos, + value: PeoplePopupAction.reviewSuggestions, child: Row( children: [ Icon(CupertinoIcons.square_stack_3d_down_right), @@ -236,22 +218,12 @@ class _AppBarWidgetState extends State { return items; }, onSelected: (PeoplePopupAction value) async { - if (value == PeoplePopupAction.viewPhotos) { + if (value == PeoplePopupAction.reviewSuggestions) { // ignore: unawaited_futures unawaited( Navigator.of(context).push( MaterialPageRoute( - builder: (context) => PersonClustersPage(widget.person), - ), - ), - ); - } else if (value == PeoplePopupAction.confirmPhotos) { - // ignore: unawaited_futures - unawaited( - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => - PersonReviewClusterSuggestion(widget.person), + builder: (context) => PersonReviewClusterSuggestion(widget.person), ), ), ); @@ -294,13 +266,11 @@ class _AppBarWidgetState extends State { bool assignName = false; await showChoiceDialog( context, - title: - "Are you sure you want to show this person in people section again?", + title: "Are you sure you want to show this person in people section again?", firstButtonLabel: "Yes, show person", firstButtonOnTap: () async { try { - await PersonService.instance - .deletePerson(widget.person.remoteID, onlyMapping: false); + await PersonService.instance.deletePerson(widget.person.remoteID, onlyMapping: false); Bus.instance.fire(PeopleChangedEvent()); assignName = true; } catch (e, s) { diff --git a/mobile/lib/ui/viewer/people/people_banner.dart b/mobile/lib/ui/viewer/people/people_banner.dart index db242a5230..e602d9aceb 100644 --- a/mobile/lib/ui/viewer/people/people_banner.dart +++ b/mobile/lib/ui/viewer/people/people_banner.dart @@ -68,65 +68,67 @@ class PeopleBanner extends StatelessWidget { roundedActionIcon = false; } - return RepaintBoundary( - child: Center( - child: GestureDetector( - onTap: onTap, - child: Container( - decoration: BoxDecoration( - boxShadow: Theme.of(context).colorScheme.enteTheme.shadowMenu, - color: backgroundColor, - ), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 2, vertical: 2), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - startWidget, - const SizedBox(width: 12), - Expanded( - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - text, - style: mainTextStyle, - textAlign: TextAlign.left, - ), - subText != null - ? const SizedBox(height: 6) - : const SizedBox.shrink(), - subText != null - ? Text( - subText!, - style: subTextStyle, - ) - : const SizedBox.shrink(), - ], + return SafeArea( + child: RepaintBoundary( + child: Center( + child: GestureDetector( + onTap: onTap, + child: Container( + decoration: BoxDecoration( + boxShadow: Theme.of(context).colorScheme.enteTheme.shadowMenu, + color: backgroundColor, + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 2, vertical: 2), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + startWidget, + const SizedBox(width: 12), + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + text, + style: mainTextStyle, + textAlign: TextAlign.left, + ), + subText != null + ? const SizedBox(height: 6) + : const SizedBox.shrink(), + subText != null + ? Text( + subText!, + style: subTextStyle, + ) + : const SizedBox.shrink(), + ], + ), ), - ), - const SizedBox(width: 12), - IconButtonWidget( - icon: actionIcon, - iconButtonType: IconButtonType.primary, - iconColor: colorScheme.strokeBase, - defaultColor: colorScheme.fillFaint, - pressedColor: colorScheme.fillMuted, - roundedIcon: roundedActionIcon, - onTap: onTap, - ), - const SizedBox(width: 6), - ], + const SizedBox(width: 12), + IconButtonWidget( + icon: actionIcon, + iconButtonType: IconButtonType.primary, + iconColor: colorScheme.strokeBase, + defaultColor: colorScheme.fillFaint, + pressedColor: colorScheme.fillMuted, + roundedIcon: roundedActionIcon, + onTap: onTap, + ), + const SizedBox(width: 6), + ], + ), ), ), ), - ), - ).animate(onPlay: (controller) => controller.repeat()).shimmer( - duration: 1000.ms, - delay: 3200.ms, - size: 0.6, - ), + ).animate(onPlay: (controller) => controller.repeat()).shimmer( + duration: 1000.ms, + delay: 3200.ms, + size: 0.6, + ), + ), ); } } diff --git a/mobile/lib/ui/viewer/people/person_cluster_suggestion.dart b/mobile/lib/ui/viewer/people/person_cluster_suggestion.dart index 2a904720bb..78d4fbe118 100644 --- a/mobile/lib/ui/viewer/people/person_cluster_suggestion.dart +++ b/mobile/lib/ui/viewer/people/person_cluster_suggestion.dart @@ -10,14 +10,21 @@ import "package:photos/face/db.dart"; import "package:photos/face/model/person.dart"; import "package:photos/models/file/file.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/theme/ente_theme.dart"; import "package:photos/ui/components/buttons/button_widget.dart"; import "package:photos/ui/components/models/button_type.dart"; -// import "package:photos/ui/viewer/people/add_person_action_sheet.dart"; import "package:photos/ui/viewer/people/cluster_page.dart"; import "package:photos/ui/viewer/people/person_clusters_page.dart"; import "package:photos/ui/viewer/search/result/person_face_widget.dart"; +class SuggestionUserFeedback { + final bool accepted; + final ClusterSuggestion suggestion; + + SuggestionUserFeedback(this.accepted, this.suggestion); +} + class PersonReviewClusterSuggestion extends StatefulWidget { final PersonEntity person; @@ -36,6 +43,8 @@ class _PersonClustersState extends State { Key futureBuilderKeySuggestions = UniqueKey(); Key futureBuilderKeyFaceThumbnails = UniqueKey(); bool canGiveFeedback = true; + List pastUserFeedback = []; + List allSuggestions = []; // Declare a variable for the future late Future> futureClusterSuggestions; @@ -61,6 +70,13 @@ class _PersonClustersState extends State { appBar: AppBar( title: const Text('Review suggestions'), actions: [ + if (pastUserFeedback.isNotEmpty) + IconButton( + icon: const Icon(Icons.undo_outlined), + onPressed: () async { + await _undoLastFeedback(); + }, + ), IconButton( icon: const Icon(Icons.history_outlined), onPressed: () { @@ -87,7 +103,7 @@ class _PersonClustersState extends State { ); } - final allSuggestions = snapshot.data!; + allSuggestions = snapshot.data!; final numberOfDifferentSuggestions = allSuggestions.length; final currentSuggestion = allSuggestions[currentSuggestionIndex]; final int clusterID = currentSuggestion.clusterIDToMerge; @@ -112,7 +128,7 @@ class _PersonClustersState extends State { setState(() {}); } }); - return InkWell( + return GestureDetector( onTap: () { final List sortedFiles = List.from(currentSuggestion.filesInCluster); @@ -130,6 +146,7 @@ class _PersonClustersState extends State { ), ); }, + behavior: HitTestBehavior.opaque, child: Container( padding: const EdgeInsets.symmetric( horizontal: 8.0, @@ -166,6 +183,13 @@ class _PersonClustersState extends State { if (!canGiveFeedback) { return; } + // Store the feedback in case the user wants to revert + pastUserFeedback.add( + SuggestionUserFeedback( + yesOrNo, + allSuggestions[currentSuggestionIndex], + ), + ); if (yesOrNo) { canGiveFeedback = false; await FaceMLDataDB.instance.assignClusterToPerson( @@ -240,7 +264,6 @@ class _PersonClustersState extends State { style: getEnteTextTheme(context).smallMuted, ), Text( - // TODO: come up with a better copy for strings below! "${widget.person.data.name}?", style: getEnteTextTheme(context).largeMuted, ), @@ -449,4 +472,35 @@ class _PersonClustersState extends State { } return faceCrops; } + + Future _undoLastFeedback() async { + if (pastUserFeedback.isNotEmpty) { + final SuggestionUserFeedback lastFeedback = pastUserFeedback.removeLast(); + if (lastFeedback.accepted) { + await PersonService.instance.removeClusterToPerson( + personID: widget.person.remoteID, + clusterID: lastFeedback.suggestion.clusterIDToMerge, + ); + } else { + await FaceMLDataDB.instance.removeNotPersonFeedback( + personID: widget.person.remoteID, + clusterID: lastFeedback.suggestion.clusterIDToMerge, + ); + } + + // futureClusterSuggestions = + // pastUserFeedback.map((element) => element.suggestion) + // as Future>; + + fetch = false; + futureClusterSuggestions = futureClusterSuggestions.then((list) { + return list.sublist(currentSuggestionIndex) + ..insert(0, lastFeedback.suggestion); + }); + currentSuggestionIndex = 0; + futureBuilderKeySuggestions = UniqueKey(); + futureBuilderKeyFaceThumbnails = UniqueKey(); + setState(() {}); + } + } } diff --git a/mobile/lib/ui/viewer/people/person_clusters_page.dart b/mobile/lib/ui/viewer/people/person_clusters_page.dart index 2c493fc21f..16d1131682 100644 --- a/mobile/lib/ui/viewer/people/person_clusters_page.dart +++ b/mobile/lib/ui/viewer/people/person_clusters_page.dart @@ -9,7 +9,6 @@ import "package:photos/services/machine_learning/face_ml/person/person_service.d import "package:photos/services/search_service.dart"; import "package:photos/theme/ente_theme.dart"; import "package:photos/ui/viewer/file/no_thumbnail_widget.dart"; -// import "package:photos/ui/viewer/file/thumbnail_widget.dart"; import "package:photos/ui/viewer/people/cluster_page.dart"; import "package:photos/ui/viewer/search/result/person_face_widget.dart"; @@ -34,16 +33,20 @@ class _PersonClustersPageState extends State { title: Text(widget.person.data.name), ), body: FutureBuilder>>( - future: SearchService.instance - .getClusterFilesForPersonID(widget.person.remoteID), + future: SearchService.instance.getClusterFilesForPersonID(widget.person.remoteID), builder: (context, snapshot) { if (snapshot.hasData) { - final List keys = snapshot.data!.keys.toList(); + final clusters = snapshot.data!; + final List keys = clusters.keys.toList(); + // Sort the clusters by the number of files in each cluster, largest first + keys.sort( + (b, a) => clusters[a]!.length.compareTo(clusters[b]!.length), + ); return ListView.builder( itemCount: keys.length, itemBuilder: (context, index) { final int clusterID = keys[index]; - final List files = snapshot.data![keys[index]]!; + final List files = clusters[clusterID]!; return InkWell( onTap: () { Navigator.of(context).push( @@ -52,6 +55,7 @@ class _PersonClustersPageState extends State { files, personID: widget.person, clusterID: index, + showNamingBanner: false, ), ), ); @@ -87,40 +91,40 @@ class _PersonClustersPageState extends State { ), // Add some spacing between the thumbnail and the text Expanded( child: Padding( - padding: - const EdgeInsets.symmetric(horizontal: 8.0), + padding: const EdgeInsets.symmetric(horizontal: 8.0), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - "${snapshot.data![keys[index]]!.length} photos", + "${files.length} photos", style: getEnteTextTheme(context).body, ), - GestureDetector( - onTap: () async { - try { - await PersonService.instance - .removeClusterToPerson( - personID: widget.person.remoteID, - clusterID: clusterID, - ); - _logger.info( - "Removed cluster $clusterID from person ${widget.person.remoteID}", - ); - Bus.instance.fire(PeopleChangedEvent()); - setState(() {}); - } catch (e) { - _logger.severe( - "removing cluster from person,", - e, - ); - } - }, - child: const Icon( - CupertinoIcons.minus_circled, - color: Colors.red, - ), - ), + (index != 0) + ? GestureDetector( + onTap: () async { + try { + await PersonService.instance.removeClusterToPerson( + personID: widget.person.remoteID, + clusterID: clusterID, + ); + _logger.info( + "Removed cluster $clusterID from person ${widget.person.remoteID}", + ); + Bus.instance.fire(PeopleChangedEvent()); + setState(() {}); + } catch (e) { + _logger.severe( + "removing cluster from person,", + e, + ); + } + }, + child: const Icon( + CupertinoIcons.minus_circled, + color: Colors.red, + ), + ) + : const SizedBox.shrink(), ], ), ), diff --git a/mobile/lib/ui/viewer/people/person_row_item.dart b/mobile/lib/ui/viewer/people/person_row_item.dart index 831fe97298..ed1fc9fa27 100644 --- a/mobile/lib/ui/viewer/people/person_row_item.dart +++ b/mobile/lib/ui/viewer/people/person_row_item.dart @@ -19,6 +19,8 @@ class PersonRowItem extends StatelessWidget { Widget build(BuildContext context) { return ListTile( dense: false, + minLeadingWidth: 0, + contentPadding: const EdgeInsets.symmetric(horizontal: 0), leading: SizedBox( width: 56, height: 56, diff --git a/mobile/lib/ui/viewer/search/result/person_face_widget.dart b/mobile/lib/ui/viewer/search/result/person_face_widget.dart index 8be99e5f6e..57fe5af654 100644 --- a/mobile/lib/ui/viewer/search/result/person_face_widget.dart +++ b/mobile/lib/ui/viewer/search/result/person_face_widget.dart @@ -121,7 +121,7 @@ class PersonFaceWidget extends StatelessWidget { ); } - Future getFaceCrop() async { + Future getFaceCrop({int fetchAttempt = 1}) async { try { final Face? face = await _getFace(); if (face == null) { @@ -187,6 +187,10 @@ class PersonFaceWidget extends StatelessWidget { error: e, stackTrace: s, ); + resetPool(fullFile: useFullFile); + if(fetchAttempt <= retryLimit) { + return getFaceCrop(fetchAttempt: fetchAttempt + 1); + } return null; } } diff --git a/mobile/lib/utils/face/face_box_crop.dart b/mobile/lib/utils/face/face_box_crop.dart index 281c0ef495..4dd1778f72 100644 --- a/mobile/lib/utils/face/face_box_crop.dart +++ b/mobile/lib/utils/face/face_box_crop.dart @@ -11,11 +11,20 @@ import "package:photos/utils/image_ml_isolate.dart"; import "package:photos/utils/thumbnail_util.dart"; import "package:pool/pool.dart"; +void resetPool({required bool fullFile}) { + if (fullFile) { + poolFullFileFaceGenerations = Pool(20, timeout: const Duration(seconds: 15)); + } else { + poolThumbnailFaceGenerations = Pool(100, timeout: const Duration(seconds: 15)); + } +} + +const int retryLimit = 3; final LRUMap faceCropCache = LRUMap(1000); final LRUMap faceCropThumbnailCache = LRUMap(1000); -final poolFullFileFaceGenerations = +Pool poolFullFileFaceGenerations = Pool(20, timeout: const Duration(seconds: 15)); -final poolThumbnailFaceGenerations = +Pool poolThumbnailFaceGenerations = Pool(100, timeout: const Duration(seconds: 15)); Future?> getFaceCrops( EnteFile file, diff --git a/mobile/lib/utils/primitive_wrapper.dart b/mobile/lib/utils/primitive_wrapper.dart new file mode 100644 index 0000000000..20ea9bbb6e --- /dev/null +++ b/mobile/lib/utils/primitive_wrapper.dart @@ -0,0 +1,6 @@ +///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/sqlite_util.dart b/mobile/lib/utils/sqlite_util.dart new file mode 100644 index 0000000000..b83bd58e15 --- /dev/null +++ b/mobile/lib/utils/sqlite_util.dart @@ -0,0 +1,39 @@ +enum SqliteAsyncConflictAlgorithm { + /// When a constraint violation occurs, an immediate ROLLBACK occurs, + /// thus ending the current transaction, and the command aborts with a + /// return code of SQLITE_CONSTRAINT. If no transaction is active + /// (other than the implied transaction that is created on every command) + /// then this algorithm works the same as ABORT. + rollback, + + /// When a constraint violation occurs,no ROLLBACK is executed + /// so changes from prior commands within the same transaction + /// are preserved. This is the default behavior. + abort, + + /// When a constraint violation occurs, the command aborts with a return + /// code SQLITE_CONSTRAINT. But any changes to the database that + /// the command made prior to encountering the constraint violation + /// are preserved and are not backed out. + fail, + + /// When a constraint violation occurs, the one row that contains + /// the constraint violation is not inserted or changed. + /// But the command continues executing normally. Other rows before and + /// after the row that contained the constraint violation continue to be + /// inserted or updated normally. No error is returned. + ignore, + + /// When a UNIQUE constraint violation occurs, the pre-existing rows that + /// are causing the constraint violation are removed prior to inserting + /// or updating the current row. Thus the insert or update always occurs. + /// The command continues executing normally. No error is returned. + /// If a NOT NULL constraint violation occurs, the NULL value is replaced + /// by the default value for that column. If the column has no default + /// value, then the ABORT algorithm is used. If a CHECK constraint + /// violation occurs then the IGNORE algorithm is used. When this conflict + /// resolution strategy deletes rows in order to satisfy a constraint, + /// it does not invoke delete triggers on those rows. + /// This behavior might change in a future release. + replace, +} diff --git a/mobile/lib/utils/thumbnail_util.dart b/mobile/lib/utils/thumbnail_util.dart index db7648b92b..052f499ff2 100644 --- a/mobile/lib/utils/thumbnail_util.dart +++ b/mobile/lib/utils/thumbnail_util.dart @@ -184,7 +184,7 @@ Future _downloadAndDecryptThumbnail(FileDownloadItem item) async { CryptoUtil.base642bin(file.thumbnailDecryptionHeader!), ); } catch (e, s) { - _logger.severe("Failed to decrypt thumbnail", e, s); + _logger.severe("Failed to decrypt thumbnail ${item.file.toString()}", e, s); item.completer.completeError(e); return; } diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index 8b71025e9f..97f5780063 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -764,26 +764,26 @@ packages: dependency: "direct main" description: name: flutter_map - sha256: "5286f72f87deb132daa1489442d6cc46e986fc105cb727d9ae1b602b35b1d1f3" + sha256: "87cc8349b8fa5dccda5af50018c7374b6645334a0d680931c1fe11bce88fa5bb" url: "https://pub.dev" source: hosted - version: "5.0.0" + version: "6.2.1" flutter_map_marker_cluster: dependency: "direct main" description: name: flutter_map_marker_cluster - sha256: "14bb31b9dd3a759ab4a1ba320d19bbb554d8d7952c8812029c6f6b7bda956906" + sha256: a324f48da5ee83a3f29fd8d08b4b1e6e3114ff5c6cab910124d6a2e1f06f08cc url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.6" flutter_map_marker_popup: dependency: transitive description: name: flutter_map_marker_popup - sha256: be209c68b19d4c10d9a2f5911e45f7c579624c43a353adb9bf0f2fec0cf30b8c + sha256: ec563bcbae24a18ac16815fb75ac5ab33ccba609e14db70e252a67de19c6639c url: "https://pub.dev" source: hosted - version: "5.2.0" + version: "6.1.2" flutter_native_splash: dependency: "direct main" description: @@ -1252,6 +1252,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.10" + logger: + dependency: transitive + description: + name: logger + sha256: af05cc8714f356fd1f3888fb6741cbe9fbe25cdb6eedbab80e1a6db21047d4a4 + url: "https://pub.dev" + source: hosted + version: "2.3.0" logging: dependency: "direct main" description: diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 1417d17f3e..9455d9665f 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.8.110+634 +version: 0.8.130+650 publish_to: none environment: @@ -83,8 +83,8 @@ dependencies: flutter_local_notifications: ^17.0.0 flutter_localizations: sdk: flutter - flutter_map: ^5.0.0 - flutter_map_marker_cluster: ^1.2.0 + flutter_map: ^6.2.0 + 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 diff --git a/web/apps/accounts/.eslintrc.js b/web/apps/accounts/.eslintrc.js index b1c4c2e168..f133a685b7 100644 --- a/web/apps/accounts/.eslintrc.js +++ b/web/apps/accounts/.eslintrc.js @@ -9,5 +9,5 @@ module.exports = { tsconfigRootDir: __dirname, project: "./tsconfig.json", }, - ignorePatterns: [".eslintrc.js", "out"], + ignorePatterns: [".eslintrc.js", "out", "next.config.js"], }; diff --git a/web/apps/auth/src/pages/auth.tsx b/web/apps/auth/src/pages/auth.tsx index 80a342f38a..9c49ffbed3 100644 --- a/web/apps/auth/src/pages/auth.tsx +++ b/web/apps/auth/src/pages/auth.tsx @@ -3,13 +3,14 @@ import { HorizontalFlex, VerticallyCentered, } from "@ente/shared/components/Container"; +import type { DialogBoxAttributesV2 } from "@ente/shared/components/DialogBoxV2/types"; import { EnteLogo } from "@ente/shared/components/EnteLogo"; import EnteSpinner from "@ente/shared/components/EnteSpinner"; import NavbarBase from "@ente/shared/components/Navbar/base"; import OverflowMenu from "@ente/shared/components/OverflowMenu/menu"; import { OverflowMenuOption } from "@ente/shared/components/OverflowMenu/option"; import { AUTH_PAGES as PAGES } from "@ente/shared/constants/pages"; -import { CustomError } from "@ente/shared/error"; +import { ApiError, CustomError } from "@ente/shared/error"; import InMemoryStore, { MS_KEYS } from "@ente/shared/storage/InMemoryStore"; import LogoutOutlined from "@mui/icons-material/LogoutOutlined"; import MoreHoriz from "@mui/icons-material/MoreHoriz"; @@ -22,12 +23,17 @@ import { generateOTPs, type Code } from "services/code"; import { getAuthCodes } from "services/remote"; const Page: React.FC = () => { - const appContext = ensure(useContext(AppContext)); + const { logout, showNavBar, setDialogBoxAttributesV2 } = ensure( + useContext(AppContext), + ); const router = useRouter(); const [codes, setCodes] = useState([]); const [hasFetched, setHasFetched] = useState(false); const [searchTerm, setSearchTerm] = useState(""); + const showSessionExpiredDialog = () => + setDialogBoxAttributesV2(sessionExpiredDialogAttributes(logout)); + useEffect(() => { const fetchCodes = async () => { try { @@ -39,6 +45,9 @@ const Page: React.FC = () => { ) { InMemoryStore.set(MS_KEYS.REDIRECT_URL, PAGES.AUTH); router.push(PAGES.ROOT); + } else if (e instanceof ApiError && e.httpStatusCode == 401) { + // We get back a 401 Unauthorized if the token is not valid. + showSessionExpiredDialog(); } else { // do not log errors } @@ -46,7 +55,7 @@ const Page: React.FC = () => { setHasFetched(true); }; void fetchCodes(); - appContext.showNavBar(false); + showNavBar(false); }, []); const lcSearch = searchTerm.toLowerCase(); @@ -131,6 +140,19 @@ const Page: React.FC = () => { export default Page; +const sessionExpiredDialogAttributes = ( + action: () => void, +): DialogBoxAttributesV2 => ({ + title: t("SESSION_EXPIRED"), + content: t("SESSION_EXPIRED_MESSAGE"), + nonClosable: true, + proceed: { + text: t("LOGIN"), + action, + variant: "accent", + }, +}); + const AuthNavbar: React.FC = () => { const { isMobile, logout } = ensure(useContext(AppContext)); diff --git a/web/apps/auth/src/services/code.ts b/web/apps/auth/src/services/code.ts index 1df085b4e3..c604bae0ce 100644 --- a/web/apps/auth/src/services/code.ts +++ b/web/apps/auth/src/services/code.ts @@ -84,20 +84,6 @@ export const codeFromURIString = (id: string, uriString: string): Code => { const _codeFromURIString = (id: string, uriString: string): Code => { const url = new URL(uriString); - // A URL like - // - // new URL("otpauth://hotp/Test?secret=AAABBBCCCDDDEEEFFF&issuer=Test&counter=0") - // - // is parsed differently by the browser and Node depending on the scheme. - // When the scheme is http(s), then both of them consider "hotp" as the - // `host`. However, when the scheme is "otpauth", as is our case, the - // browser considers the entire thing as part of the pathname. so we get. - // - // host: "" - // pathname: "//hotp/Test" - // - // Since this code run on browsers only, we parse as per that behaviour. - const [type, path] = parsePathname(url); return { @@ -115,10 +101,46 @@ const _codeFromURIString = (id: string, uriString: string): Code => { }; const parsePathname = (url: URL): [type: Code["type"], path: string] => { + // A URL like + // + // new + // URL("otpauth://hotp/Test?secret=AAABBBCCCDDDEEEFFF&issuer=Test&counter=0") + // + // is parsed differently by different browsers, and there are differences + // even depending on the scheme. + // + // When the scheme is http(s), then all of them consider "hotp" as the + // `host`. However, when the scheme is "otpauth", as is our case, Safari + // splits it into + // + // host: "hotp" + // pathname: "/Test" + // + // while Chrome and Firefox consider the entire thing as part of the + // pathname + // + // host: "" + // pathname: "//hotp/Test" + // + // So we try to handle both scenarios by first checking for the host match, + // and if not fall back to deducing the "host" from the pathname. + + switch (url.host.toLowerCase()) { + case "totp": + return ["totp", url.pathname.toLowerCase()]; + case "hotp": + return ["hotp", url.pathname.toLowerCase()]; + case "steam": + return ["steam", url.pathname.toLowerCase()]; + default: + break; + } + const p = url.pathname.toLowerCase(); if (p.startsWith("//totp")) return ["totp", url.pathname.slice(6)]; if (p.startsWith("//hotp")) return ["hotp", url.pathname.slice(6)]; if (p.startsWith("//steam")) return ["steam", url.pathname.slice(7)]; + throw new Error(`Unsupported code or unparseable path "${url.pathname}"`); }; diff --git a/web/apps/cast/.eslintrc.js b/web/apps/cast/.eslintrc.js index b1c4c2e168..f133a685b7 100644 --- a/web/apps/cast/.eslintrc.js +++ b/web/apps/cast/.eslintrc.js @@ -9,5 +9,5 @@ module.exports = { tsconfigRootDir: __dirname, project: "./tsconfig.json", }, - ignorePatterns: [".eslintrc.js", "out"], + ignorePatterns: [".eslintrc.js", "out", "next.config.js"], }; diff --git a/web/apps/photos/.eslintrc.js b/web/apps/photos/.eslintrc.js index fdec1a6b99..4eabc44fff 100644 --- a/web/apps/photos/.eslintrc.js +++ b/web/apps/photos/.eslintrc.js @@ -9,5 +9,11 @@ module.exports = { tsconfigRootDir: __dirname, project: "./tsconfig.json", }, - ignorePatterns: [".eslintrc.js", "out", "thirdparty", "public"], + ignorePatterns: [ + ".eslintrc.js", + "out", + "thirdparty", + "public", + "next.config.js", + ], }; diff --git a/web/apps/photos/package.json b/web/apps/photos/package.json index 0ec924b29b..9c07cc7aa6 100644 --- a/web/apps/photos/package.json +++ b/web/apps/photos/package.json @@ -4,6 +4,7 @@ "private": true, "dependencies": { "@/media": "*", + "@/new": "*", "@/next": "*", "@date-io/date-fns": "^2.14.0", "@ente/accounts": "*", @@ -22,7 +23,7 @@ "ffmpeg-wasm": "file:./thirdparty/ffmpeg-wasm", "formik": "^2.1.5", "hdbscan": "0.0.1-alpha.5", - "idb": "^7.1.1", + "idb": "^8", "leaflet": "^1.9.4", "leaflet-defaulticon-compatibility": "^0.1.1", "localforage": "^1.9.0", diff --git a/web/apps/photos/src/components/PhotoViewer/FileInfo/index.tsx b/web/apps/photos/src/components/PhotoViewer/FileInfo/index.tsx index a6d37ccf49..5a17f43d17 100644 --- a/web/apps/photos/src/components/PhotoViewer/FileInfo/index.tsx +++ b/web/apps/photos/src/components/PhotoViewer/FileInfo/index.tsx @@ -11,7 +11,7 @@ import { Box, DialogProps, Link, Stack, styled } from "@mui/material"; import { Chip } from "components/Chip"; import { EnteDrawer } from "components/EnteDrawer"; import Titlebar from "components/Titlebar"; -import { PhotoPeopleList, UnidentifiedFaces } from "components/ml/PeopleList"; +import { UnidentifiedFaces } from "components/ml/PeopleList"; import LinkButton from "components/pages/gallery/LinkButton"; import { t } from "i18next"; import { AppContext } from "pages/_app"; @@ -96,8 +96,6 @@ export function FileInfo({ const [parsedExifData, setParsedExifData] = useState>(); const [showExif, setShowExif] = useState(false); - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const [updateMLDataIndex, setUpdateMLDataIndex] = useState(0); const openExif = () => setShowExif(true); const closeExif = () => setShowExif(false); @@ -332,14 +330,8 @@ export function FileInfo({ {appContext.mlSearchEnabled && ( <> - - + {/* */} + )} diff --git a/web/apps/photos/src/components/Search/SearchBar/searchInput/MenuWithPeople.tsx b/web/apps/photos/src/components/Search/SearchBar/searchInput/MenuWithPeople.tsx index 3b739520e2..fd073af643 100644 --- a/web/apps/photos/src/components/Search/SearchBar/searchInput/MenuWithPeople.tsx +++ b/web/apps/photos/src/components/Search/SearchBar/searchInput/MenuWithPeople.tsx @@ -5,7 +5,6 @@ import { t } from "i18next"; import { AppContext } from "pages/_app"; import { useContext } from "react"; import { components } from "react-select"; -import { IndexStatus } from "services/face/db"; import { Suggestion, SuggestionType } from "types/search"; const { Menu } = components; @@ -35,7 +34,7 @@ const MenuWithPeople = (props) => { (o) => o.type === SuggestionType.INDEX_STATUS, )[0] as Suggestion; - const indexStatus = indexStatusSuggestion?.value as IndexStatus; + const indexStatus = indexStatusSuggestion?.value; return ( diff --git a/web/apps/photos/src/components/Sidebar/index.tsx b/web/apps/photos/src/components/Sidebar/index.tsx index 92a69b1ae1..489536e085 100644 --- a/web/apps/photos/src/components/Sidebar/index.tsx +++ b/web/apps/photos/src/components/Sidebar/index.tsx @@ -83,7 +83,6 @@ import { } from "utils/billing"; import { openLink } from "utils/common"; import { getDownloadAppMessage } from "utils/ui"; -import { isInternalUser } from "utils/user"; import { isFamilyAdmin, isPartOfFamily } from "utils/user/family"; import { testUpload } from "../../../tests/upload.test"; import { MemberSubscriptionManage } from "../MemberSubscriptionManage"; @@ -553,7 +552,7 @@ const UtilitySection: React.FC = ({ closeSidebar }) => { onClick={openRecoveryKeyModal} label={t("RECOVERY_KEY")} /> - {isInternalUser() && ( + {isInternalUserViaEmailCheck() && ( = ({ closeSidebar }) => { label={t("TWO_FACTOR")} /> - {isInternalUser() && ( + {isInternalUserViaEmailCheck() && ( { {appVersion} )} - {isInternalUser() && ( + {isInternalUserViaEmailCheck() && ( { ); }; + +// TODO: Legacy synchronous check, use the one for feature-flags.ts instead. +const isInternalUserViaEmailCheck = () => { + const userEmail = getData(LS_KEYS.USER)?.email; + if (!userEmail) return false; + + return userEmail.endsWith("@ente.io"); +}; diff --git a/web/apps/photos/src/components/WatchFolder.tsx b/web/apps/photos/src/components/WatchFolder.tsx index 812f54c6b7..e99fbb0073 100644 --- a/web/apps/photos/src/components/WatchFolder.tsx +++ b/web/apps/photos/src/components/WatchFolder.tsx @@ -153,12 +153,12 @@ const Title_ = styled("div")` `; interface WatchList { - watches: FolderWatch[]; + watches: FolderWatch[] | undefined; removeWatch: (watch: FolderWatch) => void; } const WatchList: React.FC = ({ watches, removeWatch }) => { - return watches.length === 0 ? ( + return (watches ?? []).length === 0 ? ( ) : ( diff --git a/web/apps/photos/src/components/ml/MLSearchSettings.tsx b/web/apps/photos/src/components/ml/MLSearchSettings.tsx index d71dffab7e..64c223618b 100644 --- a/web/apps/photos/src/components/ml/MLSearchSettings.tsx +++ b/web/apps/photos/src/components/ml/MLSearchSettings.tsx @@ -18,11 +18,11 @@ import { t } from "i18next"; import { AppContext } from "pages/_app"; import { useContext, useEffect, useState } from "react"; import { Trans } from "react-i18next"; +import { canEnableFaceIndexing } from "services/face/indexer"; import { getFaceSearchEnabledStatus, updateFaceSearchEnabledStatus, } from "services/userService"; -import { isInternalUserForML } from "utils/user"; export const MLSearchSettings = ({ open, onClose, onRootClose }) => { const { @@ -60,6 +60,7 @@ export const MLSearchSettings = ({ open, onClose, onRootClose }) => { const enableFaceSearch = async () => { try { startLoading(); + // Update the consent flag. await updateFaceSearchEnabledStatus(true); updateMlSearchEnabled(true); closeEnableFaceSearch(); @@ -83,7 +84,6 @@ export const MLSearchSettings = ({ open, onClose, onRootClose }) => { const disableFaceSearch = async () => { try { startLoading(); - await updateFaceSearchEnabledStatus(false); await disableMlSearch(); finishLoading(); } catch (e) { @@ -258,6 +258,12 @@ function EnableMLSearch({ onClose, enableMlSearch, onRootClose }) { // const showDetails = () => // openLink("https://ente.io/blog/desktop-ml-beta", true); + const [canEnable, setCanEnable] = useState(false); + + useEffect(() => { + canEnableFaceIndexing().then((v) => setCanEnable(v)); + }, []); + return ( - - {" "} - - {/* */} - We're putting finishing touches, coming back soon! - - - {isInternalUserForML() && ( + {canEnable ? (