diff --git a/.github/workflows/auth-crowdin.yml b/.github/workflows/auth-crowdin.yml index 811def9396c6db575a0a6b33c18ea73566064aca..bd92f145910c0df5fabfa19bfac1ceb2e9dd0e6b 100644 --- a/.github/workflows/auth-crowdin.yml +++ b/.github/workflows/auth-crowdin.yml @@ -30,7 +30,7 @@ jobs: upload_sources: true upload_translations: false download_translations: true - localization_branch_name: crowdin-translations-auth + localization_branch_name: translations/auth create_pull_request: true skip_untranslated_strings: true pull_request_title: "[auth] New translations" diff --git a/.github/workflows/auth-lint.yml b/.github/workflows/auth-lint.yml index 6504e0646ae3883b7de0807e8b785f8c31e51262..63d644c2e334ab2671b41c1de89a98aea726494a 100644 --- a/.github/workflows/auth-lint.yml +++ b/.github/workflows/auth-lint.yml @@ -3,7 +3,7 @@ name: "Lint (auth)" on: # Run on every push to a branch other than main that changes auth/ push: - branches-ignore: [main, "deploy/**"] + branches-ignore: [main, "deploy/**", "deploy-f/**"] paths: - "auth/**" - ".github/workflows/auth-lint.yml" diff --git a/.github/workflows/auth-release.yml b/.github/workflows/auth-release.yml index cc3e598e365b112bc819ab6437db254cb9d89c3b..174b6c1d33d8c1e8c1a8ab33d342aa6093a0a713 100644 --- a/.github/workflows/auth-release.yml +++ b/.github/workflows/auth-release.yml @@ -17,8 +17,8 @@ name: "Release (auth)" # We use a suffix like `-test` to indicate that these are test tags, and that # they belong to a pre-release. # -# If you need to do multiple tests, add a +x at the end of the tag. e.g. -# `auth-v1.2.3-test+1`. +# If you need to do multiple tests, add a .x at the end of the tag. e.g. +# `auth-v1.2.3-test.1`. # # Once the testing is done, also delete the tag(s) please. @@ -85,7 +85,7 @@ jobs: - name: Install dependencies for desktop build run: | sudo apt-get update -y - sudo apt-get install -y libsecret-1-dev libsodium-dev libwebkit2gtk-4.0-dev libfuse2 ninja-build libgtk-3-dev dpkg-dev pkg-config rpm libsqlite3-dev locate appindicator3-0.1 libappindicator3-dev libffi-dev libtiff5 + sudo apt-get install -y libsecret-1-dev libsodium-dev libwebkit2gtk-4.0-dev libfuse2 ninja-build libgtk-3-dev dpkg-dev pkg-config rpm patchelf libsqlite3-dev locate appindicator3-0.1 libappindicator3-dev libffi-dev libtiff5 sudo updatedb --localpaths='/usr/lib/x86_64-linux-gnu' - name: Install appimagetool diff --git a/.github/workflows/desktop-lint.yml b/.github/workflows/desktop-lint.yml new file mode 100644 index 0000000000000000000000000000000000000000..0c1929e6ac4c70f56e42c9805a8e3b5a6c16d3b3 --- /dev/null +++ b/.github/workflows/desktop-lint.yml @@ -0,0 +1,30 @@ +name: "Lint (desktop)" + +on: + # Run on every push to a branch other than main that changes desktop/ + push: + branches-ignore: [main, "deploy/**", "deploy-f/**"] + paths: + - "desktop/**" + - ".github/workflows/desktop-lint.yml" + +jobs: + lint: + runs-on: ubuntu-latest + defaults: + run: + working-directory: desktop + 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: "desktop/yarn.lock" + + - run: yarn install + + - run: yarn lint diff --git a/.github/workflows/docs-verify-build.yml b/.github/workflows/docs-verify-build.yml index a57f71c8687cae39f91ae72e1ac0eb10260185ba..5d31ff8378aa568c8995489dbabe6b22794231d7 100644 --- a/.github/workflows/docs-verify-build.yml +++ b/.github/workflows/docs-verify-build.yml @@ -6,7 +6,7 @@ name: "Verify build (docs)" on: # Run on every push to a branch other than main that changes docs/ push: - branches-ignore: [main, "deploy/**"] + branches-ignore: [main, "deploy/**", "deploy-f/**"] paths: - "docs/**" - ".github/workflows/docs-verify-build.yml" diff --git a/.github/workflows/mobile-crowdin.yml b/.github/workflows/mobile-crowdin.yml index 5c52b59ad12ac1480440ab10ac1045d74cef2025..556ac45f24ea8b69944f58c321f817a531a64002 100644 --- a/.github/workflows/mobile-crowdin.yml +++ b/.github/workflows/mobile-crowdin.yml @@ -30,7 +30,7 @@ jobs: upload_sources: true upload_translations: false download_translations: true - localization_branch_name: crowdin-translations-mobile + localization_branch_name: translations/mobile create_pull_request: true skip_untranslated_strings: true pull_request_title: "[mobile] New translations" diff --git a/.github/workflows/mobile-internal-release.yml b/.github/workflows/mobile-internal-release.yml index 9779a5d7aa957a97846e361ff1b51aff3ad771e4..4ee7367424ddad30fab21c7d642d922f9771dc82 100644 --- a/.github/workflows/mobile-internal-release.yml +++ b/.github/workflows/mobile-internal-release.yml @@ -54,3 +54,4 @@ jobs: packageName: io.ente.photos releaseFiles: mobile/build/app/outputs/bundle/playstoreRelease/app-playstore-release.aab track: internal + changesNotSentForReview: true diff --git a/.github/workflows/mobile-lint.yml b/.github/workflows/mobile-lint.yml index 57b2ca4dbd16d137e583d95ece5b69828dcb14c1..8abc6f0c73ae400c98fc6c6d027806dd5118a791 100644 --- a/.github/workflows/mobile-lint.yml +++ b/.github/workflows/mobile-lint.yml @@ -3,7 +3,7 @@ name: "Lint (mobile)" on: # Run on every push to a branch other than main that changes mobile/ push: - branches-ignore: [main, f-droid, "deploy/**"] + branches-ignore: [main, f-droid, "deploy/**", "deploy-f/**"] paths: - "mobile/**" - ".github/workflows/mobile-lint.yml" diff --git a/.github/workflows/server-lint.yml b/.github/workflows/server-lint.yml index d25f2adcc860f2b72e8759c4f8eb914ab8e53aab..30038b3b9fe9f13db5b0dd507c2ff06fcfad6ec2 100644 --- a/.github/workflows/server-lint.yml +++ b/.github/workflows/server-lint.yml @@ -3,7 +3,7 @@ name: "Lint (server)" on: # Run on every push to a branch other than main that changes server/ push: - branches-ignore: [main, "deploy/**"] + branches-ignore: [main, "deploy/**", "deploy-f/**"] paths: - "server/**" - ".github/workflows/server-lint.yml" diff --git a/.github/workflows/server-publish.yml b/.github/workflows/server-publish.yml index 1ba1935171d5f3c5dba700c4e6669b34227e8507..b5aabbb8a2dfc4037b373df1cdfecbb1574f184c 100644 --- a/.github/workflows/server-publish.yml +++ b/.github/workflows/server-publish.yml @@ -38,3 +38,8 @@ jobs: tags: ${{ inputs.commit }}, latest username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} + + - name: Tag as server/ghcr + run: | + git tag -f server/ghcr + git push -f origin server/ghcr diff --git a/.github/workflows/web-crowdin-push.yml b/.github/workflows/web-crowdin-push.yml new file mode 100644 index 0000000000000000000000000000000000000000..1d525dfe0e3e0355bd68a72f2716c105f11f84f0 --- /dev/null +++ b/.github/workflows/web-crowdin-push.yml @@ -0,0 +1,34 @@ +name: "Push Crowdin translations (web)" + +# This is a variant of web-crowdin.yml that uploads the translated strings in +# addition to the source strings. +# +# This allows us to change the strings in our source code for an automated +# refactoring (e.g. renaming a key), and then run this workflow to update the +# data in Crowdin taking our source code as the source of truth. + +on: + # Trigger manually, or using + # `gh workflow run web-crowdin-push.yml --ref ` + workflow_dispatch: + +jobs: + push-to-crowdin: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Crowdin push + uses: crowdin/github-action@v1 + with: + base_path: "web/" + config: "web/crowdin.yml" + upload_sources: true + upload_translations: true + download_translations: false + project_id: 569613 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} diff --git a/.github/workflows/web-crowdin.yml b/.github/workflows/web-crowdin.yml index d986850653095207a45eccdd906dbfddee0db433..b20b19ce3f92f3e124ec12fa3295e6ee1c13f8a1 100644 --- a/.github/workflows/web-crowdin.yml +++ b/.github/workflows/web-crowdin.yml @@ -36,7 +36,7 @@ jobs: upload_sources: true upload_translations: false download_translations: true - localization_branch_name: crowdin-translations-web + localization_branch_name: translations/web create_pull_request: true skip_untranslated_strings: true pull_request_title: "[web] New translations" diff --git a/.github/workflows/web-deploy-accounts.yml b/.github/workflows/web-deploy-accounts.yml index 61411cac6f6abf1f09399dcf4a1cf25de8bf9538..33da5ee6f10d2a57f14c87ae83858937d7d91fec 100644 --- a/.github/workflows/web-deploy-accounts.yml +++ b/.github/workflows/web-deploy-accounts.yml @@ -3,7 +3,7 @@ name: "Deploy (accounts)" on: push: # Run workflow on pushes to the deploy/accounts - branches: [deploy/accounts] + branches: [deploy/accounts, deploy-f/accounts] jobs: deploy: diff --git a/.github/workflows/web-deploy-cast.yml b/.github/workflows/web-deploy-cast.yml index c5bbca9542c91ff50bd6460c90f86391b03f2816..01e17486d0875ecc1fc3bea58c500f96a3bf8911 100644 --- a/.github/workflows/web-deploy-cast.yml +++ b/.github/workflows/web-deploy-cast.yml @@ -3,7 +3,7 @@ name: "Deploy (cast)" on: push: # Run workflow on pushes to the deploy/cast - branches: [deploy/cast] + branches: [deploy/cast, deploy-f/cast] jobs: deploy: diff --git a/.github/workflows/web-lint.yml b/.github/workflows/web-lint.yml index 0dc11aa0e7785b09a73e05d69e1f3938cd629919..baf2a98ab0fc53024e44324bcdadce356ae5d04f 100644 --- a/.github/workflows/web-lint.yml +++ b/.github/workflows/web-lint.yml @@ -3,7 +3,7 @@ name: "Lint (web)" on: # Run on every push to a branch other than main that changes web/ push: - branches-ignore: [main, "deploy/**"] + branches-ignore: [main, "deploy/**", "deploy-f/**"] paths: - "web/**" - ".github/workflows/web-lint.yml" diff --git a/auth/assets/simple-icons b/auth/assets/simple-icons index 8e7701d6a40462733043f54b3849faf35af70a83..8a3731352af133a02223a6c7b1f37c4abb096af0 160000 --- a/auth/assets/simple-icons +++ b/auth/assets/simple-icons @@ -1 +1 @@ -Subproject commit 8e7701d6a40462733043f54b3849faf35af70a83 +Subproject commit 8a3731352af133a02223a6c7b1f37c4abb096af0 diff --git a/auth/ios/Podfile.lock b/auth/ios/Podfile.lock index 7d02d123b27e59861ba2858613976f4963f3768e..991f52b42acb259b498dfc9d518aab5eb65bb1f3 100644 --- a/auth/ios/Podfile.lock +++ b/auth/ios/Podfile.lock @@ -87,7 +87,7 @@ PODS: - SDWebImage/Core (5.19.0) - Sentry/HybridSDK (8.21.0): - SentryPrivate (= 8.21.0) - - sentry_flutter (0.0.1): + - sentry_flutter (7.19.0): - Flutter - FlutterMacOS - Sentry/HybridSDK (= 8.21.0) @@ -249,7 +249,7 @@ SPEC CHECKSUMS: ReachabilitySwift: 5ae15e16814b5f9ef568963fb2c87aeb49158c66 SDWebImage: 981fd7e860af070920f249fd092420006014c3eb Sentry: ebc12276bd17613a114ab359074096b6b3725203 - sentry_flutter: dff1df05dc39c83d04f9330b36360fc374574c5e + sentry_flutter: 88ebea3f595b0bc16acc5bedacafe6d60c12dcd5 SentryPrivate: d651efb234cf385ec9a1cdd3eff94b5e78a0e0fe share_plus: c3fef564749587fc939ef86ffb283ceac0baf9f5 shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695 @@ -263,4 +263,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: b4e3a7eabb03395b66e81fc061789f61526ee6bb -COCOAPODS: 1.14.3 +COCOAPODS: 1.15.2 diff --git a/auth/lib/l10n/arb/app_ar.arb b/auth/lib/l10n/arb/app_ar.arb index 68bd38900eadbb17a8323076628a819ef198be75..f9d37c7ba91aa08514480305341ccf63c21300e1 100644 --- a/auth/lib/l10n/arb/app_ar.arb +++ b/auth/lib/l10n/arb/app_ar.arb @@ -20,7 +20,6 @@ "codeIssuerHint": "المصدِّر", "codeSecretKeyHint": "الرمز السري", "codeAccountHint": "الحساب (you@domain.com)", - "accountKeyType": "نوع المفتاح", "sessionExpired": "انتهت صلاحية الجلسة", "@sessionExpired": { "description": "Title of the dialog when the users current session is invalid/expired" diff --git a/auth/lib/l10n/arb/app_de.arb b/auth/lib/l10n/arb/app_de.arb index be769ecd5016142d3ef3d6c5828fdd41979606df..0c4d29eaf32eeaf37fce02e211d92ce0937cf9e3 100644 --- a/auth/lib/l10n/arb/app_de.arb +++ b/auth/lib/l10n/arb/app_de.arb @@ -20,7 +20,6 @@ "codeIssuerHint": "Aussteller", "codeSecretKeyHint": "Geheimer Schlüssel", "codeAccountHint": "Konto (you@domain.com)", - "accountKeyType": "Art des Schlüssels", "sessionExpired": "Sitzung abgelaufen", "@sessionExpired": { "description": "Title of the dialog when the users current session is invalid/expired" diff --git a/auth/lib/l10n/arb/app_en.arb b/auth/lib/l10n/arb/app_en.arb index e16a39c799f69d72ba564a1e33b7076f8fff35fa..c22bac930dbb0270a7b55408ce4270829a4644d7 100644 --- a/auth/lib/l10n/arb/app_en.arb +++ b/auth/lib/l10n/arb/app_en.arb @@ -20,7 +20,6 @@ "codeIssuerHint": "Issuer", "codeSecretKeyHint": "Secret Key", "codeAccountHint": "Account (you@domain.com)", - "accountKeyType": "Type of key", "sessionExpired": "Session expired", "@sessionExpired": { "description": "Title of the dialog when the users current session is invalid/expired" diff --git a/auth/lib/l10n/arb/app_es.arb b/auth/lib/l10n/arb/app_es.arb index 41113f0b9acf3a521f9f518068a72ee40fdfe308..f0c8971a0f7bbd6efe20a3a26d294904ea17bbb0 100644 --- a/auth/lib/l10n/arb/app_es.arb +++ b/auth/lib/l10n/arb/app_es.arb @@ -20,7 +20,6 @@ "codeIssuerHint": "Emisor", "codeSecretKeyHint": "Llave Secreta", "codeAccountHint": "Cuenta (tu@dominio.com)", - "accountKeyType": "Tipo de llave", "sessionExpired": "La sesión ha expirado", "@sessionExpired": { "description": "Title of the dialog when the users current session is invalid/expired" @@ -113,6 +112,7 @@ "copied": "Copiado", "pleaseTryAgain": "Por favor, inténtalo nuevamente", "existingUser": "Usuario existente", + "newUser": "Nuevo a Ente", "delete": "Borrar", "enterYourPasswordHint": "Ingrese su contraseña", "forgotPassword": "Olvidé mi contraseña", @@ -138,6 +138,8 @@ "enterCodeHint": "Ingrese el código de seis dígitos de su aplicación de autenticación", "lostDeviceTitle": "¿Perdió su dispositivo?", "twoFactorAuthTitle": "Autenticación de dos factores", + "passkeyAuthTitle": "Verificación de llave de acceso", + "verifyPasskey": "Verificar llave de acceso", "recoverAccount": "Recuperar cuenta", "enterRecoveryKeyHint": "Introduzca su clave de recuperación", "recover": "Recuperar", @@ -191,6 +193,8 @@ "recoveryKeySaveDescription": "Nosotros no almacenamos esta clave, por favor guarde dicha clave de 24 palabras en un lugar seguro.", "doThisLater": "Hacer esto más tarde", "saveKey": "Guardar Clave", + "save": "Guardar", + "send": "Enviar", "back": "Atrás", "createAccount": "Crear cuenta", "passwordStrength": "Fortaleza de la contraseña: {passwordStrengthValue}", @@ -397,5 +401,8 @@ "signOutOtherDevices": "Cerrar la sesión de otros dispositivos", "doNotSignOut": "No cerrar la sesión", "hearUsWhereTitle": "¿Cómo conoció Ente? (opcional)", - "hearUsExplanation": "No rastreamos las aplicaciones instaladas. ¡Nos ayudaría si nos dijera dónde nos encontró!" + "hearUsExplanation": "No rastreamos las aplicaciones instaladas. ¡Nos ayudaría si nos dijera dónde nos encontró!", + "passkey": "Llave de acceso", + "developerSettingsWarning": "¿Estás seguro de que quieres modificar los ajustes de desarrollador?", + "developerSettings": "Ajustes de desarrollador" } \ No newline at end of file diff --git a/auth/lib/l10n/arb/app_fa.arb b/auth/lib/l10n/arb/app_fa.arb index 0cba193a96643ce853f6be15fe21d35da5d89a16..948aa8b223c5ad9eb82374a321d48307b4039fa3 100644 --- a/auth/lib/l10n/arb/app_fa.arb +++ b/auth/lib/l10n/arb/app_fa.arb @@ -14,7 +14,6 @@ "codeIssuerHint": "صادر کننده", "codeSecretKeyHint": "کلید مخفی", "codeAccountHint": "حساب (you@domain.com)", - "accountKeyType": "نوع کلید", "sessionExpired": "نشست منقضی شده است", "@sessionExpired": { "description": "Title of the dialog when the users current session is invalid/expired" diff --git a/auth/lib/l10n/arb/app_fi.arb b/auth/lib/l10n/arb/app_fi.arb index 72309b3310ecc26737cd1539b35ba341a485b160..2a0404147587926e87fc7843b7a5343dd92b1881 100644 --- a/auth/lib/l10n/arb/app_fi.arb +++ b/auth/lib/l10n/arb/app_fi.arb @@ -12,7 +12,6 @@ "codeIssuerHint": "Myöntäjä", "codeSecretKeyHint": "Salainen avain", "codeAccountHint": "Tili (sinun@jokinosoite.com)", - "accountKeyType": "Avaimen tyyppi", "sessionExpired": "Istunto on vanheutunut", "@sessionExpired": { "description": "Title of the dialog when the users current session is invalid/expired" diff --git a/auth/lib/l10n/arb/app_fr.arb b/auth/lib/l10n/arb/app_fr.arb index 04a7058c7c877025a920a219bda71765b4578a56..71ddc0b31c815e53a7d6b0ba28a36bab788acf1e 100644 --- a/auth/lib/l10n/arb/app_fr.arb +++ b/auth/lib/l10n/arb/app_fr.arb @@ -20,7 +20,6 @@ "codeIssuerHint": "Émetteur", "codeSecretKeyHint": "Clé secrète", "codeAccountHint": "Compte (vous@exemple.com)", - "accountKeyType": "Type de clé", "sessionExpired": "Session expirée", "@sessionExpired": { "description": "Title of the dialog when the users current session is invalid/expired" diff --git a/auth/lib/l10n/arb/app_he.arb b/auth/lib/l10n/arb/app_he.arb index 33058509796fdde5dee7d093e8371e30eae75884..8f22e1e82c9eb550af1f6e35cb84c2394d850ed6 100644 --- a/auth/lib/l10n/arb/app_he.arb +++ b/auth/lib/l10n/arb/app_he.arb @@ -19,7 +19,6 @@ "codeIssuerHint": "מנפיק", "codeSecretKeyHint": "מפתח סודי", "codeAccountHint": "חשבון(you@domain.com)", - "accountKeyType": "סוג מפתח", "sessionExpired": "זמן החיבור הסתיים", "@sessionExpired": { "description": "Title of the dialog when the users current session is invalid/expired" diff --git a/auth/lib/l10n/arb/app_it.arb b/auth/lib/l10n/arb/app_it.arb index e35fd11dc02223ca6b963318fe149c215bfb111b..92543ed82161aad89a04437f3d9e330175ed2099 100644 --- a/auth/lib/l10n/arb/app_it.arb +++ b/auth/lib/l10n/arb/app_it.arb @@ -20,7 +20,6 @@ "codeIssuerHint": "Emittente", "codeSecretKeyHint": "Codice segreto", "codeAccountHint": "Account (username@dominio.it)", - "accountKeyType": "Tipo di chiave", "sessionExpired": "Sessione scaduta", "@sessionExpired": { "description": "Title of the dialog when the users current session is invalid/expired" diff --git a/auth/lib/l10n/arb/app_ja.arb b/auth/lib/l10n/arb/app_ja.arb index 60d0a51507304cd163374a4c639f0083b9defc17..8fea34c5e13a2917bd5e0ce6509e66d5a050e483 100644 --- a/auth/lib/l10n/arb/app_ja.arb +++ b/auth/lib/l10n/arb/app_ja.arb @@ -20,7 +20,6 @@ "codeIssuerHint": "発行者", "codeSecretKeyHint": "秘密鍵", "codeAccountHint": "アカウント (you@domain.com)", - "accountKeyType": "鍵の種類", "sessionExpired": "セッションが失効しました", "@sessionExpired": { "description": "Title of the dialog when the users current session is invalid/expired" diff --git a/auth/lib/l10n/arb/app_ka.arb b/auth/lib/l10n/arb/app_ka.arb index cb7dc8281856f4eac42fbc20c673789e75058fcf..93631df2d591563404251d794c0d7e21738cac15 100644 --- a/auth/lib/l10n/arb/app_ka.arb +++ b/auth/lib/l10n/arb/app_ka.arb @@ -20,7 +20,6 @@ "codeIssuerHint": "მომწოდებელი", "codeSecretKeyHint": "გასაღები", "codeAccountHint": "ანგარიში (you@domain.com)", - "accountKeyType": "გასაღების ტიპი", "sessionExpired": "სესიის დრო ამოიწურა", "@sessionExpired": { "description": "Title of the dialog when the users current session is invalid/expired" diff --git a/auth/lib/l10n/arb/app_nl.arb b/auth/lib/l10n/arb/app_nl.arb index 2e84ae11bb0692b6468f738f7106d8419cd702ef..36280f69dc7b16eb20fa6ebd5b5f019a778cc601 100644 --- a/auth/lib/l10n/arb/app_nl.arb +++ b/auth/lib/l10n/arb/app_nl.arb @@ -20,7 +20,6 @@ "codeIssuerHint": "Uitgever", "codeSecretKeyHint": "Geheime sleutel", "codeAccountHint": "Account (jij@domein.nl)", - "accountKeyType": "Type sleutel", "sessionExpired": "Sessie verlopen", "@sessionExpired": { "description": "Title of the dialog when the users current session is invalid/expired" diff --git a/auth/lib/l10n/arb/app_pl.arb b/auth/lib/l10n/arb/app_pl.arb index 8ebc935dc8c841a035ebc7a4ec629041e1ca5e5a..3132f666083cac7a1bd41df10053bb4b60c09ff7 100644 --- a/auth/lib/l10n/arb/app_pl.arb +++ b/auth/lib/l10n/arb/app_pl.arb @@ -20,7 +20,6 @@ "codeIssuerHint": "Wydawca", "codeSecretKeyHint": "Tajny klucz", "codeAccountHint": "Konto (ty@domena.com)", - "accountKeyType": "Rodzaj klucza", "sessionExpired": "Sesja wygasła", "@sessionExpired": { "description": "Title of the dialog when the users current session is invalid/expired" diff --git a/auth/lib/l10n/arb/app_pt.arb b/auth/lib/l10n/arb/app_pt.arb index b27a018fba057282826c79597d2b98b61c357174..9b1f5b1b0af4cc1d210149b009f21146fe73c1e1 100644 --- a/auth/lib/l10n/arb/app_pt.arb +++ b/auth/lib/l10n/arb/app_pt.arb @@ -20,7 +20,6 @@ "codeIssuerHint": "Emissor", "codeSecretKeyHint": "Chave secreta", "codeAccountHint": "Conta (voce@dominio.com)", - "accountKeyType": "Tipo de chave", "sessionExpired": "Sessão expirada", "@sessionExpired": { "description": "Title of the dialog when the users current session is invalid/expired" diff --git a/auth/lib/l10n/arb/app_ru.arb b/auth/lib/l10n/arb/app_ru.arb index 7ae37a87b91501078a7007eec30cc0bd1f69e6a2..ca98611ee1542576c829b401749e7cf3d1ad6317 100644 --- a/auth/lib/l10n/arb/app_ru.arb +++ b/auth/lib/l10n/arb/app_ru.arb @@ -20,7 +20,6 @@ "codeIssuerHint": "Эмитент", "codeSecretKeyHint": "Секретный ключ", "codeAccountHint": "Аккаунт (you@domain.com)", - "accountKeyType": "Тип ключа", "sessionExpired": "Сеанс истек", "@sessionExpired": { "description": "Title of the dialog when the users current session is invalid/expired" diff --git a/auth/lib/l10n/arb/app_sv.arb b/auth/lib/l10n/arb/app_sv.arb index cfb41d7bdc573579036c4106976fdf78778cb48d..9761325ce108c096a14bf10bb6a1253aaee8379c 100644 --- a/auth/lib/l10n/arb/app_sv.arb +++ b/auth/lib/l10n/arb/app_sv.arb @@ -16,7 +16,6 @@ "codeIssuerHint": "Utfärdare", "codeSecretKeyHint": "Secret Key", "codeAccountHint": "Konto (du@domän.com)", - "accountKeyType": "Typ av nyckel", "sessionExpired": "Sessionen har gått ut", "@sessionExpired": { "description": "Title of the dialog when the users current session is invalid/expired" diff --git a/auth/lib/l10n/arb/app_ti.arb b/auth/lib/l10n/arb/app_ti.arb index 27147ebb6e02fae0010103b8f6877c3922f4539f..b41128f6eaf59f2404acd9c750de35f5be05bc10 100644 --- a/auth/lib/l10n/arb/app_ti.arb +++ b/auth/lib/l10n/arb/app_ti.arb @@ -20,7 +20,6 @@ "codeIssuerHint": "ኣዋጂ", "codeSecretKeyHint": "ምስጢራዊ መፍትሕ", "codeAccountHint": "ሕሳብ (you@domain.com)", - "accountKeyType": "ዓይነት መፍትሕ", "sessionExpired": "ክፍለ ግዜኡ ኣኺሉ።", "@sessionExpired": { "description": "Title of the dialog when the users current session is invalid/expired" diff --git a/auth/lib/l10n/arb/app_tr.arb b/auth/lib/l10n/arb/app_tr.arb index 9b847faf0fb453a0a23e509d842983ed6d915396..322af5f48c444c4c0ef2094f3bd68082b6ecf4c0 100644 --- a/auth/lib/l10n/arb/app_tr.arb +++ b/auth/lib/l10n/arb/app_tr.arb @@ -20,7 +20,6 @@ "codeIssuerHint": "Yayınlayan", "codeSecretKeyHint": "Gizli Anahtar", "codeAccountHint": "Hesap (ornek@domain.com)", - "accountKeyType": "Anahtar türü", "sessionExpired": "Oturum süresi doldu", "@sessionExpired": { "description": "Title of the dialog when the users current session is invalid/expired" diff --git a/auth/lib/l10n/arb/app_vi.arb b/auth/lib/l10n/arb/app_vi.arb index e318f9b557e38f7f703401a1891a710a4c7e1248..a8cccdbec555e05d20b04ade136214e2eaea2f82 100644 --- a/auth/lib/l10n/arb/app_vi.arb +++ b/auth/lib/l10n/arb/app_vi.arb @@ -20,7 +20,6 @@ "codeIssuerHint": "Nhà phát hành", "codeSecretKeyHint": "Khóa bí mật", "codeAccountHint": "Tài khoản (bạn@miền.com)", - "accountKeyType": "Loại khóa", "sessionExpired": "Phiên làm việc đã hết hạn", "@sessionExpired": { "description": "Title of the dialog when the users current session is invalid/expired" diff --git a/auth/lib/l10n/arb/app_zh.arb b/auth/lib/l10n/arb/app_zh.arb index 077ee26fdf66c234a3e7cd3ac45a99efc129b211..c50e76c1ddd8eb86b85e73e29d1cf014fb71d974 100644 --- a/auth/lib/l10n/arb/app_zh.arb +++ b/auth/lib/l10n/arb/app_zh.arb @@ -20,7 +20,6 @@ "codeIssuerHint": "发行人", "codeSecretKeyHint": "私钥", "codeAccountHint": "账户 (you@domain.com)", - "accountKeyType": "密钥类型", "sessionExpired": "会话已过期", "@sessionExpired": { "description": "Title of the dialog when the users current session is invalid/expired" diff --git a/auth/lib/main.dart b/auth/lib/main.dart index 09b85d8b35543e33d75dca454da517cad4eff951..d8d22ca4fe7124931a5ade764a31353c28f9147d 100644 --- a/auth/lib/main.dart +++ b/auth/lib/main.dart @@ -37,6 +37,7 @@ import 'package:window_manager/window_manager.dart'; final _logger = Logger("main"); Future initSystemTray() async { + if (PlatformUtil.isMobile()) return; String path = Platform.isWindows ? 'assets/icons/auth-icon.ico' : 'assets/icons/auth-icon.png'; diff --git a/auth/lib/models/code.dart b/auth/lib/models/code.dart index 7853eb19d0a908c1af349944d8762d2f486e7e5c..bd6077326c91159f8b410d918d01c4c5edff2182 100644 --- a/auth/lib/models/code.dart +++ b/auth/lib/models/code.dart @@ -2,6 +2,7 @@ import 'package:ente_auth/utils/totp_util.dart'; class Code { static const defaultDigits = 6; + static const steamDigits = 5; static const defaultPeriod = 30; int? generatedID; @@ -57,36 +58,42 @@ class Code { updatedAlgo, updatedType, updatedCounter, - "otpauth://${updatedType.name}/$updateIssuer:$updateAccount?algorithm=${updatedAlgo.name}&digits=$updatedDigits&issuer=$updateIssuer&period=$updatePeriod&secret=$updatedSecret${updatedType == Type.hotp ? "&counter=$updatedCounter" : ""}", + "otpauth://${updatedType.name}/$updateIssuer:$updateAccount?algorithm=${updatedAlgo.name}" + "&digits=$updatedDigits&issuer=$updateIssuer" + "&period=$updatePeriod&secret=$updatedSecret${updatedType == Type.hotp ? "&counter=$updatedCounter" : ""}", generatedID: generatedID, ); } static Code fromAccountAndSecret( + Type type, String account, String issuer, String secret, + int digits, ) { return Code( account, issuer, - defaultDigits, + digits, defaultPeriod, secret, Algorithm.sha1, - Type.totp, + type, 0, - "otpauth://totp/$issuer:$account?algorithm=SHA1&digits=6&issuer=$issuer&period=30&secret=$secret", + "otpauth://${type.name}/$issuer:$account?algorithm=SHA1&digits=$digits&issuer=$issuer&period=30&secret=$secret", ); } static Code fromRawData(String rawData) { Uri uri = Uri.parse(rawData); + final issuer = _getIssuer(uri); + try { return Code( _getAccount(uri), - _getIssuer(uri), - _getDigits(uri), + issuer, + _getDigits(uri, issuer), _getPeriod(uri), getSanitizedSecret(uri.queryParameters['secret']!), _getAlgorithm(uri), @@ -140,10 +147,13 @@ class Code { } } - static int _getDigits(Uri uri) { + static int _getDigits(Uri uri, String issuer) { try { return int.parse(uri.queryParameters['digits']!); } catch (e) { + if (issuer.toLowerCase() == "steam") { + return steamDigits; + } return defaultDigits; } } @@ -186,6 +196,8 @@ class Code { static Type _getType(Uri uri) { if (uri.host == "totp") { return Type.totp; + } else if (uri.host == "steam") { + return Type.steam; } else if (uri.host == "hotp") { return Type.hotp; } @@ -223,6 +235,9 @@ class Code { enum Type { totp, hotp, + steam; + + bool get isTOTPCompatible => this == totp || this == steam; } enum Algorithm { diff --git a/auth/lib/onboarding/view/setup_enter_secret_key_page.dart b/auth/lib/onboarding/view/setup_enter_secret_key_page.dart index 3937142d6cb039e9f6714d743a287280c93640aa..57edcc2e1a893010caeb0001f50421ce713e6f9e 100644 --- a/auth/lib/onboarding/view/setup_enter_secret_key_page.dart +++ b/auth/lib/onboarding/view/setup_enter_secret_key_page.dart @@ -61,6 +61,8 @@ class _SetupEnterSecretKeyPageState extends State { }, decoration: InputDecoration( hintText: l10n.codeIssuerHint, + floatingLabelBehavior: FloatingLabelBehavior.auto, + labelText: l10n.codeIssuerHint, ), controller: _issuerController, autofocus: true, @@ -78,6 +80,8 @@ class _SetupEnterSecretKeyPageState extends State { }, decoration: InputDecoration( hintText: l10n.codeSecretKeyHint, + floatingLabelBehavior: FloatingLabelBehavior.auto, + labelText: l10n.codeSecretKeyHint, suffixIcon: IconButton( onPressed: () { setState(() { @@ -105,12 +109,12 @@ class _SetupEnterSecretKeyPageState extends State { }, decoration: InputDecoration( hintText: l10n.codeAccountHint, + floatingLabelBehavior: FloatingLabelBehavior.auto, + labelText: l10n.codeAccountHint, ), controller: _accountController, ), - const SizedBox( - height: 40, - ), + const SizedBox(height: 40), SizedBox( width: 400, child: OutlinedButton( @@ -152,6 +156,7 @@ class _SetupEnterSecretKeyPageState extends State { final account = _accountController.text.trim(); final issuer = _issuerController.text.trim(); final secret = _secretController.text.trim().replaceAll(' ', ''); + final isStreamCode = issuer.toLowerCase() == "steam"; if (widget.code != null && widget.code!.secret != secret) { ButtonResult? result = await showChoiceActionSheet( context, @@ -168,9 +173,11 @@ class _SetupEnterSecretKeyPageState extends State { } final Code newCode = widget.code == null ? Code.fromAccountAndSecret( + isStreamCode ? Type.steam : Type.totp, account, issuer, secret, + isStreamCode ? Code.steamDigits : Code.defaultDigits, ) : widget.code!.copyWith( account: account, diff --git a/auth/lib/ui/code_widget.dart b/auth/lib/ui/code_widget.dart index f97e865ec797125ac7eb7aa17fece9fcdd50a1af..d989edf18fdd25f4e80f0ddabc33d83388bbf7fe 100644 --- a/auth/lib/ui/code_widget.dart +++ b/auth/lib/ui/code_widget.dart @@ -53,7 +53,7 @@ class _CodeWidgetState extends State { String newCode = _getCurrentOTP(); if (newCode != _currentCode.value) { _currentCode.value = newCode; - if (widget.code.type == Type.totp) { + if (widget.code.type.isTOTPCompatible) { _nextCode.value = _getNextTotp(); } } @@ -78,7 +78,7 @@ class _CodeWidgetState extends State { _shouldShowLargeIcon = PreferenceService.instance.shouldShowLargeIcons(); if (!_isInitialized) { _currentCode.value = _getCurrentOTP(); - if (widget.code.type == Type.totp) { + if (widget.code.type.isTOTPCompatible) { _nextCode.value = _getNextTotp(); } _isInitialized = true; @@ -213,7 +213,7 @@ class _CodeWidgetState extends State { crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.center, children: [ - if (widget.code.type == Type.totp) + if (widget.code.type.isTOTPCompatible) CodeTimerProgress( period: widget.code.period, ), @@ -263,7 +263,7 @@ class _CodeWidgetState extends State { }, ), ), - widget.code.type == Type.totp + widget.code.type.isTOTPCompatible ? GestureDetector( onTap: () { _copyNextToClipboard(); @@ -481,7 +481,7 @@ class _CodeWidgetState extends State { String _getNextTotp() { try { - assert(widget.code.type == Type.totp); + assert(widget.code.type.isTOTPCompatible); return getNextTotp(widget.code); } catch (e) { return context.l10n.error; diff --git a/auth/lib/ui/settings/data/import/bitwarden_import.dart b/auth/lib/ui/settings/data/import/bitwarden_import.dart index 90e527dde0c1b66e18e823ca97bff7d67d143ae4..7a562d82b5470ffe7938c67923f4e8a9f99ee973 100644 --- a/auth/lib/ui/settings/data/import/bitwarden_import.dart +++ b/auth/lib/ui/settings/data/import/bitwarden_import.dart @@ -92,9 +92,11 @@ Future _processBitwardenExportFile( var account = item['login']['username']; code = Code.fromAccountAndSecret( + Type.totp, account, issuer, totp, + Code.defaultDigits, ); } diff --git a/auth/lib/utils/totp_util.dart b/auth/lib/utils/totp_util.dart index a49448524954d165e17df8168025ea07d79643bc..0d6a8bd68f6190a2da1ec481ef95455f1ccba501 100644 --- a/auth/lib/utils/totp_util.dart +++ b/auth/lib/utils/totp_util.dart @@ -3,7 +3,7 @@ import 'package:flutter/foundation.dart'; import 'package:otp/otp.dart' as otp; String getOTP(Code code) { - if(code.type == Type.hotp) { + if (code.type == Type.hotp) { return _getHOTPCode(code); } return otp.OTP.generateTOTPCodeString( @@ -60,4 +60,4 @@ String safeDecode(String value) { debugPrint("Failed to decode $e"); return value; } -} \ No newline at end of file +} diff --git a/auth/linux/packaging/rpm/make_config.yaml b/auth/linux/packaging/rpm/make_config.yaml index 5d5f3aab53cc53760e9eb696a8febe50664dd92a..e82dd63bfbc8718758fbc45d8eb3224cc9bd7d9e 100644 --- a/auth/linux/packaging/rpm/make_config.yaml +++ b/auth/linux/packaging/rpm/make_config.yaml @@ -11,7 +11,7 @@ display_name: Auth requires: - libsqlite3x - - webkit2gtk-4.0 + - webkit2gtk4.0 - libsodium - libsecret - libappindicator diff --git a/auth/pubspec.lock b/auth/pubspec.lock index 2d61b77c3903ec95053a77135e09d6e1259ffc83..77241604205d586c08a13b4cf8b1dfd9cce4d4ea 100644 --- a/auth/pubspec.lock +++ b/auth/pubspec.lock @@ -293,9 +293,9 @@ packages: dependency: "direct main" description: path: "packages/desktop_webview_window" - ref: HEAD - resolved-ref: "8cbbf9cd6efcfee5e0f420a36f7f8e7e64b667a1" - url: "https://github.com/MixinNetwork/flutter-plugins" + ref: fix-webkit-version + resolved-ref: fe2223e4edfecdbb3a97bb9e3ced73db4ae9d979 + url: "https://github.com/ente-io/flutter-desktopwebview-fork" source: git version: "0.2.4" device_info_plus: diff --git a/auth/pubspec.yaml b/auth/pubspec.yaml index 2ef543aa69c6856d8ef217fd7ed26e03d66cd859..b7a35b699623055566f4667f131cbbf80e1aaa92 100644 --- a/auth/pubspec.yaml +++ b/auth/pubspec.yaml @@ -1,6 +1,6 @@ name: ente_auth description: ente two-factor authenticator -version: 2.0.55+255 +version: 2.0.57+257 publish_to: none environment: @@ -20,7 +20,8 @@ dependencies: convert: ^3.1.1 desktop_webview_window: git: - url: https://github.com/MixinNetwork/flutter-plugins + url: https://github.com/ente-io/flutter-desktopwebview-fork + ref: fix-webkit-version path: packages/desktop_webview_window device_info_plus: ^9.1.1 dio: ^5.4.0 diff --git a/cli/README.md b/cli/README.md index 8fc9aa694808237c3295ed9d4810fddc030bcab7..40858da0f846481f2a74f6efd7824d93a44f98ed 100644 --- a/cli/README.md +++ b/cli/README.md @@ -36,7 +36,8 @@ ente --help ### Accounts -If you wish, you can add multiple accounts (your own and that of your family members) and export all data using this tool. +If you wish, you can add multiple accounts (your own and that of your family +members) and export all data using this tool. #### Add an account @@ -44,6 +45,12 @@ If you wish, you can add multiple accounts (your own and that of your family mem ente account add ``` +> [!NOTE] +> +> `ente account add` does not create new accounts, it just adds pre-existing +> accounts to the list of accounts that the CLI knows about so that you can use +> them for other actions. + #### List accounts ```shell diff --git a/cli/cmd/account.go b/cli/cmd/account.go index a4c78fb10e31a10ea4388b22467cf0320fd36826..4bc48dcf304a472bba2c28fbd358b3b133047a65 100644 --- a/cli/cmd/account.go +++ b/cli/cmd/account.go @@ -27,7 +27,8 @@ var listAccCmd = &cobra.Command{ // Subcommand for 'account add' var addAccCmd = &cobra.Command{ Use: "add", - Short: "Add a new account", + Short: "login into existing account", + Long: "Use this command to add an existing account to cli. For creating a new account, use the mobile,web or desktop app", Run: func(cmd *cobra.Command, args []string) { recoverWithLog() ctrl.AddAccount(context.Background()) diff --git a/cli/docs/generated/ente.md b/cli/docs/generated/ente.md index b9d3cde1762c85758ce19913ff1260c98303b572..4f85dd0980b94f13f4ab3f5f75dfceab9d9c18c7 100644 --- a/cli/docs/generated/ente.md +++ b/cli/docs/generated/ente.md @@ -25,4 +25,4 @@ ente [flags] * [ente export](ente_export.md) - Starts the export process * [ente version](ente_version.md) - Prints the current version -###### Auto generated by spf13/cobra on 14-Mar-2024 +###### Auto generated by spf13/cobra on 6-May-2024 diff --git a/cli/docs/generated/ente_account.md b/cli/docs/generated/ente_account.md index c48a6533655c6ecc4fa7289a817668ce5580abfc..41c37b0547295f492695bf62c596729cd42af235 100644 --- a/cli/docs/generated/ente_account.md +++ b/cli/docs/generated/ente_account.md @@ -11,9 +11,9 @@ Manage account settings ### SEE ALSO * [ente](ente.md) - CLI tool for exporting your photos from ente.io -* [ente account add](ente_account_add.md) - Add a new account +* [ente account add](ente_account_add.md) - login into existing account * [ente account get-token](ente_account_get-token.md) - Get token for an account for a specific app * [ente account list](ente_account_list.md) - list configured accounts * [ente account update](ente_account_update.md) - Update an existing account's export directory -###### Auto generated by spf13/cobra on 14-Mar-2024 +###### Auto generated by spf13/cobra on 6-May-2024 diff --git a/cli/docs/generated/ente_account_add.md b/cli/docs/generated/ente_account_add.md index 1904ca370273729452ac1b4fc82cad4e4f97dd25..1e86ae12f7458a5a36ac6162614faba68e7c113c 100644 --- a/cli/docs/generated/ente_account_add.md +++ b/cli/docs/generated/ente_account_add.md @@ -1,6 +1,10 @@ ## ente account add -Add a new account +login into existing account + +### Synopsis + +Use this command to add an existing account to cli. For creating a new account, use the mobile,web or desktop app ``` ente account add [flags] @@ -16,4 +20,4 @@ ente account add [flags] * [ente account](ente_account.md) - Manage account settings -###### Auto generated by spf13/cobra on 14-Mar-2024 +###### Auto generated by spf13/cobra on 6-May-2024 diff --git a/cli/docs/generated/ente_account_get-token.md b/cli/docs/generated/ente_account_get-token.md index d7ee77255c7e50d6bd69a248590e5454ff22b3a8..3d8814d7d11135e9469e3a5c421ccf73de4a8aae 100644 --- a/cli/docs/generated/ente_account_get-token.md +++ b/cli/docs/generated/ente_account_get-token.md @@ -18,4 +18,4 @@ ente account get-token [flags] * [ente account](ente_account.md) - Manage account settings -###### Auto generated by spf13/cobra on 14-Mar-2024 +###### Auto generated by spf13/cobra on 6-May-2024 diff --git a/cli/docs/generated/ente_account_list.md b/cli/docs/generated/ente_account_list.md index cfc59bb8d21609b6924c973b464d807b748cd948..a7677eb85552b3edaae447cd858437e18e9f787c 100644 --- a/cli/docs/generated/ente_account_list.md +++ b/cli/docs/generated/ente_account_list.md @@ -16,4 +16,4 @@ ente account list [flags] * [ente account](ente_account.md) - Manage account settings -###### Auto generated by spf13/cobra on 14-Mar-2024 +###### Auto generated by spf13/cobra on 6-May-2024 diff --git a/cli/docs/generated/ente_account_update.md b/cli/docs/generated/ente_account_update.md index acb65412aa98c71e3ad82beab3768e86fdef3471..8d9c8d7e54316f194ac1f7d5be7f19a704514e3b 100644 --- a/cli/docs/generated/ente_account_update.md +++ b/cli/docs/generated/ente_account_update.md @@ -19,4 +19,4 @@ ente account update [flags] * [ente account](ente_account.md) - Manage account settings -###### Auto generated by spf13/cobra on 14-Mar-2024 +###### Auto generated by spf13/cobra on 6-May-2024 diff --git a/cli/docs/generated/ente_admin.md b/cli/docs/generated/ente_admin.md index aafe51b396281a4aaa4ce3928c761efd16638ecf..5ac72489d628b5ee0da44fce583276e86bb7e395 100644 --- a/cli/docs/generated/ente_admin.md +++ b/cli/docs/generated/ente_admin.md @@ -21,4 +21,4 @@ Commands for admin actions like disable or enabling 2fa, bumping up the storage * [ente admin list-users](ente_admin_list-users.md) - List all users * [ente admin update-subscription](ente_admin_update-subscription.md) - Update subscription for user -###### Auto generated by spf13/cobra on 14-Mar-2024 +###### Auto generated by spf13/cobra on 6-May-2024 diff --git a/cli/docs/generated/ente_admin_delete-user.md b/cli/docs/generated/ente_admin_delete-user.md index 56c96841ed3dfeb3a77779e83eb7010f169066db..a1d52a73d2b43fba790c85dffdd1f65af562fc85 100644 --- a/cli/docs/generated/ente_admin_delete-user.md +++ b/cli/docs/generated/ente_admin_delete-user.md @@ -18,4 +18,4 @@ ente admin delete-user [flags] * [ente admin](ente_admin.md) - Commands for admin actions -###### Auto generated by spf13/cobra on 14-Mar-2024 +###### Auto generated by spf13/cobra on 6-May-2024 diff --git a/cli/docs/generated/ente_admin_disable-2fa.md b/cli/docs/generated/ente_admin_disable-2fa.md index 333f0912e31c25ff1ab25d4fdf83d6b606a8b328..23cd33080053703c71cdc7734d7d856991d1ab01 100644 --- a/cli/docs/generated/ente_admin_disable-2fa.md +++ b/cli/docs/generated/ente_admin_disable-2fa.md @@ -18,4 +18,4 @@ ente admin disable-2fa [flags] * [ente admin](ente_admin.md) - Commands for admin actions -###### Auto generated by spf13/cobra on 14-Mar-2024 +###### Auto generated by spf13/cobra on 6-May-2024 diff --git a/cli/docs/generated/ente_admin_get-user-id.md b/cli/docs/generated/ente_admin_get-user-id.md index 3d26f624ac9e01e9fa14b232ed5ab20472ba3221..47d632abb6778b71fc5b2c781a8e37fb3534dcc9 100644 --- a/cli/docs/generated/ente_admin_get-user-id.md +++ b/cli/docs/generated/ente_admin_get-user-id.md @@ -18,4 +18,4 @@ ente admin get-user-id [flags] * [ente admin](ente_admin.md) - Commands for admin actions -###### Auto generated by spf13/cobra on 14-Mar-2024 +###### Auto generated by spf13/cobra on 6-May-2024 diff --git a/cli/docs/generated/ente_admin_list-users.md b/cli/docs/generated/ente_admin_list-users.md index 8841df57b5bb8cb5674242a59560e70e69b6cd38..635e8ec3cdd6122b8cd4d1b8e0cd327d020b7714 100644 --- a/cli/docs/generated/ente_admin_list-users.md +++ b/cli/docs/generated/ente_admin_list-users.md @@ -17,4 +17,4 @@ ente admin list-users [flags] * [ente admin](ente_admin.md) - Commands for admin actions -###### Auto generated by spf13/cobra on 14-Mar-2024 +###### Auto generated by spf13/cobra on 6-May-2024 diff --git a/cli/docs/generated/ente_admin_update-subscription.md b/cli/docs/generated/ente_admin_update-subscription.md index cc1fa96234390aad7017d5ae339e2053f84c8211..d0fadcd2ba3d75c2e7a28351f841aab9ea61c0ec 100644 --- a/cli/docs/generated/ente_admin_update-subscription.md +++ b/cli/docs/generated/ente_admin_update-subscription.md @@ -23,4 +23,4 @@ ente admin update-subscription [flags] * [ente admin](ente_admin.md) - Commands for admin actions -###### Auto generated by spf13/cobra on 14-Mar-2024 +###### Auto generated by spf13/cobra on 6-May-2024 diff --git a/cli/docs/generated/ente_auth.md b/cli/docs/generated/ente_auth.md index 5770f36f39d6bbb9caac5c377d1d4aaca1ee86fa..e0e97d84fc7535e491c4b3b5ed7c3f04e585cf57 100644 --- a/cli/docs/generated/ente_auth.md +++ b/cli/docs/generated/ente_auth.md @@ -13,4 +13,4 @@ Authenticator commands * [ente](ente.md) - CLI tool for exporting your photos from ente.io * [ente auth decrypt](ente_auth_decrypt.md) - Decrypt authenticator export -###### Auto generated by spf13/cobra on 14-Mar-2024 +###### Auto generated by spf13/cobra on 6-May-2024 diff --git a/cli/docs/generated/ente_auth_decrypt.md b/cli/docs/generated/ente_auth_decrypt.md index e573db2a332035e2577558359babd9f2eba212a0..c9db6ea545d4c1a34e186cbd2b571f54996ff4cf 100644 --- a/cli/docs/generated/ente_auth_decrypt.md +++ b/cli/docs/generated/ente_auth_decrypt.md @@ -16,4 +16,4 @@ ente auth decrypt [input] [output] [flags] * [ente auth](ente_auth.md) - Authenticator commands -###### Auto generated by spf13/cobra on 14-Mar-2024 +###### Auto generated by spf13/cobra on 6-May-2024 diff --git a/cli/docs/generated/ente_export.md b/cli/docs/generated/ente_export.md index c5783236cfbd8cd15e50b1bc65b3283217a95e16..d809e06e46419c694d24ccf0f44c34203a8a7d2c 100644 --- a/cli/docs/generated/ente_export.md +++ b/cli/docs/generated/ente_export.md @@ -16,4 +16,4 @@ ente export [flags] * [ente](ente.md) - CLI tool for exporting your photos from ente.io -###### Auto generated by spf13/cobra on 14-Mar-2024 +###### Auto generated by spf13/cobra on 6-May-2024 diff --git a/cli/docs/generated/ente_version.md b/cli/docs/generated/ente_version.md index b51055697f7ad064d872ac2b9874b8701edb048d..08f384b52f3c556f6c4d2ae72157acdda94c4b06 100644 --- a/cli/docs/generated/ente_version.md +++ b/cli/docs/generated/ente_version.md @@ -16,4 +16,4 @@ ente version [flags] * [ente](ente.md) - CLI tool for exporting your photos from ente.io -###### Auto generated by spf13/cobra on 14-Mar-2024 +###### Auto generated by spf13/cobra on 6-May-2024 diff --git a/cli/pkg/account.go b/cli/pkg/account.go index 9363e2f80ba2ce0c60a877f2f4b621f65d39558d..e411ffacd52b236b184b38c25d5db51688fdc580 100644 --- a/cli/pkg/account.go +++ b/cli/pkg/account.go @@ -59,7 +59,7 @@ func (c *ClICtrl) AddAccount(cxt context.Context) { authResponse, flowErr = c.validateTOTP(cxt, authResponse) } if authResponse.EncryptedToken == "" || authResponse.KeyAttributes == nil { - panic("no encrypted token or keyAttributes") + log.Fatalf("missing key attributes or token.\nNote: Please use the mobile,web or desktop app to create a new account.\nIf you are trying to login to an existing account, report a bug.") } secretInfo, decErr := c.decryptAccSecretInfo(cxt, authResponse, keyEncKey) if decErr != nil { diff --git a/desktop/.eslintrc.js b/desktop/.eslintrc.js index 977071a27036c9be70c6e7c1a0c59950f358df8a..44d03ef0c106b50aee9988efa3490a24a7b4717c 100644 --- a/desktop/.eslintrc.js +++ b/desktop/.eslintrc.js @@ -1,21 +1,36 @@ /* eslint-env node */ module.exports = { + root: true, extends: [ "eslint:recommended", "plugin:@typescript-eslint/eslint-recommended", - /* What we really want eventually */ - // "plugin:@typescript-eslint/strict-type-checked", - // "plugin:@typescript-eslint/stylistic-type-checked", + "plugin:@typescript-eslint/strict-type-checked", + "plugin:@typescript-eslint/stylistic-type-checked", ], plugins: ["@typescript-eslint"], parser: "@typescript-eslint/parser", parserOptions: { project: true, }, - root: true, ignorePatterns: [".eslintrc.js", "app", "out", "dist"], env: { es2022: true, node: true, }, + 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/.github/workflows/build.yml b/desktop/.github/workflows/build.yml deleted file mode 100644 index acd744c05675ab0042fbf7035b2ec21a1bd04af3..0000000000000000000000000000000000000000 --- a/desktop/.github/workflows/build.yml +++ /dev/null @@ -1,55 +0,0 @@ -name: Build/release - -on: - push: - tags: - - v* - -jobs: - release: - runs-on: ${{ matrix.os }} - - strategy: - matrix: - os: [macos-latest, ubuntu-latest, windows-latest] - - steps: - - name: Check out Git repository - uses: actions/checkout@v3 - with: - submodules: recursive - - - name: Install Node.js, NPM and Yarn - uses: actions/setup-node@v3 - with: - node-version: 20 - - - name: Prepare for app notarization - if: startsWith(matrix.os, 'macos') - # Import Apple API key for app notarization on macOS - run: | - mkdir -p ~/private_keys/ - echo '${{ secrets.api_key }}' > ~/private_keys/AuthKey_${{ secrets.api_key_id }}.p8 - - - name: Install libarchive-tools for pacman build # Related https://github.com/electron-userland/electron-builder/issues/4181 - if: startsWith(matrix.os, 'ubuntu') - run: sudo apt-get install libarchive-tools - - - name: Ente Electron Builder Action - uses: ente-io/action-electron-builder@v1.0.0 - with: - # GitHub token, automatically provided to the action - # (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"), - # release the app after building - release: ${{ startsWith(github.ref, 'refs/tags/v') }} - - mac_certs: ${{ secrets.mac_certs }} - mac_certs_password: ${{ secrets.mac_certs_password }} - env: - # macOS notarization API key - API_KEY_ID: ${{ secrets.api_key_id }} - API_KEY_ISSUER_ID: ${{ secrets.api_key_issuer_id}} - USE_HARD_LINKS: false diff --git a/desktop/.github/workflows/desktop-release.yml b/desktop/.github/workflows/desktop-release.yml new file mode 100644 index 0000000000000000000000000000000000000000..70eedf3ea6b2d9e399547a641493f445f324a795 --- /dev/null +++ b/desktop/.github/workflows/desktop-release.yml @@ -0,0 +1,80 @@ +name: "Release" + +# Build the ente-io/ente's desktop/rc branch and create/update a draft release. +# +# For more details, see `docs/release.md` in ente-io/ente. + +on: + # Trigger manually or `gh workflow run desktop-release.yml`. + workflow_dispatch: + push: + # Run when a tag matching the pattern "v*"" is pushed. + # + # See: [Note: Testing release workflows that are triggered by tags]. + tags: + - "v*" + +jobs: + release: + runs-on: ${{ matrix.os }} + + defaults: + run: + working-directory: desktop + + strategy: + matrix: + os: [macos-latest, ubuntu-latest, windows-latest] + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + # Checkout the desktop/rc branch from the source repository. + repository: ente-io/ente + ref: desktop/rc + submodules: recursive + + - name: Setup node + uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Increase yarn timeout + # `yarn install` times out sometimes on the Windows runner, + # resulting in flaky builds. + run: yarn config set network-timeout 900000 -g + + - name: Install dependencies + run: yarn install + + - name: Install libarchive-tools for pacman build + if: startsWith(matrix.os, 'ubuntu') + # See: + # https://github.com/electron-userland/electron-builder/issues/4181 + run: sudo apt-get install libarchive-tools + + - name: Build + uses: ente-io/action-electron-builder@v1.0.0 + with: + package_root: desktop + build_script_name: build:ci + + # GitHub token, automatically provided to the action + # (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') }} + + mac_certs: ${{ secrets.MAC_CERTS }} + mac_certs_password: ${{ secrets.MAC_CERTS_PASSWORD }} + env: + # macOS notarization credentials key details + APPLE_ID: ${{ secrets.APPLE_ID }} + APPLE_APP_SPECIFIC_PASSWORD: + ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + USE_HARD_LINKS: false diff --git a/desktop/CHANGELOG.md b/desktop/CHANGELOG.md index 83d2123d86183870259e96fdc964a48577857cff..eb118a424d84c41cbdc544832c3f7eef4f843056 100644 --- a/desktop/CHANGELOG.md +++ b/desktop/CHANGELOG.md @@ -1,5 +1,13 @@ # CHANGELOG +## v1.7.0 (Unreleased) + +v1.7 is a major rewrite to improve the security of our app. We have enabled +sandboxing and disabled node integration for the renderer process. All this +required restructuring our IPC mechanisms, which resulted in a lot of under the +hood changes. The outcome is a more secure app that also uses the latest and +greatest Electron recommendations. + ## v1.6.63 ### New diff --git a/desktop/README.md b/desktop/README.md index 05149f5d0ca802577ea6f1d9919e25dcb8d8afc6..39b7663fab7f914a022319692035329c8a5bd17f 100644 --- a/desktop/README.md +++ b/desktop/README.md @@ -10,12 +10,6 @@ To know more about Ente, see [our main README](../README.md) or visit ## Building from source -> [!CAUTION] -> -> We're improving the security of the desktop app further by migrating to -> Electron's sandboxing and contextIsolation. These updates are still WIP and -> meanwhile the instructions below might not fully work on the main branch. - Fetch submodules ```sh diff --git a/desktop/docs/dependencies.md b/desktop/docs/dependencies.md index 5c6b222b085e08421704b3ec15ebcede3676f95a..6052357033bc97fd0f94a20bd8465b5e5a8203a1 100644 --- a/desktop/docs/dependencies.md +++ b/desktop/docs/dependencies.md @@ -90,6 +90,9 @@ Some extra ones specific to the code here are: Unix commands in our `package.json` scripts. This allows us to use the same commands (like `ln`) across different platforms like Linux and Windows. +- [@tsconfig/recommended](https://github.com/tsconfig/bases) gives us a base + tsconfig for the Node.js version that our current Electron version uses. + ## Functionality ### Format conversion diff --git a/desktop/docs/release.md b/desktop/docs/release.md index 7254e26fc175fd0a8b3fe0565b55642b420a8388..a062d7d407f2df32c8185d0949c93dff80c8f048 100644 --- a/desktop/docs/release.md +++ b/desktop/docs/release.md @@ -1,43 +1,68 @@ ## Releases -> [!NOTE] -> -> TODO(MR): This document needs to be audited and changed as we do the first -> release from this new monorepo. +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. -The Github Action that builds the desktop binaries is triggered by pushing a tag -matching the pattern `photos-desktop-v1.2.3`. This value should match the -version in `package.json`. +The complication comes by the fact that electron-builder's auto updaterr (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. -So the process for doing a release would be. +- Source code lives here, in [ente-io/ente](https://github.com/ente-io/ente). -1. Create a new branch (can be named anything). On this branch, include your - changes. +- Releases are done from + [ente-io/photos-desktop](https://github.com/ente-io/photos-desktop). -2. Mention the changes in `CHANGELOG.md`. +## Workflow - Release Candidates -3. Changing the `version` in `package.json` to `1.x.x`. +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. -4. Commit and push to remote +The workflow for making such "rc" builds is: + +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. + +## Workflow - Release + +1. Update `package.json` in the source repo to use version `1.x.x`. Create a + new draft release in the release repo with tag `v1.x.x`. + +2. Push code to the `desktop/rc` branch in the source repo. Remember to update + update the CHANGELOG. + +3. In the release repo ```sh - git add package.json && git commit -m 'Release v1.x.x' - git tag v1.x.x - git push && git push --tags + ./.github/trigger-release.sh v1.x.x ``` -This by itself will already trigger a new release. The GitHub action will create -a new draft release that can then be used as descibed below. +4. If the build is successful, tag `desktop/rc` and merge it into main: -To wrap up, we also need to merge back these changes into main. So for that, + ```sh + # Assuming we're on desktop/rc that just got build -5. Open a PR for the branch that we're working on (where the above tag was - pushed from) to get it merged into main. + git tag photosd-v1.x.x + git push origin photosd-v1.x.x + + # Now open a PR to merge it into main + ``` -6. In this PR, also increase the version number for the next release train. That - is, supposed we just released `v4.0.1`. Then we'll change the version number - in main to `v4.0.2-next.0`. Each pre-release will modify the `next.0` part. - Finally, at the time of the next release, this'll become `v4.0.2`. +## Post build The GitHub Action runs on Windows, Linux and macOS. It produces the artifacts defined in the `build` value in `package.json`. @@ -46,29 +71,11 @@ defined in the `build` value in `package.json`. - Linux - An AppImage, and 3 other packages (`.rpm`, `.deb`, `.pacman`) - macOS - A universal DMG -Additionally, the GitHub action notarizes the macOS DMG. For this it needs -credentials provided via GitHub secrets. - -During the build the Sentry webpack plugin checks to see if SENTRY_AUTH_TOKEN is -defined. If so, it uploads the sourcemaps for the renderer process to Sentry -(For our GitHub action, the SENTRY_AUTH_TOKEN is defined as a GitHub secret). - -The sourcemaps for the main (node) process are currently not sent to Sentry -(this works fine in practice since the node process files are not minified, we -only run `tsc`). - -Once the build is done, a draft release with all these artifacts attached is -created. The build is idempotent, so if something goes wrong and we need to -re-run the GitHub action, just delete the draft release (if it got created) and -start a new run by pushing a new tag (if some code changes are required). - -If no code changes are required, say the build failed for some transient network -or sentry issue, we can even be re-run by the build by going to Github Action -age and rerun from there. This will re-trigger for the same tag. +Additionally, the GitHub action notarizes and signs the macOS DMG (For this it +uses credentials provided via GitHub secrets). -If everything goes well, we'll have a release on GitHub, and the corresponding -source maps for the renderer process uploaded to Sentry. There isn't anything -else to do: +To rollout the build, we need to publish the draft release. Thereafter, +everything is automated: - The website automatically redirects to the latest release on GitHub when people try to download. @@ -76,7 +83,7 @@ else to do: - The file formats with support auto update (Windows `exe`, the Linux AppImage and the macOS DMG) also check the latest GitHub release automatically to download and apply the update (the rest of the formats don't support auto - updates). + updates yet). - We're not putting the desktop app in other stores currently. It is available as a `brew cask`, but we only had to open a PR to add the initial formula, @@ -87,6 +94,4 @@ else to do: 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. Generally, in the version number for these -we'll add a label to the version, e.g. the "beta.x" in `1.x.x-beta.x`. This -should be done both in `package.json`, and what we tag the commit with. +to pre-release builds to customers. diff --git a/desktop/electron-builder.yml b/desktop/electron-builder.yml index 298b1c5f367f2e7117d4439ffe56b12a8d1aee92..c2c000ce9f223513536f7f32afa8997ebe671160 100644 --- a/desktop/electron-builder.yml +++ b/desktop/electron-builder.yml @@ -29,4 +29,3 @@ mac: arch: [universal] category: public.app-category.photography hardenedRuntime: true -afterSign: electron-builder-notarize diff --git a/desktop/package.json b/desktop/package.json index 69d54f75be72e4f7b3df0f660a3dabba82f611b5..7297a0c17be7beebfd8759210f8a6ab649b40117 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -1,8 +1,9 @@ { "name": "ente", - "version": "1.6.63", + "version": "1.7.0-rc", "private": true, "description": "Desktop client for Ente Photos", + "repository": "github:ente-io/photos-desktop", "author": "Ente ", "main": "app/main.js", "scripts": { @@ -10,13 +11,17 @@ "build-main": "tsc && electron-builder", "build-main:quick": "tsc && electron-builder --dir --config.compression=store --config.mac.identity=null", "build-renderer": "cd ../web && yarn install && yarn build:photos && cd ../desktop && shx rm -f out && shx ln -sf ../web/apps/photos/out out", + "build:ci": "yarn build-renderer && tsc", "build:quick": "yarn build-renderer && yarn build-main:quick", "dev": "concurrently --kill-others --success first --names 'main,rndr' \"yarn dev-main\" \"yarn dev-renderer\"", "dev-main": "tsc && electron app/main.js", "dev-renderer": "cd ../web && yarn install && yarn dev:photos", "postinstall": "electron-builder install-app-deps", - "lint": "yarn prettier --check . && eslint --ext .ts src", - "lint-fix": "yarn prettier --write . && eslint --fix --ext .ts src" + "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" + }, + "resolutions": { + "jackspeak": "2.1.1" }, "dependencies": { "any-shell-escape": "^0.1", @@ -34,14 +39,14 @@ "onnxruntime-node": "^1.17" }, "devDependencies": { + "@tsconfig/node20": "^20.1.4", "@types/auto-launch": "^5.0", "@types/ffmpeg-static": "^3.0", "@typescript-eslint/eslint-plugin": "^7", "@typescript-eslint/parser": "^7", "concurrently": "^8", - "electron": "^29", - "electron-builder": "^24", - "electron-builder-notarize": "^1.5", + "electron": "^30", + "electron-builder": "25.0.0-alpha.6", "eslint": "^8", "prettier": "^3", "prettier-plugin-organize-imports": "^3", diff --git a/desktop/src/main.ts b/desktop/src/main.ts index 2774ec730c7b88666414da18a3a06a44c4fb3a16..9cba9178df2a4a3a9ee09dc676dcb07fe7ac23b8 100644 --- a/desktop/src/main.ts +++ b/desktop/src/main.ts @@ -26,7 +26,7 @@ import { createWatcher } from "./main/services/watch"; import { userPreferences } from "./main/stores/user-preferences"; import { migrateLegacyWatchStoreIfNeeded } from "./main/stores/watch"; import { registerStreamProtocol } from "./main/stream"; -import { isDev } from "./main/utils-electron"; +import { isDev } from "./main/utils/electron"; /** * The URL where the renderer HTML is being served from. @@ -127,54 +127,22 @@ const registerPrivilegedSchemes = () => { { scheme: "stream", privileges: { - // TODO(MR): Remove the commented bits if we don't end up - // needing them by the time the IPC refactoring is done. - - // Prevent the insecure origin issues when fetching this - // secure: true, - // Allow the web fetch API in the renderer to use this scheme. supportFetchAPI: true, - // Allow it to be used with video tags. - // stream: true, }, }, ]); }; -/** - * [Note: Increased disk cache for the desktop app] - * - * Set the "disk-cache-size" command line flag to ask the Chromium process to - * use a larger size for the caches that it keeps on disk. This allows us to use - * the web based caching mechanisms on both the web and the desktop app, just - * ask the embedded Chromium to be a bit more generous in disk usage when - * running as the desktop app. - * - * The size we provide is in bytes. - * https://www.electronjs.org/docs/latest/api/command-line-switches#--disk-cache-sizesize - * - * Note that increasing the disk cache size does not guarantee that Chromium - * will respect in verbatim, it uses its own heuristics atop this hint. - * https://superuser.com/questions/378991/what-is-chrome-default-cache-size-limit/1577693#1577693 - * - * See also: [Note: Caching files]. - */ -const increaseDiskCache = () => - app.commandLine.appendSwitch( - "disk-cache-size", - `${5 * 1024 * 1024 * 1024}`, // 5 GB - ); - /** * Create an return the {@link BrowserWindow} that will form our app's UI. * * This window will show the HTML served from {@link rendererURL}. */ -const createMainWindow = async () => { +const createMainWindow = () => { // Create the main window. This'll show our web content. const window = new BrowserWindow({ webPreferences: { - preload: path.join(app.getAppPath(), "preload.js"), + preload: path.join(__dirname, "preload.js"), sandbox: true, }, // The color to show in the window until the web content gets loaded. @@ -184,7 +152,7 @@ const createMainWindow = async () => { show: false, }); - const wasAutoLaunched = await autoLauncher.wasAutoLaunched(); + const wasAutoLaunched = autoLauncher.wasAutoLaunched(); if (wasAutoLaunched) { // Don't automatically show the app's window if we were auto-launched. // On macOS, also hide the dock icon on macOS. @@ -198,7 +166,7 @@ const createMainWindow = async () => { if (isDev) window.webContents.openDevTools(); window.webContents.on("render-process-gone", (_, details) => { - log.error(`render-process-gone: ${details}`); + log.error(`render-process-gone: ${details.reason}`); window.webContents.reload(); }); @@ -227,7 +195,7 @@ const createMainWindow = async () => { }); window.on("show", () => { - if (process.platform == "darwin") app.dock.show(); + if (process.platform == "darwin") void app.dock.show(); }); // Let ipcRenderer know when mainWindow is in the foreground so that it can @@ -281,7 +249,7 @@ export const allowExternalLinks = (webContents: WebContents) => { // Returning `action` "deny" accomplishes this. webContents.setWindowOpenHandler(({ url }) => { if (!url.startsWith(rendererURL)) { - shell.openExternal(url); + void shell.openExternal(url); return { action: "deny" }; } else { return { action: "allow" }; @@ -319,30 +287,46 @@ const setupTrayItem = (mainWindow: BrowserWindow) => { /** * Older versions of our app used to maintain a cache dir using the main - * process. This has been deprecated in favor of using a normal web cache. + * process. This has been removed in favor of cache on the web layer. + * + * Delete the old cache dir if it exists. + * + * This will happen in two phases. The cache had three subdirectories: + * + * - Two of them, "thumbs" and "files", will be removed now (v1.7.0, May 2024). * - * See [Note: Increased disk cache for the desktop app] + * - The third one, "face-crops" will be removed once we finish the face search + * changes. See: [Note: Legacy face crops]. * - * Delete the old cache dir if it exists. This code was added March 2024, and - * can be removed after some time once most people have upgraded to newer - * versions. + * This migration code can be removed after some time once most people have + * upgraded to newer versions. */ const deleteLegacyDiskCacheDirIfExists = async () => { - // The existing code was passing "cache" as a parameter to getPath. This is - // incorrect if we go by the types - "cache" is not a valid value for the - // parameter to `app.getPath`. + 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. // - // It might be an issue in the types, since at runtime it seems to work. For - // example, on macOS I get `~/Library/Caches`. + // However, "cache" is not a valid parameter to getPath. It works! (for + // example, on macOS I get `~/Library/Caches`), but it is intentionally not + // documented as part of the public API: + // + // - docs: remove "cache" from app.getPath + // https://github.com/electron/electron/pull/33509 // // Irrespective, we replicate the original behaviour so that we get back the - // same path that the old got was getting. + // same path that the old code was getting. // - // @ts-expect-error + // @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 }); + await removeIfExists(path.join(cacheDir, "thumbs")); + await removeIfExists(path.join(cacheDir, "files")); } }; @@ -375,7 +359,6 @@ const main = () => { // The order of the next two calls is important setupRendererServer(); registerPrivilegedSchemes(); - increaseDiskCache(); migrateLegacyWatchStoreIfNeeded(); app.on("second-instance", () => { @@ -390,39 +373,35 @@ const main = () => { // Emitted once, when Electron has finished initializing. // // Note that some Electron APIs can only be used after this event occurs. - app.on("ready", async () => { - // Create window and prepare for the renderer. - mainWindow = await createMainWindow(); - attachIPCHandlers(); - attachFSWatchIPCHandlers(createWatcher(mainWindow)); - registerStreamProtocol(); - - // Configure the renderer's environment. - setDownloadPath(mainWindow.webContents); - allowExternalLinks(mainWindow.webContents); - - // TODO(MR): Remove or resurrect - // The commit that introduced this header override had the message - // "fix cors issue for uploads". Not sure what that means, so disabling - // it for now to see why exactly this is required. - // addAllowOriginHeader(mainWindow); - - // Start loading the renderer. - mainWindow.loadURL(rendererURL); - - // Continue on with the rest of the startup sequence. - Menu.setApplicationMenu(await createApplicationMenu(mainWindow)); - setupTrayItem(mainWindow); - if (!isDev) setupAutoUpdater(mainWindow); - - try { - deleteLegacyDiskCacheDirIfExists(); - deleteLegacyKeysStoreIfExists(); - } catch (e) { - // Log but otherwise ignore errors during non-critical startup - // actions. - log.error("Ignoring startup error", e); - } + void app.whenReady().then(() => { + void (async () => { + // Create window and prepare for the renderer. + mainWindow = createMainWindow(); + attachIPCHandlers(); + attachFSWatchIPCHandlers(createWatcher(mainWindow)); + registerStreamProtocol(); + + // Configure the renderer's environment. + setDownloadPath(mainWindow.webContents); + allowExternalLinks(mainWindow.webContents); + + // Start loading the renderer. + void mainWindow.loadURL(rendererURL); + + // Continue on with the rest of the startup sequence. + Menu.setApplicationMenu(await createApplicationMenu(mainWindow)); + setupTrayItem(mainWindow); + setupAutoUpdater(mainWindow); + + try { + await deleteLegacyDiskCacheDirIfExists(); + await deleteLegacyKeysStoreIfExists(); + } catch (e) { + // Log but otherwise ignore errors during non-critical startup + // actions. + log.error("Ignoring startup error", e); + } + })(); }); // This is a macOS only event. Show our window when the user activates the diff --git a/desktop/src/main/dialogs.ts b/desktop/src/main/dialogs.ts deleted file mode 100644 index f119e3d133c76e1dcca022f07e7ead1f4f45b046..0000000000000000000000000000000000000000 --- a/desktop/src/main/dialogs.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { dialog } from "electron/main"; -import fs from "node:fs/promises"; -import path from "node:path"; -import type { ElectronFile } from "../types/ipc"; -import { getElectronFile } from "./services/fs"; -import { getElectronFilesFromGoogleZip } from "./services/upload"; - -export const selectDirectory = async () => { - const result = await dialog.showOpenDialog({ - properties: ["openDirectory"], - }); - if (result.filePaths && result.filePaths.length > 0) { - return result.filePaths[0]?.split(path.sep)?.join(path.posix.sep); - } -}; - -export const showUploadFilesDialog = async () => { - const selectedFiles = await dialog.showOpenDialog({ - properties: ["openFile", "multiSelections"], - }); - const filePaths = selectedFiles.filePaths; - return await Promise.all(filePaths.map(getElectronFile)); -}; - -export const showUploadDirsDialog = async () => { - const dir = await dialog.showOpenDialog({ - properties: ["openDirectory", "multiSelections"], - }); - - let filePaths: string[] = []; - for (const dirPath of dir.filePaths) { - filePaths = [...filePaths, ...(await getDirFilePaths(dirPath))]; - } - - return await Promise.all(filePaths.map(getElectronFile)); -}; - -// https://stackoverflow.com/a/63111390 -const getDirFilePaths = async (dirPath: string) => { - if (!(await fs.stat(dirPath)).isDirectory()) { - return [dirPath]; - } - - let files: string[] = []; - const filePaths = await fs.readdir(dirPath); - - for (const filePath of filePaths) { - const absolute = path.join(dirPath, filePath); - files = [...files, ...(await getDirFilePaths(absolute))]; - } - - return files; -}; - -export const showUploadZipDialog = async () => { - const selectedFiles = await dialog.showOpenDialog({ - properties: ["openFile", "multiSelections"], - filters: [{ name: "Zip File", extensions: ["zip"] }], - }); - const filePaths = selectedFiles.filePaths; - - let files: ElectronFile[] = []; - - for (const filePath of filePaths) { - files = [...files, ...(await getElectronFilesFromGoogleZip(filePath))]; - } - - return { - zipPaths: filePaths, - files, - }; -}; diff --git a/desktop/src/main/fs.ts b/desktop/src/main/fs.ts deleted file mode 100644 index 2428d3a80ca9dbf98212672f5437552b55e68695..0000000000000000000000000000000000000000 --- a/desktop/src/main/fs.ts +++ /dev/null @@ -1,29 +0,0 @@ -/** - * @file file system related functions exposed over the context bridge. - */ -import { existsSync } from "node:fs"; -import fs from "node:fs/promises"; - -export const fsExists = (path: string) => existsSync(path); - -export const fsRename = (oldPath: string, newPath: string) => - fs.rename(oldPath, newPath); - -export const fsMkdirIfNeeded = (dirPath: string) => - fs.mkdir(dirPath, { recursive: true }); - -export const fsRmdir = (path: string) => fs.rmdir(path); - -export const fsRm = (path: string) => fs.rm(path); - -export const fsReadTextFile = async (filePath: string) => - fs.readFile(filePath, "utf-8"); - -export const fsWriteFile = (path: string, contents: string) => - fs.writeFile(path, contents); - -export const fsIsDir = async (dirPath: string) => { - if (!existsSync(dirPath)) return false; - const stat = await fs.stat(dirPath); - return stat.isDirectory(); -}; diff --git a/desktop/src/main/init.ts b/desktop/src/main/init.ts deleted file mode 100644 index d0aee17f8f2f73bb1bf463ecd9196d415eac3326..0000000000000000000000000000000000000000 --- a/desktop/src/main/init.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { BrowserWindow } from "electron"; - -export function addAllowOriginHeader(mainWindow: BrowserWindow) { - mainWindow.webContents.session.webRequest.onHeadersReceived( - (details, callback) => { - details.responseHeaders = lowerCaseHeaders(details.responseHeaders); - details.responseHeaders["access-control-allow-origin"] = ["*"]; - callback({ - responseHeaders: details.responseHeaders, - }); - }, - ); -} - -function lowerCaseHeaders(responseHeaders: Record) { - const headers: Record = {}; - for (const key of Object.keys(responseHeaders)) { - headers[key.toLowerCase()] = responseHeaders[key]; - } - return headers; -} diff --git a/desktop/src/main/ipc.ts b/desktop/src/main/ipc.ts index df6ab7c8ea4fcba25c41f83004fb67f67f85e6fc..1393f4bfd3471e0571620399295e082355a3eac8 100644 --- a/desktop/src/main/ipc.ts +++ b/desktop/src/main/ipc.ts @@ -16,12 +16,20 @@ import type { PendingUploads, ZipItem, } from "../types/ipc"; +import { logToDisk } from "./log"; +import { + appVersion, + skipAppUpdate, + updateAndRestart, + updateOnNextRestart, +} from "./services/app-update"; import { + legacyFaceCrop, + openDirectory, + openLogDirectory, selectDirectory, - showUploadDirsDialog, - showUploadFilesDialog, - showUploadZipDialog, -} from "./dialogs"; +} from "./services/dir"; +import { ffmpegExec } from "./services/ffmpeg"; import { fsExists, fsIsDir, @@ -31,15 +39,7 @@ import { fsRm, fsRmdir, fsWriteFile, -} from "./fs"; -import { logToDisk } from "./log"; -import { - appVersion, - skipAppUpdate, - updateAndRestart, - updateOnNextRestart, -} from "./services/app-update"; -import { ffmpegExec } from "./services/ffmpeg"; +} from "./services/fs"; import { convertToJPEG, generateImageThumbnail } from "./services/image"; import { clipImageEmbedding, @@ -65,10 +65,10 @@ import { watchFindFiles, watchGet, watchRemove, + watchReset, watchUpdateIgnoredFiles, watchUpdateSyncedFiles, } from "./services/watch"; -import { openDirectory, openLogDirectory } from "./utils-electron"; /** * Listen for IPC events sent/invoked by the renderer process, and route them to @@ -95,16 +95,20 @@ export const attachIPCHandlers = () => { ipcMain.handle("appVersion", () => appVersion()); - ipcMain.handle("openDirectory", (_, dirPath) => openDirectory(dirPath)); + ipcMain.handle("openDirectory", (_, dirPath: string) => + openDirectory(dirPath), + ); ipcMain.handle("openLogDirectory", () => openLogDirectory()); // See [Note: Catching exception during .send/.on] - ipcMain.on("logToDisk", (_, message) => logToDisk(message)); + ipcMain.on("logToDisk", (_, message: string) => logToDisk(message)); + + ipcMain.handle("selectDirectory", () => selectDirectory()); ipcMain.on("clearStores", () => clearStores()); - ipcMain.handle("saveEncryptionKey", (_, encryptionKey) => + ipcMain.handle("saveEncryptionKey", (_, encryptionKey: string) => saveEncryptionKey(encryptionKey), ); @@ -114,21 +118,23 @@ export const attachIPCHandlers = () => { ipcMain.on("updateAndRestart", () => updateAndRestart()); - ipcMain.on("updateOnNextRestart", (_, version) => + ipcMain.on("updateOnNextRestart", (_, version: string) => updateOnNextRestart(version), ); - ipcMain.on("skipAppUpdate", (_, version) => skipAppUpdate(version)); + ipcMain.on("skipAppUpdate", (_, version: string) => skipAppUpdate(version)); // - FS - ipcMain.handle("fsExists", (_, path) => fsExists(path)); + ipcMain.handle("fsExists", (_, path: string) => fsExists(path)); ipcMain.handle("fsRename", (_, oldPath: string, newPath: string) => fsRename(oldPath, newPath), ); - ipcMain.handle("fsMkdirIfNeeded", (_, dirPath) => fsMkdirIfNeeded(dirPath)); + ipcMain.handle("fsMkdirIfNeeded", (_, dirPath: string) => + fsMkdirIfNeeded(dirPath), + ); ipcMain.handle("fsRmdir", (_, path: string) => fsRmdir(path)); @@ -193,15 +199,9 @@ export const attachIPCHandlers = () => { faceEmbedding(input), ); - // - File selection - - ipcMain.handle("selectDirectory", () => selectDirectory()); - - ipcMain.handle("showUploadFilesDialog", () => showUploadFilesDialog()); - - ipcMain.handle("showUploadDirsDialog", () => showUploadDirsDialog()); - - ipcMain.handle("showUploadZipDialog", () => showUploadZipDialog()); + ipcMain.handle("legacyFaceCrop", (_, faceID: string) => + legacyFaceCrop(faceID), + ); // - Upload @@ -269,4 +269,6 @@ export const attachFSWatchIPCHandlers = (watcher: FSWatcher) => { ipcMain.handle("watchFindFiles", (_, folderPath: string) => watchFindFiles(folderPath), ); + + ipcMain.handle("watchReset", () => watchReset(watcher)); }; diff --git a/desktop/src/main/log.ts b/desktop/src/main/log.ts index 22ebb5300a0e48fc34329b154ac957e92ee5c7cc..cf1404a90a9a02e9c29b39b9be3167132a87d999 100644 --- a/desktop/src/main/log.ts +++ b/desktop/src/main/log.ts @@ -1,15 +1,15 @@ import log from "electron-log"; import util from "node:util"; -import { isDev } from "./utils-electron"; +import { isDev } from "./utils/electron"; /** * Initialize logging in the main process. * * This will set our underlying logger up to log to a file named `ente.log`, * - * - on Linux at ~/.config/ente/logs/main.log - * - on macOS at ~/Library/Logs/ente/main.log - * - on Windows at %USERPROFILE%\AppData\Roaming\ente\logs\main.log + * - on Linux at ~/.config/ente/logs/ente.log + * - on macOS at ~/Library/Logs/ente/ente.log + * - on Windows at %USERPROFILE%\AppData\Roaming\ente\logs\ente.log * * On dev builds, it will also log to the console. */ @@ -65,7 +65,7 @@ const logError_ = (message: string) => { if (isDev) console.error(`[error] ${message}`); }; -const logInfo = (...params: any[]) => { +const logInfo = (...params: unknown[]) => { const message = params .map((p) => (typeof p == "string" ? p : util.inspect(p))) .join(" "); @@ -73,7 +73,7 @@ const logInfo = (...params: any[]) => { if (isDev) console.log(`[info] ${message}`); }; -const logDebug = (param: () => any) => { +const logDebug = (param: () => unknown) => { if (isDev) { const p = param(); console.log(`[debug] ${typeof p == "string" ? p : util.inspect(p)}`); diff --git a/desktop/src/main/menu.ts b/desktop/src/main/menu.ts index 12b1ee17d3a31314eb1d3a28d6a38ddd1f1a93ef..188b195f825f1751bfa5f5a32358f8ed65db1a39 100644 --- a/desktop/src/main/menu.ts +++ b/desktop/src/main/menu.ts @@ -8,8 +8,8 @@ import { import { allowWindowClose } from "../main"; import { forceCheckForAppUpdates } from "./services/app-update"; import autoLauncher from "./services/auto-launcher"; +import { openLogDirectory } from "./services/dir"; import { userPreferences } from "./stores/user-preferences"; -import { isDev, openLogDirectory } from "./utils-electron"; /** Create and return the entries in the app's main menu bar */ export const createApplicationMenu = async (mainWindow: BrowserWindow) => { @@ -18,23 +18,20 @@ export const createApplicationMenu = async (mainWindow: BrowserWindow) => { // Whenever the menu is redrawn the current value of these variables is used // to set the checked state for the various settings checkboxes. let isAutoLaunchEnabled = await autoLauncher.isEnabled(); - let shouldHideDockIcon = userPreferences.get("hideDockIcon"); + let shouldHideDockIcon = !!userPreferences.get("hideDockIcon"); const macOSOnly = (options: MenuItemConstructorOptions[]) => process.platform == "darwin" ? options : []; - const devOnly = (options: MenuItemConstructorOptions[]) => - isDev ? options : []; - const handleCheckForUpdates = () => forceCheckForAppUpdates(mainWindow); const handleViewChangelog = () => - shell.openExternal( + void shell.openExternal( "https://github.com/ente-io/ente/blob/main/desktop/CHANGELOG.md", ); const toggleAutoLaunch = () => { - autoLauncher.toggleAutoLaunch(); + void autoLauncher.toggleAutoLaunch(); isAutoLaunchEnabled = !isAutoLaunchEnabled; }; @@ -45,13 +42,15 @@ export const createApplicationMenu = async (mainWindow: BrowserWindow) => { shouldHideDockIcon = !shouldHideDockIcon; }; - const handleHelp = () => shell.openExternal("https://help.ente.io/photos/"); + const handleHelp = () => + void shell.openExternal("https://help.ente.io/photos/"); - const handleSupport = () => shell.openExternal("mailto:support@ente.io"); + const handleSupport = () => + void shell.openExternal("mailto:support@ente.io"); - const handleBlog = () => shell.openExternal("https://ente.io/blog/"); + const handleBlog = () => void shell.openExternal("https://ente.io/blog/"); - const handleViewLogs = openLogDirectory; + const handleViewLogs = () => void openLogDirectory(); return Menu.buildFromTemplate([ { @@ -83,12 +82,14 @@ export const createApplicationMenu = async (mainWindow: BrowserWindow) => { checked: isAutoLaunchEnabled, click: toggleAutoLaunch, }, - { - label: "Hide Dock Icon", - type: "checkbox", - checked: shouldHideDockIcon, - click: toggleHideDockIcon, - }, + ...macOSOnly([ + { + label: "Hide Dock Icon", + type: "checkbox", + checked: shouldHideDockIcon, + click: toggleHideDockIcon, + }, + ]), ], }, @@ -127,11 +128,11 @@ export const createApplicationMenu = async (mainWindow: BrowserWindow) => { submenu: [ { role: "startSpeaking", - label: "start speaking", + label: "Start Speaking", }, { role: "stopSpeaking", - label: "stop speaking", + label: "Stop Speaking", }, ], }, @@ -142,9 +143,7 @@ export const createApplicationMenu = async (mainWindow: BrowserWindow) => { label: "View", submenu: [ { label: "Reload", role: "reload" }, - ...devOnly([ - { label: "Toggle Dev Tools", role: "toggleDevTools" }, - ]), + { label: "Toggle Dev Tools", role: "toggleDevTools" }, { type: "separator" }, { label: "Toggle Full Screen", role: "togglefullscreen" }, ], diff --git a/desktop/src/main/services/app-update.ts b/desktop/src/main/services/app-update.ts index e20d42fb705f3a8e521f7441437e71c45ecef479..c12e1e31932050c16713aa740fa99860b0035d6e 100644 --- a/desktop/src/main/services/app-update.ts +++ b/desktop/src/main/services/app-update.ts @@ -6,14 +6,93 @@ import { allowWindowClose } from "../../main"; import { AppUpdate } from "../../types/ipc"; import log from "../log"; import { userPreferences } from "../stores/user-preferences"; +import { isDev } from "../utils/electron"; export const setupAutoUpdater = (mainWindow: BrowserWindow) => { autoUpdater.logger = electronLog; autoUpdater.autoDownload = false; + /** + * [Note: Testing auto updates] + * + * By default, we skip checking for updates automatically in dev builds. + * This is because even if installing updates would fail (at least on macOS) + * because auto updates only work for signed builds. + * + * So an end to end testing for updates requires using a temporary GitHub + * repository and signed builds therein. More on this later. + * + * --------------- + * + * [Note: Testing auto updates - Sanity checks] + * + * However, for partial checks of the UI flow, something like the following + * can be used to do a test of the update process (up until the actual + * installation itself). + * + * Create a `app/dev-app-update.yml` with: + * + * provider: generic + * url: http://127.0.0.1:7777/ + * + * and start a local webserver in some directory: + * + * python3 -m http.server 7777 + * + * In this directory, put `latest-mac.yml` and the DMG file that this YAML + * file refers to. + * + * Alternatively, `dev-app-update.yml` can point to some arbitrary GitHub + * repository too, e.g.: + * + * provider: github + * owner: ente-io + * repo: test-desktop-updates + * + * Now we can use the "Check for updates..." menu option to trigger the + * update flow. + */ + autoUpdater.forceDevUpdateConfig = isDev; + if (isDev) return; + + /** + * [Note: Testing auto updates - End to end checks] + * + * Since end-to-end update testing can only be done with signed builds, the + * easiest way is to create temporary builds in a test repository. + * + * Let us say we have v2.0.0 about to go out. We have builds artifacts for + * v2.0.0 also in some draft release in our normal release repository. + * + * Create a new test repository, say `ente-io/test-desktop-updates`. In this + * repository, create a release v2.0.0, attaching the actual build + * artifacts. Make this release the latest. + * + * Now we need to create a old signed build. + * + * First, modify `package.json` to put in a version number older than the + * new version number that we want to test updating to, e.g. `v1.0.0-test`. + * + * Then uncomment the following block of code. This tells the auto updater + * to use `ente-io/test-desktop-updates` to get updates. + * + * With these two changes (older version and setFeedURL), create a new + * release signed build on CI. Install this build - it will check for + * updates in the temporary feed URL that we set, and we'll be able to check + * the full update flow. + */ + + /* + autoUpdater.setFeedURL({ + provider: "github", + owner: "ente-io", + repo: "test-desktop-updates", + }); + */ + const oneDay = 1 * 24 * 60 * 60 * 1000; - setInterval(() => checkForUpdatesAndNotify(mainWindow), oneDay); - checkForUpdatesAndNotify(mainWindow); + setInterval(() => void checkForUpdatesAndNotify(mainWindow), oneDay); + void checkForUpdatesAndNotify(mainWindow); }; /** @@ -22,7 +101,7 @@ export const setupAutoUpdater = (mainWindow: BrowserWindow) => { export const forceCheckForAppUpdates = (mainWindow: BrowserWindow) => { userPreferences.delete("skipAppVersion"); userPreferences.delete("muteUpdateNotificationVersion"); - checkForUpdatesAndNotify(mainWindow); + void checkForUpdatesAndNotify(mainWindow); }; const checkForUpdatesAndNotify = async (mainWindow: BrowserWindow) => { @@ -36,18 +115,21 @@ const checkForUpdatesAndNotify = async (mainWindow: BrowserWindow) => { log.debug(() => `Update check found version ${version}`); + if (!version) + throw new Error("Unexpected empty version obtained from auto-updater"); + if (compareVersions(version, app.getVersion()) <= 0) { log.debug(() => "Skipping update, already at latest version"); return; } - if (version === userPreferences.get("skipAppVersion")) { + if (version == userPreferences.get("skipAppVersion")) { log.info(`User chose to skip version ${version}`); return; } const mutedVersion = userPreferences.get("muteUpdateNotificationVersion"); - if (version === mutedVersion) { + if (version == mutedVersion) { log.info(`User has muted update notifications for version ${version}`); return; } @@ -56,7 +138,7 @@ const checkForUpdatesAndNotify = async (mainWindow: BrowserWindow) => { mainWindow.webContents.send("appUpdateAvailable", update); log.debug(() => "Attempting auto update"); - autoUpdater.downloadUpdate(); + await autoUpdater.downloadUpdate(); let timeoutId: ReturnType; const fiveMinutes = 5 * 60 * 1000; diff --git a/desktop/src/main/services/auto-launcher.ts b/desktop/src/main/services/auto-launcher.ts index c704f739990081b922ab0a1eb13fee189b70d96c..0942a49359884cc2db5213347e827237ff10028b 100644 --- a/desktop/src/main/services/auto-launcher.ts +++ b/desktop/src/main/services/auto-launcher.ts @@ -27,23 +27,22 @@ class AutoLauncher { } async toggleAutoLaunch() { - const isEnabled = await this.isEnabled(); + const wasEnabled = await this.isEnabled(); const autoLaunch = this.autoLaunch; if (autoLaunch) { - if (isEnabled) await autoLaunch.disable(); + if (wasEnabled) await autoLaunch.disable(); else await autoLaunch.enable(); } else { - if (isEnabled) app.setLoginItemSettings({ openAtLogin: false }); - else app.setLoginItemSettings({ openAtLogin: true }); + const openAtLogin = !wasEnabled; + app.setLoginItemSettings({ openAtLogin }); } } - async wasAutoLaunched() { + wasAutoLaunched() { if (this.autoLaunch) { return app.commandLine.hasSwitch("hidden"); } else { - // TODO(MR): This apparently doesn't work anymore. - return app.getLoginItemSettings().wasOpenedAtLogin; + return app.getLoginItemSettings().openAtLogin; } } } diff --git a/desktop/src/main/services/dir.ts b/desktop/src/main/services/dir.ts new file mode 100644 index 0000000000000000000000000000000000000000..293a720f0194c39bfa1f8f18ed267600df9b20d3 --- /dev/null +++ b/desktop/src/main/services/dir.ts @@ -0,0 +1,89 @@ +import { shell } from "electron/common"; +import { app, dialog } from "electron/main"; +import { existsSync } from "fs"; +import fs from "node:fs/promises"; +import path from "node:path"; +import { posixPath } from "../utils/electron"; + +export const selectDirectory = async () => { + const result = await dialog.showOpenDialog({ + properties: ["openDirectory"], + }); + const dirPath = result.filePaths[0]; + return dirPath ? posixPath(dirPath) : undefined; +}; + +/** + * Open the given {@link dirPath} in the system's folder viewer. + * + * For example, on macOS this'll open {@link dirPath} in Finder. + */ +export const openDirectory = async (dirPath: string) => { + // We need to use `path.normalize` because `shell.openPath; does not support + // POSIX path, it needs to be a platform specific path: + // https://github.com/electron/electron/issues/28831#issuecomment-826370589 + const res = await shell.openPath(path.normalize(dirPath)); + // `shell.openPath` resolves with a string containing the error message + // corresponding to the failure if a failure occurred, otherwise "". + if (res) throw new Error(`Failed to open directory ${dirPath}: res`); +}; + +/** + * Open the app's log directory in the system's folder viewer. + * + * @see {@link openDirectory} + */ +export const openLogDirectory = () => openDirectory(logDirectoryPath()); + +/** + * Return the path where the logs for the app are saved. + * + * [Note: Electron app paths] + * + * There are three paths we need to be aware of usually. + * + * First is the "appData". We can obtain this with `app.getPath("appData")`. + * This is per-user application data directory. This is usually the following: + * + * - Windows: `%APPDATA%`, e.g. `C:\Users\\AppData\Local` + * - Linux: `~/.config` + * - macOS: `~/Library/Application Support` + * + * Now, if we suffix the app's name onto the appData directory, we get the + * "userData" directory. This is the **primary** place applications are meant to + * store user's data, e.g. various configuration files and saved state. + * + * During development, our app name is "Electron", so this'd be, for example, + * `~/Library/Application Support/Electron` if we run using `yarn dev`. For the + * packaged production app, our app name is "ente", so this would be: + * + * - Windows: `%APPDATA%\ente`, e.g. `C:\Users\\AppData\Local\ente` + * - Linux: `~/.config/ente` + * - macOS: `~/Library/Application Support/ente` + * + * Note that Chromium also stores the browser state, e.g. localStorage or disk + * caches, in userData. + * + * Finally, there is the "logs" directory. This is not within "appData" but has + * a slightly different OS specific path. Since our log file is named + * "ente.log", it can be found at: + * + * - macOS: ~/Library/Logs/ente/ente.log (production) + * - macOS: ~/Library/Logs/Electron/ente.log (dev) + * + * https://www.electronjs.org/docs/latest/api/app + */ +const logDirectoryPath = () => app.getPath("logs"); + +/** + * See: [Note: Legacy face crops] + */ +export const legacyFaceCrop = async ( + faceID: string, +): Promise => { + // See: [Note: Getting the cache path] + // @ts-expect-error "cache" works but is not part of the public API. + const cacheDir = path.join(app.getPath("cache"), "ente"); + const filePath = path.join(cacheDir, "face-crops", faceID); + return existsSync(filePath) ? await fs.readFile(filePath) : undefined; +}; diff --git a/desktop/src/main/services/ffmpeg.ts b/desktop/src/main/services/ffmpeg.ts index 35977409ae82cecdda52f02c2e2059c3c27ba69a..6b1171459fe35214ecaccd04c359f2a084406568 100644 --- a/desktop/src/main/services/ffmpeg.ts +++ b/desktop/src/main/services/ffmpeg.ts @@ -2,13 +2,13 @@ import pathToFfmpeg from "ffmpeg-static"; import fs from "node:fs/promises"; import type { ZipItem } from "../../types/ipc"; import log from "../log"; -import { withTimeout } from "../utils"; -import { execAsync } from "../utils-electron"; +import { ensure, withTimeout } from "../utils/common"; +import { execAsync } from "../utils/electron"; import { deleteTempFile, makeFileForDataOrPathOrZipItem, makeTempFilePath, -} from "../utils-temp"; +} from "../utils/temp"; /* Duplicated in the web app's code (used by the WASM FFmpeg implementation). */ const ffmpegPathPlaceholder = "FFMPEG"; @@ -69,7 +69,7 @@ export const ffmpegExec = async ( outputFilePath, ); - if (timeoutMS) await withTimeout(execAsync(cmd), 30 * 1000); + if (timeoutMS) await withTimeout(execAsync(cmd), timeoutMS); else await execAsync(cmd); return fs.readFile(outputFilePath); @@ -110,5 +110,5 @@ const ffmpegBinaryPath = () => { // This substitution of app.asar by app.asar.unpacked is suggested by the // ffmpeg-static library author themselves: // https://github.com/eugeneware/ffmpeg-static/issues/16 - return pathToFfmpeg.replace("app.asar", "app.asar.unpacked"); + return ensure(pathToFfmpeg).replace("app.asar", "app.asar.unpacked"); }; diff --git a/desktop/src/main/services/fs.ts b/desktop/src/main/services/fs.ts index 609fc82d7e05861319514280641740f42ce022c4..4570a4a33a1bef37c6150615bcda96ccc8a9fc90 100644 --- a/desktop/src/main/services/fs.ts +++ b/desktop/src/main/services/fs.ts @@ -1,154 +1,30 @@ -import StreamZip from "node-stream-zip"; +/** + * @file file system related functions exposed over the context bridge. + */ + import { existsSync } from "node:fs"; import fs from "node:fs/promises"; -import path from "node:path"; -import { ElectronFile } from "../../types/ipc"; -import log from "../log"; -const FILE_STREAM_CHUNK_SIZE: number = 4 * 1024 * 1024; +export const fsExists = (path: string) => existsSync(path); -const getFileStream = async (filePath: string) => { - const file = await fs.open(filePath, "r"); - let offset = 0; - const readableStream = new ReadableStream({ - async pull(controller) { - try { - const buff = new Uint8Array(FILE_STREAM_CHUNK_SIZE); - const bytesRead = (await file.read( - buff, - 0, - FILE_STREAM_CHUNK_SIZE, - offset, - )) as unknown as number; - offset += bytesRead; - if (bytesRead === 0) { - controller.close(); - await file.close(); - } else { - controller.enqueue(buff.slice(0, bytesRead)); - } - } catch (e) { - await file.close(); - } - }, - async cancel() { - await file.close(); - }, - }); - return readableStream; -}; +export const fsRename = (oldPath: string, newPath: string) => + fs.rename(oldPath, newPath); -export async function getElectronFile(filePath: string): Promise { - const fileStats = await fs.stat(filePath); - return { - path: filePath.split(path.sep).join(path.posix.sep), - name: path.basename(filePath), - size: fileStats.size, - lastModified: fileStats.mtime.valueOf(), - stream: async () => { - if (!existsSync(filePath)) { - throw new Error("electronFile does not exist"); - } - return await getFileStream(filePath); - }, - blob: async () => { - if (!existsSync(filePath)) { - throw new Error("electronFile does not exist"); - } - const blob = await fs.readFile(filePath); - return new Blob([new Uint8Array(blob)]); - }, - arrayBuffer: async () => { - if (!existsSync(filePath)) { - throw new Error("electronFile does not exist"); - } - const blob = await fs.readFile(filePath); - return new Uint8Array(blob); - }, - }; -} +export const fsMkdirIfNeeded = (dirPath: string) => + fs.mkdir(dirPath, { recursive: true }); -export const getZipFileStream = async ( - zip: StreamZip.StreamZipAsync, - filePath: string, -) => { - const stream = await zip.stream(filePath); - const done = { - current: false, - }; - const inProgress = { - current: false, - }; - // eslint-disable-next-line no-unused-vars - let resolveObj: (value?: any) => void = null; - // eslint-disable-next-line no-unused-vars - let rejectObj: (reason?: any) => void = null; - stream.on("readable", () => { - try { - if (resolveObj) { - inProgress.current = true; - const chunk = stream.read(FILE_STREAM_CHUNK_SIZE) as Buffer; - if (chunk) { - resolveObj(new Uint8Array(chunk)); - resolveObj = null; - } - inProgress.current = false; - } - } catch (e) { - rejectObj(e); - } - }); - stream.on("end", () => { - try { - done.current = true; - if (resolveObj && !inProgress.current) { - resolveObj(null); - resolveObj = null; - } - } catch (e) { - rejectObj(e); - } - }); - stream.on("error", (e) => { - try { - done.current = true; - if (rejectObj) { - rejectObj(e); - rejectObj = null; - } - } catch (e) { - rejectObj(e); - } - }); +export const fsRmdir = (path: string) => fs.rmdir(path); - const readStreamData = async () => { - return new Promise((resolve, reject) => { - const chunk = stream.read(FILE_STREAM_CHUNK_SIZE) as Buffer; +export const fsRm = (path: string) => fs.rm(path); - if (chunk || done.current) { - resolve(chunk); - } else { - resolveObj = resolve; - rejectObj = reject; - } - }); - }; +export const fsReadTextFile = async (filePath: string) => + fs.readFile(filePath, "utf-8"); - const readableStream = new ReadableStream({ - async pull(controller) { - try { - const data = await readStreamData(); +export const fsWriteFile = (path: string, contents: string) => + fs.writeFile(path, contents); - if (data) { - controller.enqueue(data); - } else { - controller.close(); - } - } catch (e) { - log.error("Failed to pull from readableStream", e); - controller.close(); - } - }, - }); - return readableStream; +export const fsIsDir = async (dirPath: string) => { + if (!existsSync(dirPath)) return false; + const stat = await fs.stat(dirPath); + return stat.isDirectory(); }; diff --git a/desktop/src/main/services/image.ts b/desktop/src/main/services/image.ts index c48e87c5bf3668398677a1b5ac5d60c22893c9c6..957fe8120003d4ad59a73634c7080bad6f78a33a 100644 --- a/desktop/src/main/services/image.ts +++ b/desktop/src/main/services/image.ts @@ -1,15 +1,15 @@ /** @file Image format conversions and thumbnail generation */ import fs from "node:fs/promises"; -import path from "path"; +import path from "node:path"; import { CustomErrorMessage, type ZipItem } from "../../types/ipc"; import log from "../log"; -import { execAsync, isDev } from "../utils-electron"; +import { execAsync, isDev } from "../utils/electron"; import { deleteTempFile, makeFileForDataOrPathOrZipItem, makeTempFilePath, -} from "../utils-temp"; +} from "../utils/temp"; export const convertToJPEG = async (imageData: Uint8Array) => { const inputFilePath = await makeTempFilePath(); diff --git a/desktop/src/main/services/ml-clip.ts b/desktop/src/main/services/ml-clip.ts index cdd2baab76e5316056cb4e83824bd78e2c2b8e96..e3dd99204a369e6f8d965a1629220a860d81ff86 100644 --- a/desktop/src/main/services/ml-clip.ts +++ b/desktop/src/main/services/ml-clip.ts @@ -11,7 +11,8 @@ import * as ort from "onnxruntime-node"; import Tokenizer from "../../thirdparty/clip-bpe-ts/mod"; import log from "../log"; import { writeStream } from "../stream"; -import { deleteTempFile, makeTempFilePath } from "../utils-temp"; +import { ensure } from "../utils/common"; +import { deleteTempFile, makeTempFilePath } from "../utils/temp"; import { makeCachedInferenceSession } from "./ml"; const cachedCLIPImageSession = makeCachedInferenceSession( @@ -22,7 +23,7 @@ const cachedCLIPImageSession = makeCachedInferenceSession( export const clipImageEmbedding = async (jpegImageData: Uint8Array) => { const tempFilePath = await makeTempFilePath(); const imageStream = new Response(jpegImageData.buffer).body; - await writeStream(tempFilePath, imageStream); + await writeStream(tempFilePath, ensure(imageStream)); try { return await clipImageEmbedding_(tempFilePath); } finally { @@ -44,30 +45,30 @@ const clipImageEmbedding_ = async (jpegFilePath: string) => { `onnx/clip image embedding took ${Date.now() - t1} ms (prep: ${t2 - t1} ms, inference: ${Date.now() - t2} ms)`, ); /* Need these model specific casts to type the result */ - const imageEmbedding = results["output"].data as Float32Array; + const imageEmbedding = ensure(results.output).data as Float32Array; return normalizeEmbedding(imageEmbedding); }; -const getRGBData = async (jpegFilePath: string) => { +const getRGBData = async (jpegFilePath: string): Promise => { const jpegData = await fs.readFile(jpegFilePath); const rawImageData = jpeg.decode(jpegData, { useTArray: true, formatAsRGBA: false, }); - const nx: number = rawImageData.width; - const ny: number = rawImageData.height; - const inputImage: Uint8Array = rawImageData.data; + const nx = rawImageData.width; + const ny = rawImageData.height; + const inputImage = rawImageData.data; - const nx2: number = 224; - const ny2: number = 224; - const totalSize: number = 3 * nx2 * ny2; + const nx2 = 224; + const ny2 = 224; + const totalSize = 3 * nx2 * ny2; - const result: number[] = Array(totalSize).fill(0); - const scale: number = Math.max(nx, ny) / 224; + const result = Array(totalSize).fill(0); + const scale = Math.max(nx, ny) / 224; - const nx3: number = Math.round(nx / scale); - const ny3: number = Math.round(ny / scale); + const nx3 = Math.round(nx / scale); + const ny3 = Math.round(ny / scale); const mean: number[] = [0.48145466, 0.4578275, 0.40821073]; const std: number[] = [0.26862954, 0.26130258, 0.27577711]; @@ -76,40 +77,40 @@ const getRGBData = async (jpegFilePath: string) => { for (let x = 0; x < nx3; x++) { for (let c = 0; c < 3; c++) { // Linear interpolation - const sx: number = (x + 0.5) * scale - 0.5; - const sy: number = (y + 0.5) * scale - 0.5; + const sx = (x + 0.5) * scale - 0.5; + const sy = (y + 0.5) * scale - 0.5; - const x0: number = Math.max(0, Math.floor(sx)); - const y0: number = Math.max(0, Math.floor(sy)); + const x0 = Math.max(0, Math.floor(sx)); + const y0 = Math.max(0, Math.floor(sy)); - const x1: number = Math.min(x0 + 1, nx - 1); - const y1: number = Math.min(y0 + 1, ny - 1); + const x1 = Math.min(x0 + 1, nx - 1); + const y1 = Math.min(y0 + 1, ny - 1); - const dx: number = sx - x0; - const dy: number = sy - y0; + const dx = sx - x0; + const dy = sy - y0; - const j00: number = 3 * (y0 * nx + x0) + c; - const j01: number = 3 * (y0 * nx + x1) + c; - const j10: number = 3 * (y1 * nx + x0) + c; - const j11: number = 3 * (y1 * nx + x1) + c; + const j00 = 3 * (y0 * nx + x0) + c; + const j01 = 3 * (y0 * nx + x1) + c; + const j10 = 3 * (y1 * nx + x0) + c; + const j11 = 3 * (y1 * nx + x1) + c; - const v00: number = inputImage[j00]; - const v01: number = inputImage[j01]; - const v10: number = inputImage[j10]; - const v11: number = inputImage[j11]; + const v00 = inputImage[j00] ?? 0; + const v01 = inputImage[j01] ?? 0; + const v10 = inputImage[j10] ?? 0; + const v11 = inputImage[j11] ?? 0; - const v0: number = v00 * (1 - dx) + v01 * dx; - const v1: number = v10 * (1 - dx) + v11 * dx; + const v0 = v00 * (1 - dx) + v01 * dx; + const v1 = v10 * (1 - dx) + v11 * dx; - const v: number = v0 * (1 - dy) + v1 * dy; + const v = v0 * (1 - dy) + v1 * dy; - const v2: number = Math.min(Math.max(Math.round(v), 0), 255); + const v2 = Math.min(Math.max(Math.round(v), 0), 255); // createTensorWithDataList is dumb compared to reshape and // hence has to be given with one channel after another - const i: number = y * nx3 + x + (c % 3) * 224 * 224; + const i = y * nx3 + x + (c % 3) * 224 * 224; - result[i] = (v2 / 255 - mean[c]) / std[c]; + result[i] = (v2 / 255 - (mean[c] ?? 0)) / (std[c] ?? 1); } } } @@ -119,13 +120,12 @@ const getRGBData = async (jpegFilePath: string) => { const normalizeEmbedding = (embedding: Float32Array) => { let normalization = 0; - for (let index = 0; index < embedding.length; index++) { - normalization += embedding[index] * embedding[index]; - } + for (const v of embedding) normalization += v * v; + const sqrtNormalization = Math.sqrt(normalization); - for (let index = 0; index < embedding.length; index++) { - embedding[index] = embedding[index] / sqrtNormalization; - } + for (let index = 0; index < embedding.length; index++) + embedding[index] = ensure(embedding[index]) / sqrtNormalization; + return embedding; }; @@ -134,11 +134,9 @@ const cachedCLIPTextSession = makeCachedInferenceSession( 64173509 /* 61.2 MB */, ); -let _tokenizer: Tokenizer = null; +let _tokenizer: Tokenizer | undefined; const getTokenizer = () => { - if (!_tokenizer) { - _tokenizer = new Tokenizer(); - } + if (!_tokenizer) _tokenizer = new Tokenizer(); return _tokenizer; }; @@ -169,6 +167,6 @@ export const clipTextEmbeddingIfAvailable = async (text: string) => { () => `onnx/clip text embedding took ${Date.now() - t1} ms (prep: ${t2 - t1} ms, inference: ${Date.now() - t2} ms)`, ); - const textEmbedding = results["output"].data as Float32Array; + const textEmbedding = ensure(results.output).data as Float32Array; return normalizeEmbedding(textEmbedding); }; diff --git a/desktop/src/main/services/ml-face.ts b/desktop/src/main/services/ml-face.ts index 2309d193cddfbf11c1fa2759f8811a423d5b10c5..9765252555420c1552f4d49f72a332ba667291fe 100644 --- a/desktop/src/main/services/ml-face.ts +++ b/desktop/src/main/services/ml-face.ts @@ -8,6 +8,7 @@ */ import * as ort from "onnxruntime-node"; import log from "../log"; +import { ensure } from "../utils/common"; import { makeCachedInferenceSession } from "./ml"; const cachedFaceDetectionSession = makeCachedInferenceSession( @@ -23,7 +24,7 @@ export const detectFaces = async (input: Float32Array) => { }; const results = await session.run(feeds); log.debug(() => `onnx/yolo face detection took ${Date.now() - t} ms`); - return results["output"].data; + return ensure(results.output).data; }; const cachedFaceEmbeddingSession = makeCachedInferenceSession( @@ -46,5 +47,6 @@ export const faceEmbedding = async (input: Float32Array) => { const results = await session.run(feeds); log.debug(() => `onnx/yolo face embedding took ${Date.now() - t} ms`); /* Need these model specific casts to extract and type the result */ - return (results.embeddings as unknown as any)["cpuData"] as Float32Array; + return (results.embeddings as unknown as Record) + .cpuData as Float32Array; }; diff --git a/desktop/src/main/services/ml.ts b/desktop/src/main/services/ml.ts index 8292596a22f697687ca701dead15a131ab6b2aaf..6b38bc74dc7d4e29663a5938849fb5369717eb76 100644 --- a/desktop/src/main/services/ml.ts +++ b/desktop/src/main/services/ml.ts @@ -34,6 +34,7 @@ import { writeStream } from "../stream"; * actively trigger a download until the returned function is called. * * @param modelName The name of the model to download. + * * @param modelByteSize The size in bytes that we expect the model to have. If * the size of the downloaded model does not match the expected size, then we * will redownload it. @@ -99,13 +100,15 @@ const downloadModel = async (saveLocation: string, name: string) => { // `mkdir -p` the directory where we want to save the model. const saveDir = path.dirname(saveLocation); await fs.mkdir(saveDir, { recursive: true }); - // Download + // Download. log.info(`Downloading ML model from ${name}`); const url = `https://models.ente.io/${name}`; const res = await net.fetch(url); if (!res.ok) throw new Error(`Failed to fetch ${url}: HTTP ${res.status}`); - // Save - await writeStream(saveLocation, res.body); + const body = res.body; + if (!body) throw new Error(`Received an null response for ${url}`); + // Save. + await writeStream(saveLocation, body); log.info(`Downloaded CLIP model ${name}`); }; @@ -114,9 +117,9 @@ const downloadModel = async (saveLocation: string, name: string) => { */ const createInferenceSession = async (modelPath: string) => { return await ort.InferenceSession.create(modelPath, { - // Restrict the number of threads to 1 + // Restrict the number of threads to 1. intraOpNumThreads: 1, - // Be more conservative with RAM usage + // Be more conservative with RAM usage. enableCpuMemArena: false, }); }; diff --git a/desktop/src/main/services/store.ts b/desktop/src/main/services/store.ts index 9ec65c8c38117b1cd417d03771e36471fa8b6a48..471928d76ce70a5f85e77f22d516b6d9c9a27037 100644 --- a/desktop/src/main/services/store.ts +++ b/desktop/src/main/services/store.ts @@ -9,20 +9,29 @@ import { watchStore } from "../stores/watch"; * This is useful to reset state when the user logs out. */ export const clearStores = () => { - uploadStatusStore.clear(); safeStorageStore.clear(); + uploadStatusStore.clear(); watchStore.clear(); }; -export const saveEncryptionKey = async (encryptionKey: string) => { - const encryptedKey: Buffer = await safeStorage.encryptString(encryptionKey); +/** + * [Note: Safe storage keys] + * + * On macOS, `safeStorage` stores our data under a Keychain entry named + * " Safe Storage". Which resolves to: + * + * - Electron Safe Storage (dev) + * - ente Safe Storage (prod) + */ +export const saveEncryptionKey = (encryptionKey: string) => { + const encryptedKey = safeStorage.encryptString(encryptionKey); const b64EncryptedKey = Buffer.from(encryptedKey).toString("base64"); safeStorageStore.set("encryptionKey", b64EncryptedKey); }; -export const encryptionKey = async (): Promise => { +export const encryptionKey = (): string | undefined => { const b64EncryptedKey = safeStorageStore.get("encryptionKey"); if (!b64EncryptedKey) return undefined; const keyBuffer = Buffer.from(b64EncryptedKey, "base64"); - return await safeStorage.decryptString(keyBuffer); + return safeStorage.decryptString(keyBuffer); }; diff --git a/desktop/src/main/services/upload.ts b/desktop/src/main/services/upload.ts index 9b24cc0eadf907ac05440fb412ee4589137bdc17..f7d0436c0b0f3b7c149ff85089c647031c938a1e 100644 --- a/desktop/src/main/services/upload.ts +++ b/desktop/src/main/services/upload.ts @@ -1,10 +1,9 @@ import StreamZip from "node-stream-zip"; import fs from "node:fs/promises"; +import path from "node:path"; import { existsSync } from "original-fs"; -import path from "path"; -import type { ElectronFile, PendingUploads, ZipItem } from "../../types/ipc"; +import type { PendingUploads, ZipItem } from "../../types/ipc"; import { uploadStatusStore } from "../stores/upload-status"; -import { getZipFileStream } from "./fs"; export const listZipItems = async (zipPath: string): Promise => { const zip = new StreamZip.async({ file: zipPath }); @@ -15,13 +14,13 @@ export const listZipItems = async (zipPath: string): Promise => { for (const entry of Object.values(entries)) { const basename = path.basename(entry.name); // Ignore "hidden" files (files whose names begins with a dot). - if (entry.isFile && basename.length > 0 && basename[0] != ".") { + if (entry.isFile && !basename.startsWith(".")) { // `entry.name` is the path within the zip. entryNames.push(entry.name); } } - zip.close(); + await zip.close(); return entryNames.map((entryName) => [zipPath, entryName]); }; @@ -36,14 +35,18 @@ export const pathOrZipItemSize = async ( const [zipPath, entryName] = pathOrZipItem; const zip = new StreamZip.async({ file: zipPath }); const entry = await zip.entry(entryName); + if (!entry) + throw new Error( + `An entry with name ${entryName} does not exist in the zip file at ${zipPath}`, + ); const size = entry.size; - zip.close(); + await zip.close(); return size; } }; export const pendingUploads = async (): Promise => { - const collectionName = uploadStatusStore.get("collectionName"); + const collectionName = uploadStatusStore.get("collectionName") ?? undefined; const allFilePaths = uploadStatusStore.get("filePaths") ?? []; const filePaths = allFilePaths.filter((f) => existsSync(f)); @@ -59,9 +62,9 @@ export const pendingUploads = async (): Promise => { // // This potentially can be cause us to try reuploading an already uploaded // file, but the dedup logic will kick in at that point so no harm will come - // off it. + // of it. if (allZipItems === undefined) { - const allZipPaths = uploadStatusStore.get("filePaths"); + const allZipPaths = uploadStatusStore.get("filePaths") ?? []; const zipPaths = allZipPaths.filter((f) => existsSync(f)); zipItems = []; for (const zip of zipPaths) @@ -79,19 +82,64 @@ export const pendingUploads = async (): Promise => { }; }; -export const setPendingUploads = async (pendingUploads: PendingUploads) => - uploadStatusStore.set(pendingUploads); +/** + * [Note: Missing values in electron-store] + * + * Suppose we were to create a store like this: + * + * const store = new Store({ + * schema: { + * foo: { type: "string" }, + * bars: { type: "array", items: { type: "string" } }, + * }, + * }); + * + * If we fetch `store.get("foo")` or `store.get("bars")`, we get `undefined`. + * But if we try to set these back to `undefined`, say `store.set("foo", + * someUndefValue)`, we get asked to + * + * TypeError: Use `delete()` to clear values + * + * This happens even if we do bulk object updates, e.g. with a JS object that + * has undefined keys: + * + * > TypeError: Setting a value of type `undefined` for key `collectionName` is + * > not allowed as it's not supported by JSON + * + * So what should the TypeScript type for "foo" be? + * + * If it is were to not include the possibility of `undefined`, then the type + * would lie because `store.get("foo")` can indeed be `undefined. But if we were + * to include the possibility of `undefined`, then trying to `store.set("foo", + * someUndefValue)` will throw. + * + * The approach we take is to rely on false-y values (empty strings and empty + * arrays) to indicate missing values, and then converting those to `undefined` + * when reading from the store, and converting `undefined` to the corresponding + * false-y value when writing. + */ +export const setPendingUploads = ({ + collectionName, + filePaths, + zipItems, +}: PendingUploads) => { + uploadStatusStore.set({ + collectionName: collectionName ?? "", + filePaths: filePaths, + zipItems: zipItems, + }); +}; -export const markUploadedFiles = async (paths: string[]) => { - const existing = uploadStatusStore.get("filePaths"); +export const markUploadedFiles = (paths: string[]) => { + const existing = uploadStatusStore.get("filePaths") ?? []; const updated = existing.filter((p) => !paths.includes(p)); uploadStatusStore.set("filePaths", updated); }; -export const markUploadedZipItems = async ( +export const markUploadedZipItems = ( items: [zipPath: string, entryName: string][], ) => { - const existing = uploadStatusStore.get("zipItems"); + const existing = uploadStatusStore.get("zipItems") ?? []; const updated = existing.filter( (z) => !items.some((e) => z[0] == e[0] && z[1] == e[1]), ); @@ -99,51 +147,3 @@ export const markUploadedZipItems = async ( }; export const clearPendingUploads = () => uploadStatusStore.clear(); - -export const getElectronFilesFromGoogleZip = async (filePath: string) => { - const zip = new StreamZip.async({ - file: filePath, - }); - const zipName = path.basename(filePath, ".zip"); - - const entries = await zip.entries(); - const files: ElectronFile[] = []; - - for (const entry of Object.values(entries)) { - const basename = path.basename(entry.name); - if (entry.isFile && basename.length > 0 && basename[0] !== ".") { - files.push(await getZipEntryAsElectronFile(zipName, zip, entry)); - } - } - - zip.close(); - - return files; -}; - -export async function getZipEntryAsElectronFile( - zipName: string, - zip: StreamZip.StreamZipAsync, - entry: StreamZip.ZipEntry, -): Promise { - return { - path: path - .join(zipName, entry.name) - .split(path.sep) - .join(path.posix.sep), - name: path.basename(entry.name), - size: entry.size, - lastModified: entry.time, - stream: async () => { - return await getZipFileStream(zip, entry.name); - }, - blob: async () => { - const buffer = await zip.entryData(entry.name); - return new Blob([new Uint8Array(buffer)]); - }, - arrayBuffer: async () => { - const buffer = await zip.entryData(entry.name); - return new Uint8Array(buffer); - }, - }; -} diff --git a/desktop/src/main/services/watch.ts b/desktop/src/main/services/watch.ts index 73a13c545510cb7f42e780d77b9a8e1d3273ac05..de66dcca1c0498e0606dd567853da50d31a5a66d 100644 --- a/desktop/src/main/services/watch.ts +++ b/desktop/src/main/services/watch.ts @@ -3,9 +3,10 @@ import { BrowserWindow } from "electron/main"; import fs from "node:fs/promises"; import path from "node:path"; import { FolderWatch, type CollectionMapping } from "../../types/ipc"; -import { fsIsDir } from "../fs"; import log from "../log"; import { watchStore } from "../stores/watch"; +import { posixPath } from "../utils/electron"; +import { fsIsDir } from "./fs"; /** * Create and return a new file system watcher. @@ -34,8 +35,8 @@ export const createWatcher = (mainWindow: BrowserWindow) => { return watcher; }; -const eventData = (path: string): [string, FolderWatch] => { - path = posixPath(path); +const eventData = (platformPath: string): [string, FolderWatch] => { + const path = posixPath(platformPath); const watch = folderWatches().find((watch) => path.startsWith(watch.folderPath + "/"), @@ -46,23 +47,15 @@ const eventData = (path: string): [string, FolderWatch] => { return [path, watch]; }; -/** - * Convert a file system {@link filePath} that uses the local system specific - * path separators into a path that uses POSIX file separators. - */ -const posixPath = (filePath: string) => - filePath.split(path.sep).join(path.posix.sep); - -export const watchGet = (watcher: FSWatcher) => { - const [valid, deleted] = folderWatches().reduce( - ([valid, deleted], watch) => { - (fsIsDir(watch.folderPath) ? valid : deleted).push(watch); - return [valid, deleted]; - }, - [[], []], - ); - if (deleted.length) { - for (const watch of deleted) watchRemove(watcher, watch.folderPath); +export const watchGet = async (watcher: FSWatcher): Promise => { + const valid: FolderWatch[] = []; + const deletedPaths: string[] = []; + for (const watch of folderWatches()) { + if (await fsIsDir(watch.folderPath)) valid.push(watch); + else deletedPaths.push(watch.folderPath); + } + if (deletedPaths.length) { + await Promise.all(deletedPaths.map((p) => watchRemove(watcher, p))); setFolderWatches(valid); } return valid; @@ -80,7 +73,7 @@ export const watchAdd = async ( ) => { const watches = folderWatches(); - if (!fsIsDir(folderPath)) + if (!(await fsIsDir(folderPath))) throw new Error( `Attempting to add a folder watch for a folder path ${folderPath} that is not an existing directory`, ); @@ -104,7 +97,7 @@ export const watchAdd = async ( return watches; }; -export const watchRemove = async (watcher: FSWatcher, folderPath: string) => { +export const watchRemove = (watcher: FSWatcher, folderPath: string) => { const watches = folderWatches(); const filtered = watches.filter((watch) => watch.folderPath != folderPath); if (watches.length == filtered.length) @@ -157,3 +150,7 @@ export const watchFindFiles = async (dirPath: string) => { } return paths; }; + +export const watchReset = (watcher: FSWatcher) => { + watcher.unwatch(folderWatches().map((watch) => watch.folderPath)); +}; diff --git a/desktop/src/main/stores/safe-storage.ts b/desktop/src/main/stores/safe-storage.ts index 1e1369db89f2546b33a6242a12de19521c565af2..040af1f3edb4ec3bf243d58a94d389307281fc29 100644 --- a/desktop/src/main/stores/safe-storage.ts +++ b/desktop/src/main/stores/safe-storage.ts @@ -1,7 +1,7 @@ import Store, { Schema } from "electron-store"; interface SafeStorageStore { - encryptionKey: string; + encryptionKey?: string; } const safeStorageSchema: Schema = { diff --git a/desktop/src/main/stores/upload-status.ts b/desktop/src/main/stores/upload-status.ts index 472f38a7f921e3ac19c9dd295029f0b0c4b33f37..8cb2410df6c1ba697d040a269c1bb8415a9ddecd 100644 --- a/desktop/src/main/stores/upload-status.ts +++ b/desktop/src/main/stores/upload-status.ts @@ -9,15 +9,10 @@ export interface UploadStatusStore { collectionName?: string; /** * Paths to regular files that are pending upload. - * - * This should generally be present, albeit empty, but it is marked optional - * in sympathy with its siblings. */ filePaths?: string[]; /** * Each item is the path to a zip file and the name of an entry within it. - * - * This is marked optional since legacy stores will not have it. */ zipItems?: [zipPath: string, entryName: string][]; /** diff --git a/desktop/src/main/stores/user-preferences.ts b/desktop/src/main/stores/user-preferences.ts index b4a02bc5be966b2e88253fc73f2d7f21e76b1510..f3b1929892554a71217b57ea9a90aaf669d799d8 100644 --- a/desktop/src/main/stores/user-preferences.ts +++ b/desktop/src/main/stores/user-preferences.ts @@ -1,7 +1,7 @@ import Store, { Schema } from "electron-store"; interface UserPreferences { - hideDockIcon: boolean; + hideDockIcon?: boolean; skipAppVersion?: string; muteUpdateNotificationVersion?: string; } diff --git a/desktop/src/main/stores/watch.ts b/desktop/src/main/stores/watch.ts index 7ee383038ad4eb79c5e450e3c190cdd636d0cde1..59032c9acdd84f9936a5782dcda2deeef5919024 100644 --- a/desktop/src/main/stores/watch.ts +++ b/desktop/src/main/stores/watch.ts @@ -3,7 +3,7 @@ import { type FolderWatch } from "../../types/ipc"; import log from "../log"; interface WatchStore { - mappings: FolderWatchWithLegacyFields[]; + mappings?: FolderWatchWithLegacyFields[]; } type FolderWatchWithLegacyFields = FolderWatch & { @@ -54,8 +54,12 @@ export const watchStore = new Store({ */ export const migrateLegacyWatchStoreIfNeeded = () => { let needsUpdate = false; - const watches = watchStore.get("mappings")?.map((watch) => { + const updatedWatches = []; + for (const watch of watchStore.get("mappings") ?? []) { let collectionMapping = watch.collectionMapping; + // The required type defines the latest schema, but before migration + // this'll be undefined, so tell ESLint to calm down. + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (!collectionMapping) { collectionMapping = watch.uploadStrategy == 1 ? "parent" : "root"; needsUpdate = true; @@ -64,10 +68,10 @@ export const migrateLegacyWatchStoreIfNeeded = () => { delete watch.rootFolderName; needsUpdate = true; } - return { ...watch, collectionMapping }; - }); + updatedWatches.push({ ...watch, collectionMapping }); + } if (needsUpdate) { - watchStore.set("mappings", watches); + watchStore.set("mappings", updatedWatches); log.info("Migrated legacy watch store data to new schema"); } }; diff --git a/desktop/src/main/stream.ts b/desktop/src/main/stream.ts index b37970cfaeda2ce6c24f67cabd350b81fd8c7d03..bae13aa121598e95822eb0b401e2443d69fcfd59 100644 --- a/desktop/src/main/stream.ts +++ b/desktop/src/main/stream.ts @@ -6,8 +6,10 @@ import StreamZip from "node-stream-zip"; import { createWriteStream, existsSync } from "node:fs"; import fs from "node:fs/promises"; import { Readable } from "node:stream"; +import { ReadableStream } from "node:stream/web"; import { pathToFileURL } from "node:url"; import log from "./log"; +import { ensure } from "./utils/common"; /** * Register a protocol handler that we use for streaming large files between the @@ -35,25 +37,18 @@ export const registerStreamProtocol = () => { protocol.handle("stream", async (request: Request) => { const url = request.url; // The request URL contains the command to run as the host, and the - // pathname of the file as the path. An additional path can be specified - // as the URL hash. - // - // For example, - // - // stream://write/path/to/file#/path/to/another/file - // host[pathname----] [pathname-2---------] - // - const { host, pathname, hash } = new URL(url); - // Convert e.g. "%20" to spaces. - const path = decodeURIComponent(pathname); - const hashPath = decodeURIComponent(hash); + // pathname of the file(s) as the search params. + const { host, searchParams } = new URL(url); switch (host) { case "read": - return handleRead(path); + return handleRead(ensure(searchParams.get("path"))); case "read-zip": - return handleReadZip(path, hashPath); + return handleReadZip( + ensure(searchParams.get("zipPath")), + ensure(searchParams.get("entryName")), + ); case "write": - return handleWrite(path, request); + return handleWrite(ensure(searchParams.get("path")), request); default: return new Response("", { status: 404 }); } @@ -89,7 +84,7 @@ const handleRead = async (path: string) => { return res; } catch (e) { log.error(`Failed to read stream at ${path}`, e); - return new Response(`Failed to read stream: ${e.message}`, { + return new Response(`Failed to read stream: ${String(e)}`, { status: 500, }); } @@ -99,10 +94,24 @@ const handleReadZip = async (zipPath: string, entryName: string) => { try { const zip = new StreamZip.async({ file: zipPath }); const entry = await zip.entry(entryName); - const stream = await zip.stream(entry); - // TODO(MR): when to call zip.close() + if (!entry) return new Response("", { status: 404 }); - return new Response(Readable.toWeb(new Readable(stream)), { + // This returns an "old style" NodeJS.ReadableStream. + const stream = await zip.stream(entry); + // Convert it into a new style NodeJS.Readable. + const nodeReadable = new Readable().wrap(stream); + // Then convert it into a Web stream. + const webReadableStreamAny = Readable.toWeb(nodeReadable); + // However, we get a ReadableStream now. This doesn't go into the + // `BodyInit` expected by the Response constructor, which wants a + // ReadableStream. Force a cast. + const webReadableStream = + webReadableStreamAny as ReadableStream; + + // Close the zip handle when the underlying stream closes. + stream.on("end", () => void zip.close()); + + return new Response(webReadableStream, { headers: { // We don't know the exact type, but it doesn't really matter, // just set it to a generic binary content-type so that the @@ -122,7 +131,7 @@ const handleReadZip = async (zipPath: string, entryName: string) => { `Failed to read entry ${entryName} from zip file at ${zipPath}`, e, ); - return new Response(`Failed to read stream: ${e.message}`, { + return new Response(`Failed to read stream: ${String(e)}`, { status: 500, }); } @@ -130,11 +139,11 @@ const handleReadZip = async (zipPath: string, entryName: string) => { const handleWrite = async (path: string, request: Request) => { try { - await writeStream(path, request.body); + await writeStream(path, ensure(request.body)); return new Response("", { status: 200 }); } catch (e) { log.error(`Failed to write stream to ${path}`, e); - return new Response(`Failed to write stream: ${e.message}`, { + return new Response(`Failed to write stream: ${String(e)}`, { status: 500, }); } @@ -146,56 +155,29 @@ const handleWrite = async (path: string, request: Request) => { * The returned promise resolves when the write completes. * * @param filePath The local filesystem path where the file should be written. - * @param readableStream A [web - * ReadableStream](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream) - */ -export const writeStream = (filePath: string, readableStream: ReadableStream) => - writeNodeStream(filePath, convertWebReadableStreamToNode(readableStream)); - -/** - * Convert a Web ReadableStream into a Node.js ReadableStream * - * This can be used to, for example, write a ReadableStream obtained via - * `net.fetch` into a file using the Node.js `fs` APIs + * @param readableStream A web + * [ReadableStream](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream). */ -const convertWebReadableStreamToNode = (readableStream: ReadableStream) => { - const reader = readableStream.getReader(); - const rs = new Readable(); - - rs._read = async () => { - try { - const result = await reader.read(); - - if (!result.done) { - rs.push(Buffer.from(result.value)); - } else { - rs.push(null); - return; - } - } catch (e) { - rs.emit("error", e); - } - }; - - return rs; -}; +export const writeStream = (filePath: string, readableStream: ReadableStream) => + writeNodeStream(filePath, Readable.fromWeb(readableStream)); const writeNodeStream = async (filePath: string, fileStream: Readable) => { const writeable = createWriteStream(filePath); - fileStream.on("error", (error) => { - writeable.destroy(error); // Close the writable stream with an error + fileStream.on("error", (err) => { + writeable.destroy(err); // Close the writable stream with an error }); fileStream.pipe(writeable); await new Promise((resolve, reject) => { writeable.on("finish", resolve); - writeable.on("error", async (e: unknown) => { + writeable.on("error", (err) => { if (existsSync(filePath)) { - await fs.unlink(filePath); + void fs.unlink(filePath); } - reject(e); + reject(err); }); }); }; diff --git a/desktop/src/main/utils.ts b/desktop/src/main/utils/common.ts similarity index 67% rename from desktop/src/main/utils.ts rename to desktop/src/main/utils/common.ts index 132859a436a6117d40b56518a96a8c5c8041cdd6..1f5016e617fdd31bb876ee313937b22ae6314c23 100644 --- a/desktop/src/main/utils.ts +++ b/desktop/src/main/utils/common.ts @@ -1,10 +1,19 @@ /** - * @file grab bag of utitity functions. + * @file grab bag of utility functions. * - * Many of these are verbatim copies of functions from web code since there - * isn't currently a common package that both of them share. + * These are verbatim copies of functions from web code since there isn't + * currently a common package that both of them share. */ +/** + * Throw an exception if the given value is `null` or `undefined`. + */ +export const ensure = (v: T | null | undefined): T => { + if (v === null) throw new Error("Required value was null"); + if (v === undefined) throw new Error("Required value was not found"); + return v; +}; + /** * Wait for {@link ms} milliseconds * diff --git a/desktop/src/main/utils-electron.ts b/desktop/src/main/utils/electron.ts similarity index 55% rename from desktop/src/main/utils-electron.ts rename to desktop/src/main/utils/electron.ts index e8a98f1dfe7b0e5a58114a4905cf0c1f72a9ba83..93e8565ef2945a6c98daf10c46979cada7cc80da 100644 --- a/desktop/src/main/utils-electron.ts +++ b/desktop/src/main/utils/electron.ts @@ -1,14 +1,35 @@ import shellescape from "any-shell-escape"; -import { shell } from "electron"; /* TODO(MR): Why is this not in /main? */ import { app } from "electron/main"; import { exec } from "node:child_process"; import path from "node:path"; import { promisify } from "node:util"; -import log from "./log"; +import log from "../log"; /** `true` if the app is running in development mode. */ export const isDev = !app.isPackaged; +/** + * Convert a file system {@link platformPath} that uses the local system + * specific path separators into a path that uses POSIX file separators. + * + * For all paths that we persist or pass over the IPC boundary, we always use + * POSIX paths, even on Windows. + * + * Windows recognizes both forward and backslashes. This also works with drive + * names. c:\foo\bar and c:/foo/bar are both valid. + * + * > Almost all paths passed to Windows APIs are normalized. During + * > normalization, Windows performs the following steps: ... All forward + * > slashes (/) are converted into the standard Windows separator, the back + * > slash (\). + * > + * > https://learn.microsoft.com/en-us/dotnet/standard/io/file-path-formats + */ +export const posixPath = (platformPath: string) => + path.sep == path.posix.sep + ? platformPath + : platformPath.split(path.sep).join(path.posix.sep); + /** * Run a shell command asynchronously. * @@ -41,39 +62,3 @@ export const execAsync = (command: string | string[]) => { }; const execAsync_ = promisify(exec); - -/** - * Open the given {@link dirPath} in the system's folder viewer. - * - * For example, on macOS this'll open {@link dirPath} in Finder. - */ -export const openDirectory = async (dirPath: string) => { - const res = await shell.openPath(path.normalize(dirPath)); - // shell.openPath resolves with a string containing the error message - // corresponding to the failure if a failure occurred, otherwise "". - if (res) throw new Error(`Failed to open directory ${dirPath}: res`); -}; - -/** - * Open the app's log directory in the system's folder viewer. - * - * @see {@link openDirectory} - */ -export const openLogDirectory = () => openDirectory(logDirectoryPath()); - -/** - * Return the path where the logs for the app are saved. - * - * [Note: Electron app paths] - * - * By default, these paths are at the following locations: - * - * - macOS: `~/Library/Application Support/ente` - * - Linux: `~/.config/ente` - * - Windows: `%APPDATA%`, e.g. `C:\Users\\AppData\Local\ente` - * - Windows: C:\Users\\AppData\Local\ - * - * https://www.electronjs.org/docs/latest/api/app - * - */ -const logDirectoryPath = () => app.getPath("logs"); diff --git a/desktop/src/main/utils-temp.ts b/desktop/src/main/utils/temp.ts similarity index 81% rename from desktop/src/main/utils-temp.ts rename to desktop/src/main/utils/temp.ts index 3f3a6081e4b9dc9e2e96efcb96d1dec5aa62cdf5..11f7a5d84531c659b0137f3fa8ba6f6f3681785d 100644 --- a/desktop/src/main/utils-temp.ts +++ b/desktop/src/main/utils/temp.ts @@ -2,8 +2,9 @@ import { app } from "electron/main"; import StreamZip from "node-stream-zip"; import { existsSync } from "node:fs"; import fs from "node:fs/promises"; -import path from "path"; -import type { ZipItem } from "../types/ipc"; +import path from "node:path"; +import type { ZipItem } from "../../types/ipc"; +import { ensure } from "./common"; /** * Our very own directory within the system temp directory. Go crazy, but @@ -17,13 +18,10 @@ const enteTempDirPath = async () => { /** Generate a random string suitable for being used as a file name prefix */ const randomPrefix = () => { - const alphabet = - "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + const ch = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + const randomChar = () => ensure(ch[Math.floor(Math.random() * ch.length)]); - let result = ""; - for (let i = 0; i < 10; i++) - result += alphabet[Math.floor(Math.random() * alphabet.length)]; - return result; + return Array(10).fill("").map(randomChar).join(""); }; /** @@ -76,15 +74,14 @@ interface FileForDataOrPathOrZipItem { */ isFileTemporary: boolean; /** - * If set, this'll be a function that can be called to actually write the - * contents of the source `Uint8Array | string | ZipItem` into the file at - * {@link path}. + * A function that can be called to actually write the contents of the + * source `Uint8Array | string | ZipItem` into the file at {@link path}. * - * It will be undefined if the source is already a path since nothing needs - * to be written in that case. In the other two cases this function will - * write the data or zip item into the file at {@link path}. + * It will do nothing in the case when the source is already a path. In the + * other two cases this function will write the data or zip item into the + * file at {@link path}. */ - writeToTemporaryFile?: () => Promise; + writeToTemporaryFile: () => Promise; } /** @@ -101,7 +98,9 @@ export const makeFileForDataOrPathOrZipItem = async ( ): Promise => { let path: string; let isFileTemporary: boolean; - let writeToTemporaryFile: () => Promise | undefined; + let writeToTemporaryFile = async () => { + /* no-op */ + }; if (typeof dataOrPathOrZipItem == "string") { path = dataOrPathOrZipItem; @@ -117,7 +116,7 @@ export const makeFileForDataOrPathOrZipItem = async ( const [zipPath, entryName] = dataOrPathOrZipItem; const zip = new StreamZip.async({ file: zipPath }); await zip.extract(entryName, path); - zip.close(); + await zip.close(); }; } } diff --git a/desktop/src/preload.ts b/desktop/src/preload.ts index 61955b52405bfa3dafa04921583e6c722c367db2..f9147e28833f7a75d86c62798659620943c7c613 100644 --- a/desktop/src/preload.ts +++ b/desktop/src/preload.ts @@ -44,7 +44,6 @@ import { contextBridge, ipcRenderer, webUtils } from "electron/renderer"; import type { AppUpdate, CollectionMapping, - ElectronFile, FolderWatch, PendingUploads, ZipItem, @@ -52,23 +51,23 @@ import type { // - General -const appVersion = (): Promise => ipcRenderer.invoke("appVersion"); +const appVersion = () => ipcRenderer.invoke("appVersion"); const logToDisk = (message: string): void => ipcRenderer.send("logToDisk", message); -const openDirectory = (dirPath: string): Promise => +const openDirectory = (dirPath: string) => ipcRenderer.invoke("openDirectory", dirPath); -const openLogDirectory = (): Promise => - ipcRenderer.invoke("openLogDirectory"); +const openLogDirectory = () => ipcRenderer.invoke("openLogDirectory"); + +const selectDirectory = () => ipcRenderer.invoke("selectDirectory"); const clearStores = () => ipcRenderer.send("clearStores"); -const encryptionKey = (): Promise => - ipcRenderer.invoke("encryptionKey"); +const encryptionKey = () => ipcRenderer.invoke("encryptionKey"); -const saveEncryptionKey = (encryptionKey: string): Promise => +const saveEncryptionKey = (encryptionKey: string) => ipcRenderer.invoke("saveEncryptionKey", encryptionKey); const onMainWindowFocus = (cb?: () => void) => { @@ -100,39 +99,36 @@ const skipAppUpdate = (version: string) => { // - FS -const fsExists = (path: string): Promise => - ipcRenderer.invoke("fsExists", path); +const fsExists = (path: string) => ipcRenderer.invoke("fsExists", path); -const fsMkdirIfNeeded = (dirPath: string): Promise => +const fsMkdirIfNeeded = (dirPath: string) => ipcRenderer.invoke("fsMkdirIfNeeded", dirPath); -const fsRename = (oldPath: string, newPath: string): Promise => +const fsRename = (oldPath: string, newPath: string) => ipcRenderer.invoke("fsRename", oldPath, newPath); -const fsRmdir = (path: string): Promise => - ipcRenderer.invoke("fsRmdir", path); +const fsRmdir = (path: string) => ipcRenderer.invoke("fsRmdir", path); -const fsRm = (path: string): Promise => ipcRenderer.invoke("fsRm", path); +const fsRm = (path: string) => ipcRenderer.invoke("fsRm", path); -const fsReadTextFile = (path: string): Promise => +const fsReadTextFile = (path: string) => ipcRenderer.invoke("fsReadTextFile", path); -const fsWriteFile = (path: string, contents: string): Promise => +const fsWriteFile = (path: string, contents: string) => ipcRenderer.invoke("fsWriteFile", path, contents); -const fsIsDir = (dirPath: string): Promise => - ipcRenderer.invoke("fsIsDir", dirPath); +const fsIsDir = (dirPath: string) => ipcRenderer.invoke("fsIsDir", dirPath); // - Conversion -const convertToJPEG = (imageData: Uint8Array): Promise => +const convertToJPEG = (imageData: Uint8Array) => ipcRenderer.invoke("convertToJPEG", imageData); const generateImageThumbnail = ( dataOrPathOrZipItem: Uint8Array | string | ZipItem, maxDimension: number, maxSize: number, -): Promise => +) => ipcRenderer.invoke( "generateImageThumbnail", dataOrPathOrZipItem, @@ -145,7 +141,7 @@ const ffmpegExec = ( dataOrPathOrZipItem: Uint8Array | string | ZipItem, outputFileExtension: string, timeoutMS: number, -): Promise => +) => ipcRenderer.invoke( "ffmpegExec", command, @@ -156,62 +152,40 @@ const ffmpegExec = ( // - ML -const clipImageEmbedding = (jpegImageData: Uint8Array): Promise => +const clipImageEmbedding = (jpegImageData: Uint8Array) => ipcRenderer.invoke("clipImageEmbedding", jpegImageData); -const clipTextEmbeddingIfAvailable = ( - text: string, -): Promise => +const clipTextEmbeddingIfAvailable = (text: string) => ipcRenderer.invoke("clipTextEmbeddingIfAvailable", text); -const detectFaces = (input: Float32Array): Promise => +const detectFaces = (input: Float32Array) => ipcRenderer.invoke("detectFaces", input); -const faceEmbedding = (input: Float32Array): Promise => +const faceEmbedding = (input: Float32Array) => ipcRenderer.invoke("faceEmbedding", input); -// - File selection - -// TODO: Deprecated - use dialogs on the renderer process itself - -const selectDirectory = (): Promise => - ipcRenderer.invoke("selectDirectory"); - -const showUploadFilesDialog = (): Promise => - ipcRenderer.invoke("showUploadFilesDialog"); - -const showUploadDirsDialog = (): Promise => - ipcRenderer.invoke("showUploadDirsDialog"); - -const showUploadZipDialog = (): Promise<{ - zipPaths: string[]; - files: ElectronFile[]; -}> => ipcRenderer.invoke("showUploadZipDialog"); +const legacyFaceCrop = (faceID: string) => + ipcRenderer.invoke("legacyFaceCrop", faceID); // - Watch -const watchGet = (): Promise => ipcRenderer.invoke("watchGet"); +const watchGet = () => ipcRenderer.invoke("watchGet"); -const watchAdd = ( - folderPath: string, - collectionMapping: CollectionMapping, -): Promise => +const watchAdd = (folderPath: string, collectionMapping: CollectionMapping) => ipcRenderer.invoke("watchAdd", folderPath, collectionMapping); -const watchRemove = (folderPath: string): Promise => +const watchRemove = (folderPath: string) => ipcRenderer.invoke("watchRemove", folderPath); const watchUpdateSyncedFiles = ( syncedFiles: FolderWatch["syncedFiles"], folderPath: string, -): Promise => - ipcRenderer.invoke("watchUpdateSyncedFiles", syncedFiles, folderPath); +) => ipcRenderer.invoke("watchUpdateSyncedFiles", syncedFiles, folderPath); const watchUpdateIgnoredFiles = ( ignoredFiles: FolderWatch["ignoredFiles"], folderPath: string, -): Promise => - ipcRenderer.invoke("watchUpdateIgnoredFiles", ignoredFiles, folderPath); +) => ipcRenderer.invoke("watchUpdateIgnoredFiles", ignoredFiles, folderPath); const watchOnAddFile = (f: (path: string, watch: FolderWatch) => void) => { ipcRenderer.removeAllListeners("watchAddFile"); @@ -234,34 +208,56 @@ const watchOnRemoveDir = (f: (path: string, watch: FolderWatch) => void) => { ); }; -const watchFindFiles = (folderPath: string): Promise => +const watchFindFiles = (folderPath: string) => ipcRenderer.invoke("watchFindFiles", folderPath); +const watchReset = async () => { + ipcRenderer.removeAllListeners("watchAddFile"); + ipcRenderer.removeAllListeners("watchRemoveFile"); + ipcRenderer.removeAllListeners("watchRemoveDir"); + await ipcRenderer.invoke("watchReset"); +}; + // - Upload -const pathForFile = (file: File) => webUtils.getPathForFile(file); +const pathForFile = (file: File) => { + const path = webUtils.getPathForFile(file); + // The path that we get back from `webUtils.getPathForFile` on Windows uses + // "/" as the path separator. Convert them to POSIX separators. + // + // Note that we do not have access to the path or the os module in the + // preload script, thus this hand rolled transformation. + + // However that makes TypeScript fidgety since we it cannot find navigator, + // as we haven't included "lib": ["dom"] in our tsconfig to avoid making DOM + // APIs available to our main Node.js code. We could create a separate + // tsconfig just for the preload script, but for now let's go with a cast. + // + // @ts-expect-error navigator is not defined. + const platform = (navigator as { platform: string }).platform; + return platform.toLowerCase().includes("win") + ? path.split("\\").join("/") + : path; +}; -const listZipItems = (zipPath: string): Promise => +const listZipItems = (zipPath: string) => ipcRenderer.invoke("listZipItems", zipPath); -const pathOrZipItemSize = (pathOrZipItem: string | ZipItem): Promise => +const pathOrZipItemSize = (pathOrZipItem: string | ZipItem) => ipcRenderer.invoke("pathOrZipItemSize", pathOrZipItem); -const pendingUploads = (): Promise => - ipcRenderer.invoke("pendingUploads"); +const pendingUploads = () => ipcRenderer.invoke("pendingUploads"); -const setPendingUploads = (pendingUploads: PendingUploads): Promise => +const setPendingUploads = (pendingUploads: PendingUploads) => ipcRenderer.invoke("setPendingUploads", pendingUploads); -const markUploadedFiles = (paths: PendingUploads["filePaths"]): Promise => +const markUploadedFiles = (paths: PendingUploads["filePaths"]) => ipcRenderer.invoke("markUploadedFiles", paths); -const markUploadedZipItems = ( - items: PendingUploads["zipItems"], -): Promise => ipcRenderer.invoke("markUploadedZipItems", items); +const markUploadedZipItems = (items: PendingUploads["zipItems"]) => + ipcRenderer.invoke("markUploadedZipItems", items); -const clearPendingUploads = (): Promise => - ipcRenderer.invoke("clearPendingUploads"); +const clearPendingUploads = () => ipcRenderer.invoke("clearPendingUploads"); /** * These objects exposed here will become available to the JS code in our @@ -310,6 +306,7 @@ contextBridge.exposeInMainWorld("electron", { logToDisk, openDirectory, openLogDirectory, + selectDirectory, clearStores, encryptionKey, saveEncryptionKey, @@ -347,13 +344,7 @@ contextBridge.exposeInMainWorld("electron", { clipTextEmbeddingIfAvailable, detectFaces, faceEmbedding, - - // - File selection - - selectDirectory, - showUploadFilesDialog, - showUploadDirsDialog, - showUploadZipDialog, + legacyFaceCrop, // - Watch @@ -361,12 +352,13 @@ contextBridge.exposeInMainWorld("electron", { get: watchGet, add: watchAdd, remove: watchRemove, + updateSyncedFiles: watchUpdateSyncedFiles, + updateIgnoredFiles: watchUpdateIgnoredFiles, onAddFile: watchOnAddFile, onRemoveFile: watchOnRemoveFile, onRemoveDir: watchOnRemoveDir, findFiles: watchFindFiles, - updateSyncedFiles: watchUpdateSyncedFiles, - updateIgnoredFiles: watchUpdateIgnoredFiles, + reset: watchReset, }, // - Upload diff --git a/desktop/src/thirdparty/clip-bpe-ts/mod.ts b/desktop/src/thirdparty/clip-bpe-ts/mod.ts index 6cdf246f75c4bd7dc5bc176718f5956c575d4069..4d00eef0e4fd69fef354164d4c4c990a1f6ac717 100644 --- a/desktop/src/thirdparty/clip-bpe-ts/mod.ts +++ b/desktop/src/thirdparty/clip-bpe-ts/mod.ts @@ -1,3 +1,5 @@ +/* eslint-disable */ + import * as htmlEntities from "html-entities"; import bpeVocabData from "./bpe_simple_vocab_16e6"; // import ftfy from "https://deno.land/x/ftfy_pyodide@v0.1.1/mod.js"; @@ -410,6 +412,7 @@ export default class { newWord.push(first + second); i += 2; } else { + // @ts-expect-error "Array indexing can return undefined but not modifying thirdparty code" newWord.push(word[i]); i += 1; } @@ -434,6 +437,7 @@ export default class { .map((b) => this.byteEncoder[b.charCodeAt(0) as number]) .join(""); bpeTokens.push( + // @ts-expect-error "Array indexing can return undefined but not modifying thirdparty code" ...this.bpe(token) .split(" ") .map((bpeToken: string) => this.encoder[bpeToken]), @@ -458,6 +462,7 @@ export default class { .join(""); text = [...text] .map((c) => this.byteDecoder[c]) + // @ts-expect-error "Array indexing can return undefined but not modifying thirdparty code" .map((v) => String.fromCharCode(v)) .join("") .replace(/<\/w>/g, " "); diff --git a/desktop/src/types/ipc.ts b/desktop/src/types/ipc.ts index 6e47b7a3a6094ae69f1eed27be699688e83e9b27..f4985bfc714f0bc315ccfa6eec087307049f06c6 100644 --- a/desktop/src/types/ipc.ts +++ b/desktop/src/types/ipc.ts @@ -28,7 +28,7 @@ export interface FolderWatchSyncedFile { export type ZipItem = [zipPath: string, entryName: string]; export interface PendingUploads { - collectionName: string; + collectionName: string | undefined; filePaths: string[]; zipItems: ZipItem[]; } @@ -42,25 +42,3 @@ export interface PendingUploads { export const CustomErrorMessage = { NotAvailable: "This feature in not available on the current OS/arch", }; - -/** - * Deprecated - Use File + webUtils.getPathForFile instead - * - * Electron used to augment the standard web - * [File](https://developer.mozilla.org/en-US/docs/Web/API/File) object with an - * additional `path` property. This is now deprecated, and will be removed in a - * future release. - * https://www.electronjs.org/docs/latest/api/file-object - * - * The alternative to the `path` property is to use `webUtils.getPathForFile` - * https://www.electronjs.org/docs/latest/api/web-utils - */ -export interface ElectronFile { - name: string; - path: string; - size: number; - lastModified: number; - stream: () => Promise>; - blob: () => Promise; - arrayBuffer: () => Promise; -} diff --git a/desktop/tsconfig.json b/desktop/tsconfig.json index 700ea3fa00d06c8fc5cf11bd8a23783172053e8b..7806cd93a7ef183f7ce02e716fc051cdea938f12 100644 --- a/desktop/tsconfig.json +++ b/desktop/tsconfig.json @@ -3,71 +3,34 @@ into JavaScript that'll then be loaded and run by the main (node) process of our Electron app. */ - /* TSConfig docs: https://aka.ms/tsconfig.json */ + /* + * Recommended target, lib and other settings for code running in the + * version of Node.js bundled with Electron. + * + * Currently, with Electron 30, this is Node.js 20.11.1. + * https://www.electronjs.org/blog/electron-30-0 + */ + "extends": "@tsconfig/node20/tsconfig.json", + /* TSConfig docs: https://aka.ms/tsconfig.json */ "compilerOptions": { - /* Recommended target, lib and other settings for code running in the - version of Node.js bundled with Electron. - - Currently, with Electron 29, this is Node.js 20.9 - https://www.electronjs.org/blog/electron-29-0 - - Note that we cannot do - - "extends": "@tsconfig/node20/tsconfig.json", - - because that sets "lib": ["es2023"]. However (and I don't fully - understand what's going on here), that breaks our compilation since - tsc can then not find type definitions of things like ReadableStream. - - Adding "dom" to "lib" (e.g. `"lib": ["es2023", "dom"]`) fixes the - issue, but that doesn't sound correct - the main Electron process - isn't running in a browser context. - - It is possible that we're using some of the types incorrectly. For - now, we just omit the "lib" definition and rely on the defaults for - the "target" we've chosen. This is also what the current - electron-forge starter does: - - yarn create electron-app electron-forge-starter -- --template=webpack-typescript - - Enhancement: Can revisit this later. - - Refs: - - https://github.com/electron/electron/issues/27092 - - https://github.com/electron/electron/issues/16146 - */ - - "target": "es2022", - "module": "node16", - - /* Enable various workarounds to play better with CJS libraries */ - "esModuleInterop": true, - /* Speed things up by not type checking `node_modules` */ - "skipLibCheck": true, - /* Emit the generated JS into `app/` */ "outDir": "app", - /* Temporary overrides to get things to compile with the older config */ - "strict": false, - "noImplicitAny": true - - /* Below is the state we want */ - /* Enable these one by one */ - // "strict": true, - /* Require the `type` modifier when importing types */ - // "verbatimModuleSyntax": true + /* We want this, but it causes "ESM syntax is not allowed in a CommonJS + module when 'verbatimModuleSyntax' is enabled" currently */ + /* "verbatimModuleSyntax": true, */ + "strict": true, /* Stricter than strict */ - // "noImplicitReturns": true, - // "noUnusedParameters": true, - // "noUnusedLocals": true, - // "noFallthroughCasesInSwitch": true, + "noImplicitReturns": true, + "noUnusedParameters": true, + "noUnusedLocals": true, + "noFallthroughCasesInSwitch": true, /* e.g. makes array indexing returns undefined */ - // "noUncheckedIndexedAccess": true, - // "exactOptionalPropertyTypes": true, + "noUncheckedIndexedAccess": true, + "exactOptionalPropertyTypes": true }, /* Transpile all `.ts` files in `src/` */ "include": ["src/**/*.ts"] diff --git a/desktop/yarn.lock b/desktop/yarn.lock index a5b86f1eb3ecd7c042bed41f8d7a1c30471f32c5..eee3c4b3acf857e8c6c4286da8e6006b0cbbb56e 100644 --- a/desktop/yarn.lock +++ b/desktop/yarn.lock @@ -7,38 +7,10 @@ resolved "https://registry.yarnpkg.com/7zip-bin/-/7zip-bin-5.2.0.tgz#7a03314684dd6572b7dfa89e68ce31d60286854d" integrity sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A== -"@aashutoshrathi/word-wrap@^1.2.3": - version "1.2.6" - resolved "https://registry.yarnpkg.com/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz#bd9154aec9983f77b3a034ecaa015c2e4201f6cf" - integrity sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA== - -"@babel/code-frame@^7.0.0": - version "7.24.2" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.24.2.tgz#718b4b19841809a58b29b68cde80bc5e1aa6d9ae" - integrity sha512-y5+tLQyV8pg3fsiln67BVLD1P13Eg4lh5RW9mF0zUuvLrv9uIQ4MCL+CRT+FTsBlBjcIan6PGsLcBN0m3ClUyQ== - dependencies: - "@babel/highlight" "^7.24.2" - picocolors "^1.0.0" - -"@babel/helper-validator-identifier@^7.22.20": - version "7.22.20" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz#c4ae002c61d2879e724581d96665583dbc1dc0e0" - integrity sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A== - -"@babel/highlight@^7.24.2": - version "7.24.2" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.24.2.tgz#3f539503efc83d3c59080a10e6634306e0370d26" - integrity sha512-Yac1ao4flkTxTteCDZLEvdxg2fZfz1v8M4QpaGypq/WPDqg3ijHYbDfs+LG5hvzSoqaSZ9/Z9lKSP3CjZjv+pA== - dependencies: - "@babel/helper-validator-identifier" "^7.22.20" - chalk "^2.4.2" - js-tokens "^4.0.0" - picocolors "^1.0.0" - "@babel/runtime@^7.21.0": - version "7.24.0" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.0.tgz#584c450063ffda59697021430cb47101b085951e" - integrity sha512-Chk32uHMg6TnQdvw2e9IlqPpFX/6NLuK0Ys2PqLb7/gL5uFn9mXvK715FGLlOLQrcO4qIkNHkvPGktzzXexsFw== + version "7.24.5" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.5.tgz#230946857c053a36ccc66e1dd03b17dd0c4ed02c" + integrity sha512-Nms86NXrsaeU9vbBJKni6gXiEXZ4CVpYVzEjDH9Sb8vmZ3UljyA1GSOJl/6LGPO8EHLuSF9H+IxNXHPX8QHJ4g== dependencies: regenerator-runtime "^0.14.0" @@ -60,10 +32,10 @@ ajv "^6.12.0" ajv-keywords "^3.4.1" -"@electron/asar@^3.2.1": - version "3.2.9" - resolved "https://registry.yarnpkg.com/@electron/asar/-/asar-3.2.9.tgz#7b3a1fd677b485629f334dd80ced8c85353ba7e7" - integrity sha512-Vu2P3X2gcZ3MY9W7yH72X9+AMXwUQZEJBrsPIbX0JsdllLtoh62/Q8Wg370/DawIEVKOyfD6KtTLo645ezqxUA== +"@electron/asar@^3.2.7": + version "3.2.10" + resolved "https://registry.yarnpkg.com/@electron/asar/-/asar-3.2.10.tgz#615cf346b734b23cafa4e0603551010bd0e50aa8" + integrity sha512-mvBSwIBUeiRscrCeJE1LwctAriBj65eUDm0Pc11iE5gRwzkmsdbS7FnZ1XUWjpSeQWL1L5g12Fc/SchPM9DUOw== dependencies: commander "^5.0.0" glob "^7.1.6" @@ -84,10 +56,10 @@ optionalDependencies: global-agent "^3.0.0" -"@electron/notarize@2.2.1": - version "2.2.1" - resolved "https://registry.yarnpkg.com/@electron/notarize/-/notarize-2.2.1.tgz#d0aa6bc43cba830c41bfd840b85dbe0e273f59fe" - integrity sha512-aL+bFMIkpR0cmmj5Zgy0LMKEpgy43/hw5zadEArgmAMWWlKc5buwFvFT9G/o/YJkvXAJm5q3iuTuLaiaXW39sg== +"@electron/notarize@2.3.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@electron/notarize/-/notarize-2.3.0.tgz#9659cf6c92563dd69411afce229f52f9f7196227" + integrity sha512-EiTBU0BwE7HZZjAG1fFWQaiQpCuPrVGn7jPss1kUjD6eTTdXXd29RiZqEqkgN7xqt/Pgn4g3I7Saqovanrfj3w== dependencies: debug "^4.1.1" fs-extra "^9.0.1" @@ -105,18 +77,38 @@ minimist "^1.2.6" plist "^3.0.5" -"@electron/universal@1.5.1": - version "1.5.1" - resolved "https://registry.yarnpkg.com/@electron/universal/-/universal-1.5.1.tgz#f338bc5bcefef88573cf0ab1d5920fac10d06ee5" - integrity sha512-kbgXxyEauPJiQQUNG2VgUeyfQNFk6hBF11ISN2PNI6agUgPl55pv4eQmaqHzTAzchBvqZ2tQuRVaPStGf0mxGw== +"@electron/rebuild@3.6.0": + version "3.6.0" + resolved "https://registry.yarnpkg.com/@electron/rebuild/-/rebuild-3.6.0.tgz#60211375a5f8541a71eb07dd2f97354ad0b2b96f" + integrity sha512-zF4x3QupRU3uNGaP5X1wjpmcjfw1H87kyqZ00Tc3HvriV+4gmOGuvQjGNkrJuXdsApssdNyVwLsy+TaeTGGcVw== dependencies: - "@electron/asar" "^3.2.1" - "@malept/cross-spawn-promise" "^1.1.0" + "@malept/cross-spawn-promise" "^2.0.0" + chalk "^4.0.0" + debug "^4.1.1" + detect-libc "^2.0.1" + fs-extra "^10.0.0" + got "^11.7.0" + node-abi "^3.45.0" + node-api-version "^0.2.0" + node-gyp "^9.0.0" + ora "^5.1.0" + read-binary-file-arch "^1.0.6" + semver "^7.3.5" + tar "^6.0.5" + yargs "^17.0.1" + +"@electron/universal@2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@electron/universal/-/universal-2.0.1.tgz#7b070ab355e02957388f3dbd68e2c3cd08c448ae" + integrity sha512-fKpv9kg4SPmt+hY7SVBnIYULE9QJl8L3sCfcBsnqbJwwBwAeTLokJ9TRt9y7bK0JAzIW2y78TVVjvnQEms/yyA== + dependencies: + "@electron/asar" "^3.2.7" + "@malept/cross-spawn-promise" "^2.0.0" debug "^4.3.1" - dir-compare "^3.0.0" - fs-extra "^9.0.1" - minimatch "^3.0.4" - plist "^3.0.4" + dir-compare "^4.2.0" + fs-extra "^11.1.1" + minimatch "^9.0.3" + plist "^3.1.0" "@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0": version "4.4.0" @@ -150,6 +142,11 @@ resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.57.0.tgz#a5417ae8427873f1dd08b70b3574b453e67b5f7f" integrity sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g== +"@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" @@ -165,26 +162,21 @@ integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== "@humanwhocodes/object-schema@^2.0.2": - version "2.0.2" - resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz#d9fae00a2d5cb40f92cfe64b47ad749fbc38f917" - integrity sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw== - -"@isaacs/cliui@^8.0.2": - version "8.0.2" - resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550" - integrity sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA== - dependencies: - string-width "^5.1.2" - string-width-cjs "npm:string-width@^4.2.0" - strip-ansi "^7.0.1" - strip-ansi-cjs "npm:strip-ansi@^6.0.1" - wrap-ansi "^8.1.0" - wrap-ansi-cjs "npm:wrap-ansi@^7.0.0" - -"@malept/cross-spawn-promise@^1.1.0": - version "1.1.1" - resolved "https://registry.yarnpkg.com/@malept/cross-spawn-promise/-/cross-spawn-promise-1.1.1.tgz#504af200af6b98e198bce768bc1730c6936ae01d" - integrity sha512-RTBGWL5FWQcg9orDOCcp4LvItNzUPcyEU9bwaeJX0rJ1IQxzucC48Y0/sQLp/g6t99IQgAlGIaesJS+gTn7tVQ== + 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== + +"@isaacs/fs-minipass@^4.0.0": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz#2d59ae3ab4b38fb4270bfa23d30f8e2e86c7fe32" + integrity sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w== + dependencies: + minipass "^7.0.4" + +"@malept/cross-spawn-promise@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@malept/cross-spawn-promise/-/cross-spawn-promise-2.0.0.tgz#d0772de1aa680a0bfb9ba2f32b4c828c7857cb9d" + integrity sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg== dependencies: cross-spawn "^7.0.1" @@ -219,6 +211,22 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" +"@npmcli/fs@^2.1.0": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@npmcli/fs/-/fs-2.1.2.tgz#a9e2541a4a2fec2e69c29b35e6060973da79b865" + integrity sha512-yOJKRvohFOaLqipNtwYB9WugyZKhC/DZC4VYPmpaCzDBrA8YpK3qHZ8/HGscMnE4GqbkLNuVcCnxkeQEdGt6LQ== + dependencies: + "@gar/promisify" "^1.1.3" + semver "^7.3.5" + +"@npmcli/move-file@^2.0.0": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@npmcli/move-file/-/move-file-2.0.1.tgz#26f6bdc379d87f75e55739bab89db525b06100e4" + integrity sha512-mJd2Z5TjYWq/ttPLLGqArdtnC74J6bOzg4rMDnN+p1xTacZ2yPRCk2y0oSWQtygLR9YVQXgOcONrwtnk3JupxQ== + dependencies: + mkdirp "^1.0.4" + rimraf "^3.0.2" + "@pkgjs/parseargs@^0.11.0": version "0.11.0" resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" @@ -246,6 +254,11 @@ resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf" integrity sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A== +"@tsconfig/node20@^20.1.4": + version "20.1.4" + resolved "https://registry.yarnpkg.com/@tsconfig/node20/-/node20-20.1.4.tgz#3457d42eddf12d3bde3976186ab0cd22b85df928" + integrity sha512-sqgsT69YFeLWf5NtJ4Xq/xAF8p4ZQHlmGW74Nu2tD4+g5fAsposc4ZfaaPixVu4y01BEiDCWLRDCvDM5JOsRxg== + "@types/auto-launch@^5.0": version "5.0.5" resolved "https://registry.yarnpkg.com/@types/auto-launch/-/auto-launch-5.0.5.tgz#439ed36aaaea501e2e2cfbddd8a20c366c34863b" @@ -314,11 +327,6 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.60.tgz#35f3d6213daed95da7f0f73e75bcc6980e90597b" integrity sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw== -"@types/normalize-package-data@^2.4.0": - version "2.4.4" - resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz#56e2cc26c397c038fab0e3a917a12d5c5909e901" - integrity sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA== - "@types/plist@^3.0.1": version "3.0.5" resolved "https://registry.yarnpkg.com/@types/plist/-/plist-3.0.5.tgz#9a0c49c0f9886c8c8696a7904dd703f6284036e0" @@ -352,15 +360,15 @@ "@types/node" "*" "@typescript-eslint/eslint-plugin@^7": - version "7.6.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.6.0.tgz#1f5df5cda490a0bcb6fbdd3382e19f1241024242" - integrity sha512-gKmTNwZnblUdnTIJu3e9kmeRRzV2j1a/LUO27KNNAnIC5zjy1aSvXSRp4rVNlmAoHlQ7HzX42NbKpcSr4jF80A== + 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== dependencies: "@eslint-community/regexpp" "^4.10.0" - "@typescript-eslint/scope-manager" "7.6.0" - "@typescript-eslint/type-utils" "7.6.0" - "@typescript-eslint/utils" "7.6.0" - "@typescript-eslint/visitor-keys" "7.6.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" debug "^4.3.4" graphemer "^1.4.0" ignore "^5.3.1" @@ -369,46 +377,46 @@ ts-api-utils "^1.3.0" "@typescript-eslint/parser@^7": - version "7.6.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-7.6.0.tgz#0aca5de3045d68b36e88903d15addaf13d040a95" - integrity sha512-usPMPHcwX3ZoPWnBnhhorc14NJw9J4HpSXQX4urF2TPKG0au0XhJoZyX62fmvdHONUkmyUe74Hzm1//XA+BoYg== - dependencies: - "@typescript-eslint/scope-manager" "7.6.0" - "@typescript-eslint/types" "7.6.0" - "@typescript-eslint/typescript-estree" "7.6.0" - "@typescript-eslint/visitor-keys" "7.6.0" + version "7.8.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-7.8.0.tgz#1e1db30c8ab832caffee5f37e677dbcb9357ddc8" + integrity sha512-KgKQly1pv0l4ltcftP59uQZCi4HUYswCLbTqVZEJu7uLX8CTLyswqMLqLN+2QFz4jCptqWVV4SB7vdxcH2+0kQ== + 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" debug "^4.3.4" -"@typescript-eslint/scope-manager@7.6.0": - version "7.6.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-7.6.0.tgz#1e9972f654210bd7500b31feadb61a233f5b5e9d" - integrity sha512-ngttyfExA5PsHSx0rdFgnADMYQi+Zkeiv4/ZxGYUWd0nLs63Ha0ksmp8VMxAIC0wtCFxMos7Lt3PszJssG/E6w== +"@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== dependencies: - "@typescript-eslint/types" "7.6.0" - "@typescript-eslint/visitor-keys" "7.6.0" + "@typescript-eslint/types" "7.8.0" + "@typescript-eslint/visitor-keys" "7.8.0" -"@typescript-eslint/type-utils@7.6.0": - version "7.6.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-7.6.0.tgz#644f75075f379827d25fe0713e252ccd4e4a428c" - integrity sha512-NxAfqAPNLG6LTmy7uZgpK8KcuiS2NZD/HlThPXQRGwz6u7MDBWRVliEEl1Gj6U7++kVJTpehkhZzCJLMK66Scw== +"@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== dependencies: - "@typescript-eslint/typescript-estree" "7.6.0" - "@typescript-eslint/utils" "7.6.0" + "@typescript-eslint/typescript-estree" "7.8.0" + "@typescript-eslint/utils" "7.8.0" debug "^4.3.4" ts-api-utils "^1.3.0" -"@typescript-eslint/types@7.6.0": - version "7.6.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-7.6.0.tgz#53dba7c30c87e5f10a731054266dd905f1fbae38" - integrity sha512-h02rYQn8J+MureCvHVVzhl69/GAfQGPQZmOMjG1KfCl7o3HtMSlPaPUAPu6lLctXI5ySRGIYk94clD/AUMCUgQ== +"@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/typescript-estree@7.6.0": - version "7.6.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-7.6.0.tgz#112a3775563799fd3f011890ac8322f80830ac17" - integrity sha512-+7Y/GP9VuYibecrCQWSKgl3GvUM5cILRttpWtnAu8GNL9j11e4tbuGZmZjJ8ejnKYyBRb2ddGQ3rEFCq3QjMJw== +"@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== dependencies: - "@typescript-eslint/types" "7.6.0" - "@typescript-eslint/visitor-keys" "7.6.0" + "@typescript-eslint/types" "7.8.0" + "@typescript-eslint/visitor-keys" "7.8.0" debug "^4.3.4" globby "^11.1.0" is-glob "^4.0.3" @@ -416,25 +424,25 @@ semver "^7.6.0" ts-api-utils "^1.3.0" -"@typescript-eslint/utils@7.6.0": - version "7.6.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-7.6.0.tgz#e400d782280b6f724c8a1204269d984c79202282" - integrity sha512-x54gaSsRRI+Nwz59TXpCsr6harB98qjXYzsRxGqvA5Ue3kQH+FxS7FYU81g/omn22ML2pZJkisy6Q+ElK8pBCA== +"@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== dependencies: "@eslint-community/eslint-utils" "^4.4.0" "@types/json-schema" "^7.0.15" "@types/semver" "^7.5.8" - "@typescript-eslint/scope-manager" "7.6.0" - "@typescript-eslint/types" "7.6.0" - "@typescript-eslint/typescript-estree" "7.6.0" + "@typescript-eslint/scope-manager" "7.8.0" + "@typescript-eslint/types" "7.8.0" + "@typescript-eslint/typescript-estree" "7.8.0" semver "^7.6.0" -"@typescript-eslint/visitor-keys@7.6.0": - version "7.6.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-7.6.0.tgz#d1ce13145844379021e1f9bd102c1d78946f4e76" - integrity sha512-4eLB7t+LlNUmXzfOu1VAIAdkjbu5xNSerURS9X/S5TUKWFRpXRQZbmtPqgKmYx8bj3J0irtQXSiWAOY82v+cgw== +"@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== dependencies: - "@typescript-eslint/types" "7.6.0" + "@typescript-eslint/types" "7.8.0" eslint-visitor-keys "^3.4.3" "@ungap/structured-clone@^1.2.0": @@ -447,6 +455,11 @@ resolved "https://registry.yarnpkg.com/@xmldom/xmldom/-/xmldom-0.8.10.tgz#a1337ca426aa61cef9fe15b5b28e340a72f6fa99" integrity sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw== +abbrev@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" + integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== + acorn-jsx@^5.3.2: version "5.3.2" resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" @@ -457,13 +470,28 @@ acorn@^8.9.0: resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.3.tgz#71e0b14e13a4ec160724b38fb7b0f233b1b81d7a" integrity sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg== -agent-base@6: +agent-base@6, agent-base@^6.0.2: version "6.0.2" resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== dependencies: debug "4" +agentkeepalive@^4.2.1: + version "4.5.0" + resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-4.5.0.tgz#2673ad1389b3c418c5a20c5d7364f93ca04be923" + integrity sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew== + dependencies: + humanize-ms "^1.2.1" + +aggregate-error@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a" + integrity sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA== + dependencies: + clean-stack "^2.0.0" + indent-string "^4.0.0" + ajv-formats@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/ajv-formats/-/ajv-formats-2.1.1.tgz#6e669400659eb74973bbf2e33327180a0996b520" @@ -487,32 +515,20 @@ ajv@^6.10.0, ajv@^6.12.0, ajv@^6.12.4: uri-js "^4.2.2" ajv@^8.0.0, ajv@^8.6.3: - version "8.12.0" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.12.0.tgz#d1a0527323e22f53562c567c00991577dfbe19d1" - integrity sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA== + version "8.13.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.13.0.tgz#a3939eaec9fb80d217ddf0c3376948c023f28c91" + integrity sha512-PRA911Blj99jR5RMeTunVbNXMF6Lp4vZXnk5GQjcnUWUTsrXtekg/pnmFFI2u/I36Y/2bITGS30GZCXei6uNkA== dependencies: - fast-deep-equal "^3.1.1" + fast-deep-equal "^3.1.3" json-schema-traverse "^1.0.0" require-from-string "^2.0.2" - uri-js "^4.2.2" + uri-js "^4.4.1" 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-regex@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.0.1.tgz#3183e38fae9a65d7cb5e53945cd5897d0260a06a" - integrity sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA== - -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.0.0, ansi-styles@^4.1.0: version "4.3.0" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" @@ -520,11 +536,6 @@ ansi-styles@^4.0.0, ansi-styles@^4.1.0: dependencies: color-convert "^2.0.1" -ansi-styles@^6.1.0: - version "6.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5" - integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== - any-shell-escape@^0.1: version "0.1.1" resolved "https://registry.yarnpkg.com/any-shell-escape/-/any-shell-escape-0.1.1.tgz#d55ab972244c71a9a5e1ab0879f30bf110806959" @@ -543,25 +554,26 @@ 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@24.13.3: - version "24.13.3" - resolved "https://registry.yarnpkg.com/app-builder-lib/-/app-builder-lib-24.13.3.tgz#36e47b65fecb8780bb73bff0fee4e0480c28274b" - integrity sha512-FAzX6IBit2POXYGnTCT8YHFO/lr5AapAII6zzhQO3Rw4cEDOgK+t1xhLc5tNcKlicTHlo9zxIwnYCX9X2DLkig== +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== dependencies: "@develar/schema-utils" "~2.6.5" - "@electron/notarize" "2.2.1" + "@electron/notarize" "2.3.0" "@electron/osx-sign" "1.0.5" - "@electron/universal" "1.5.1" + "@electron/rebuild" "3.6.0" + "@electron/universal" "2.0.1" "@malept/flatpak-bundler" "^0.4.0" "@types/fs-extra" "9.0.13" async-exit-hook "^2.0.1" bluebird-lst "^1.0.9" - builder-util "24.13.1" - builder-util-runtime "9.2.4" + builder-util "25.0.0-alpha.6" + builder-util-runtime "9.2.5-alpha.2" chromium-pickle-js "^0.2.0" debug "^4.3.4" ejs "^3.1.8" - electron-publish "24.13.1" + electron-publish "25.0.0-alpha.6" form-data "^4.0.0" fs-extra "^10.1.0" hosted-git-info "^4.1.0" @@ -581,12 +593,18 @@ applescript@^1.0.0: resolved "https://registry.yarnpkg.com/applescript/-/applescript-1.0.0.tgz#bb87af568cad034a4e48c4bdaf6067a3a2701317" integrity sha512-yvtNHdWvtbYEiIazXAdp/NY+BBb65/DAseqlNiJQjOx9DynuzOYDbVLBJvuc0ve0VL9x6B3OHF6eH52y9hCBtQ== -argparse@^1.0.7: - version "1.0.10" - resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" - integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== +"aproba@^1.0.3 || ^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/aproba/-/aproba-2.0.0.tgz#52520b8ae5b569215b354efc0caa3fe1e45a8adc" + integrity sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ== + +are-we-there-yet@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz#679df222b278c64f2cdba1175cdc00b0d96164bd" + integrity sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg== dependencies: - sprintf-js "~1.0.2" + delegates "^1.0.0" + readable-stream "^3.6.0" argparse@^2.0.1: version "2.0.1" @@ -659,6 +677,15 @@ binary-extensions@^2.0.0: resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522" integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw== +bl@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a" + integrity sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w== + dependencies: + buffer "^5.5.0" + inherits "^2.0.4" + readable-stream "^3.4.0" + bluebird-lst@^1.0.9: version "1.0.9" resolved "https://registry.yarnpkg.com/bluebird-lst/-/bluebird-lst-1.0.9.tgz#a64a0e4365658b9ab5fe875eb9dfb694189bb41c" @@ -703,17 +730,12 @@ buffer-crc32@~0.2.3: resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" integrity sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ== -buffer-equal@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/buffer-equal/-/buffer-equal-1.0.1.tgz#2f7651be5b1b3f057fcd6e7ee16cf34767077d90" - integrity sha512-QoV3ptgEaQpvVwbXdSO39iqPQTCxSF7A5U99AxbHYqUdCizL/lH2Z0A2y6nbZucxMEOtNyZfG2s6gsVugGpKkg== - buffer-from@^1.0.0: version "1.1.2" resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== -buffer@^5.1.0: +buffer@^5.1.0, buffer@^5.5.0: version "5.7.1" resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== @@ -729,24 +751,24 @@ builder-util-runtime@9.2.3: debug "^4.3.4" sax "^1.2.4" -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== +builder-util-runtime@9.2.5-alpha.2: + version "9.2.5-alpha.2" + resolved "https://registry.yarnpkg.com/builder-util-runtime/-/builder-util-runtime-9.2.5-alpha.2.tgz#b0a1737996717d7ae0b71e5efdf0bfbd1dd2c21e" + integrity sha512-/Ln2ddejGj2HNMJ+X66mKHRcOvmRzUO/dSi8t4hSV64J7IA+DE+mqDb+zogIE2gin7p7YwcGiOkKny4nwPPPXg== dependencies: debug "^4.3.4" sax "^1.2.4" -builder-util@24.13.1: - version "24.13.1" - resolved "https://registry.yarnpkg.com/builder-util/-/builder-util-24.13.1.tgz#4a4c4f9466b016b85c6990a0ea15aa14edec6816" - integrity sha512-NhbCSIntruNDTOVI9fdXz0dihaqX2YuE1D6zZMrwiErzH4ELZHE6mdiB40wEgZNprDia+FghRFgKoAqMZRRjSA== +builder-util@25.0.0-alpha.6: + version "25.0.0-alpha.6" + resolved "https://registry.yarnpkg.com/builder-util/-/builder-util-25.0.0-alpha.6.tgz#4ac5e13d9e6c750987efc9cd9c1eace58622a30b" + integrity sha512-ghT1XcP6JI926AArlBcPHRRKYCsVWbT/ywnXPwW5X1ani2jmnddPpnwm92xRvCPWGBmeXd2diF69FV5rBJxhRQ== dependencies: "7zip-bin" "~5.2.0" "@types/debug" "^4.1.6" app-builder-bin "4.0.0" bluebird-lst "^1.0.9" - builder-util-runtime "9.2.4" + builder-util-runtime "9.2.5-alpha.2" chalk "^4.1.2" cross-spawn "^7.0.3" debug "^4.3.4" @@ -759,6 +781,30 @@ builder-util@24.13.1: stat-mode "^1.0.0" temp-file "^3.4.0" +cacache@^16.1.0: + version "16.1.3" + resolved "https://registry.yarnpkg.com/cacache/-/cacache-16.1.3.tgz#a02b9f34ecfaf9a78c9f4bc16fceb94d5d67a38e" + integrity sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ== + dependencies: + "@npmcli/fs" "^2.1.0" + "@npmcli/move-file" "^2.0.0" + chownr "^2.0.0" + fs-minipass "^2.1.0" + glob "^8.0.1" + infer-owner "^1.0.4" + lru-cache "^7.7.1" + minipass "^3.1.6" + minipass-collect "^1.0.2" + minipass-flush "^1.0.5" + minipass-pipeline "^1.2.4" + mkdirp "^1.0.4" + p-map "^4.0.0" + promise-inflight "^1.0.1" + rimraf "^3.0.2" + ssri "^9.0.0" + tar "^6.1.11" + unique-filename "^2.0.0" + cacheable-lookup@^5.0.3: version "5.0.4" resolved "https://registry.yarnpkg.com/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz#5a6b865b2c44357be3d5ebc2a467b032719a7005" @@ -787,16 +833,7 @@ caseless@^0.12.0: resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" integrity sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw== -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, chalk@^4.0.2, chalk@^4.1.2: +chalk@^4.0.0, chalk@^4.0.2, chalk@^4.1.0, chalk@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== @@ -824,6 +861,11 @@ chownr@^2.0.0: resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece" integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ== +chownr@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-3.0.0.tgz#9855e64ecd240a9cc4267ce8a4aa5d24a1da15e4" + integrity sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g== + chromium-pickle-js@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/chromium-pickle-js/-/chromium-pickle-js-0.2.0.tgz#04a106672c18b085ab774d983dfa3ea138f22205" @@ -834,6 +876,23 @@ ci-info@^3.2.0: resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.9.0.tgz#4279a62028a7b1f262f3473fc9605f5e218c59b4" integrity sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ== +clean-stack@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" + integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== + +cli-cursor@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307" + integrity sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw== + dependencies: + restore-cursor "^3.1.0" + +cli-spinners@^2.5.0: + version "2.9.2" + resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.9.2.tgz#1773a8f4b9c4d6ac31563df53b3fc1d79462fe41" + integrity sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg== + cli-truncate@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/cli-truncate/-/cli-truncate-2.1.0.tgz#c39e28bf05edcde5be3b98992a22deed5a2b93c7" @@ -858,12 +917,10 @@ clone-response@^1.0.2: dependencies: mimic-response "^1.0.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" +clone@^1.0.2: + version "1.0.4" + resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" + integrity sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg== color-convert@^2.0.1: version "2.0.1" @@ -872,16 +929,16 @@ color-convert@^2.0.1: 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== +color-support@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2" + integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg== + combined-stream@^1.0.8: version "1.0.8" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" @@ -958,6 +1015,11 @@ config-file-ts@^0.2.4: glob "^10.3.10" typescript "^5.3.3" +console-control-strings@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" + integrity sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ== + core-util-is@1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" @@ -993,7 +1055,7 @@ 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.4: +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: version "4.3.4" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== @@ -1012,6 +1074,13 @@ deep-is@^0.1.3: resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== +defaults@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/defaults/-/defaults-1.0.4.tgz#b0b02062c1e2aa62ff5d9528f0f98baa90978d7a" + integrity sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A== + dependencies: + clone "^1.0.2" + defer-to-connect@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-2.0.1.tgz#8016bdb4143e4632b77a3449c6236277de520587" @@ -1026,7 +1095,7 @@ define-data-property@^1.0.1: es-errors "^1.3.0" gopd "^1.0.1" -define-properties@^1.1.3: +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== @@ -1040,11 +1109,21 @@ delayed-stream@~1.0.0: resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== +delegates@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" + integrity sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ== + 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-libc@^2.0.1: + version "2.0.3" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.3.tgz#f0cd503b40f9939b894697d19ad50895e30cf700" + integrity sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw== + detect-newline@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-4.0.1.tgz#fcefdb5713e1fb8cb2839b8b6ee22e6716ab8f23" @@ -1055,13 +1134,13 @@ detect-node@^2.0.4: resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.1.0.tgz#c9c70775a49c3d03bc2c06d9a73be550f978f8b1" integrity sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g== -dir-compare@^3.0.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/dir-compare/-/dir-compare-3.3.0.tgz#2c749f973b5c4b5d087f11edaae730db31788416" - integrity sha512-J7/et3WlGUCxjdnD3HAAzQ6nsnc0WL6DD7WcwJb7c39iH1+AWfg+9OqzJNaI6PkBwBvm1mhZNL9iY/nRiZXlPg== +dir-compare@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/dir-compare/-/dir-compare-4.2.0.tgz#d1d4999c14fbf55281071fdae4293b3b9ce86f19" + integrity sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ== dependencies: - buffer-equal "^1.0.0" - minimatch "^3.0.4" + minimatch "^3.0.5" + p-limit "^3.1.0 " dir-glob@^3.0.1: version "3.0.1" @@ -1070,14 +1149,14 @@ dir-glob@^3.0.1: dependencies: path-type "^4.0.0" -dmg-builder@24.13.3: - version "24.13.3" - resolved "https://registry.yarnpkg.com/dmg-builder/-/dmg-builder-24.13.3.tgz#95d5b99c587c592f90d168a616d7ec55907c7e55" - integrity sha512-rcJUkMfnJpfCboZoOOPf4L29TRtEieHNOeAbYPWPxlaBw/Z1RKrRA86dOI9rwaI4tQSc/RD82zTNHprfUHXsoQ== +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== dependencies: - app-builder-lib "24.13.3" - builder-util "24.13.1" - builder-util-runtime "9.2.4" + app-builder-lib "25.0.0-alpha.6" + builder-util "25.0.0-alpha.6" + builder-util-runtime "9.2.5-alpha.2" fs-extra "^10.1.0" iconv-lite "^0.6.2" js-yaml "^4.1.0" @@ -1117,48 +1196,28 @@ dotenv-expand@^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@^8.2.0: - version "8.6.0" - resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.6.0.tgz#061af664d19f7f4d8fc6e4ff9b584ce237adcb8b" - integrity sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g== - dotenv@^9.0.2: version "9.0.2" resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-9.0.2.tgz#dacc20160935a37dea6364aa1bef819fb9b6ab05" integrity sha512-I9OvvrHp4pIARv4+x9iuewrWycX6CcZtoAu1XrzPxc5UygMJXJZYmBsynku8IkrJwgypE5DGNjDPmPRhDCptUg== -eastasianwidth@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" - integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== - ejs@^3.1.8: - version "3.1.9" - resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.9.tgz#03c9e8777fe12686a9effcef22303ca3d8eeb361" - integrity sha512-rC+QVNMJWv+MtPgkt0y+0rVEIdbtxVADApW9JXrUVlzHetgcyczP/E7DJmWJ4fJCZF2cPcBk0laWO9ZHMG3DmQ== + version "3.1.10" + resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.10.tgz#69ab8358b14e896f80cc39e62087b88500c3ac3b" + integrity sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA== dependencies: jake "^10.8.5" -electron-builder-notarize@^1.5: - version "1.5.2" - resolved "https://registry.yarnpkg.com/electron-builder-notarize/-/electron-builder-notarize-1.5.2.tgz#540185b57a336fc6eec01bfe092a3b4764459255" - integrity sha512-vo6RGgIFYxMk2yp59N4NsvmAYfB7ncYi6gV9Fcq2TVKxEn2tPXrSjIKB2e/pu+5iXIY6BHNZNXa75F3DHgOOLA== +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== dependencies: - dotenv "^8.2.0" - electron-notarize "^1.1.1" - js-yaml "^3.14.0" - read-pkg-up "^7.0.0" - -electron-builder@^24: - version "24.13.3" - resolved "https://registry.yarnpkg.com/electron-builder/-/electron-builder-24.13.3.tgz#c506dfebd36d9a50a83ee8aa32d803d83dbe4616" - integrity sha512-yZSgVHft5dNVlo31qmJAe4BVKQfFdwpRw7sFp1iQglDRCDD6r22zfRJuZlhtB5gp9FHUxCMEoWGq10SkCnMAIg== - dependencies: - app-builder-lib "24.13.3" - builder-util "24.13.1" - builder-util-runtime "9.2.4" + app-builder-lib "25.0.0-alpha.6" + builder-util "25.0.0-alpha.6" + builder-util-runtime "9.2.5-alpha.2" chalk "^4.1.2" - dmg-builder "24.13.3" + dmg-builder "25.0.0-alpha.6" fs-extra "^10.1.0" is-ci "^3.0.0" lazy-val "^1.0.5" @@ -1171,22 +1230,14 @@ electron-log@^5.1: resolved "https://registry.yarnpkg.com/electron-log/-/electron-log-5.1.2.tgz#fb40ad7f4ae694dd0e4c02c662d1a65c03e1243e" integrity sha512-Cpg4hAZ27yM9wzE77c4TvgzxzavZ+dVltCczParXN+Vb3jocojCSAuSMCVOI9fhFuuOR+iuu3tZLX1cu0y0kgQ== -electron-notarize@^1.1.1: - version "1.2.2" - resolved "https://registry.yarnpkg.com/electron-notarize/-/electron-notarize-1.2.2.tgz#ebf2b258e8e08c1c9f8ff61dc53d5b16b439daf4" - integrity sha512-ZStVWYcWI7g87/PgjPJSIIhwQXOaw4/XeXU+pWqMMktSLHaGMLHdyPPN7Cmao7+Cr7fYufA16npdtMndYciHNw== - dependencies: - debug "^4.1.1" - fs-extra "^9.0.1" - -electron-publish@24.13.1: - version "24.13.1" - resolved "https://registry.yarnpkg.com/electron-publish/-/electron-publish-24.13.1.tgz#57289b2f7af18737dc2ad134668cdd4a1b574a0c" - integrity sha512-2ZgdEqJ8e9D17Hwp5LEq5mLQPjqU3lv/IALvgp+4W8VeNhryfGhYEQC/PgDPMrnWUp+l60Ou5SJLsu+k4mhQ8A== +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== dependencies: "@types/fs-extra" "^9.0.11" - builder-util "24.13.1" - builder-util-runtime "9.2.4" + builder-util "25.0.0-alpha.6" + builder-util-runtime "9.2.5-alpha.2" chalk "^4.1.2" fs-extra "^10.1.0" lazy-val "^1.0.5" @@ -1214,10 +1265,10 @@ electron-updater@^6.1: semver "^7.3.8" tiny-typed-emitter "^2.1.0" -electron@^29: - version "29.3.0" - resolved "https://registry.yarnpkg.com/electron/-/electron-29.3.0.tgz#8e65cb08e9c0952c66d3196e1b5c811c43b8c5b0" - integrity sha512-ZxFKm0/v48GSoBuO3DdnMlCYXefEUKUHLMsKxyXY4nZGgzbBKpF/X8haZa2paNj23CLfsCKBOtfc2vsEQiOOsA== +electron@^30: + version "30.0.2" + resolved "https://registry.yarnpkg.com/electron/-/electron-30.0.2.tgz#95ba019216bf8be9f3097580123e33ea37497733" + integrity sha512-zv7T+GG89J/hyWVkQsLH4Y/rVEfqJG5M/wOBIGNaDdqd8UV9/YZPdS7CuFeaIj0H9LhCt95xkIQNpYB/3svOkQ== dependencies: "@electron/get" "^2.0.0" "@types/node" "^20.9.0" @@ -1228,10 +1279,12 @@ emoji-regex@^8.0.0: resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== -emoji-regex@^9.2.2: - version "9.2.2" - resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72" - integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== +encoding@^0.1.13: + version "0.1.13" + resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.13.tgz#56574afdd791f54a8e9b2785c0582a2d26210fa9" + integrity sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A== + dependencies: + iconv-lite "^0.6.2" end-of-stream@^1.1.0: version "1.4.4" @@ -1250,13 +1303,6 @@ err-code@^2.0.2: resolved "https://registry.yarnpkg.com/err-code/-/err-code-2.0.3.tgz#23c2f3b756ffdfc608d30e27c9a941024807e7f9" integrity sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA== -error-ex@^1.3.1: - version "1.3.2" - resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" - integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== - dependencies: - is-arrayish "^0.2.1" - 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" @@ -1279,11 +1325,6 @@ escalade@^3.1.1: 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" @@ -1355,11 +1396,6 @@ espree@^9.6.0, espree@^9.6.1: acorn-jsx "^5.3.2" eslint-visitor-keys "^3.4.1" -esprima@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" - integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== - esquery@^1.4.2: version "1.5.0" resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.5.0.tgz#6ce17738de8577694edd7361c57182ac8cb0db0b" @@ -1384,6 +1420,11 @@ esutils@^2.0.2: resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== +exponential-backoff@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/exponential-backoff/-/exponential-backoff-3.1.1.tgz#64ac7526fe341ab18a39016cd22c787d01e00bf6" + integrity sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw== + extract-zip@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-2.0.1.tgz#663dca56fe46df890d5f131ef4a06d22bb8ba13a" @@ -1478,14 +1519,6 @@ find-up@^3.0.0: dependencies: locate-path "^3.0.0" -find-up@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" - integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== - dependencies: - locate-path "^5.0.0" - path-exists "^4.0.0" - find-up@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" @@ -1495,17 +1528,18 @@ find-up@^5.0.0: path-exists "^4.0.0" flat-cache@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11" - integrity sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg== + 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.1.0" + flatted "^3.2.9" + keyv "^4.5.3" rimraf "^3.0.2" -flatted@^3.1.0: - version "3.2.6" - resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.6.tgz#022e9218c637f9f3fc9c35ab9c9193f05add60b2" - integrity sha512-0sQoMh9s0BYsm+12Huy/rkKxVu4R1+r96YX5cG44rHV0pQ6iC3Q+mkoMFaGWObMFYQxCVT+ssG1ksneA2MI9KQ== +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== foreground-child@^3.1.0: version "3.1.1" @@ -1533,6 +1567,15 @@ fs-extra@^10.0.0, fs-extra@^10.1.0: jsonfile "^6.0.1" universalify "^2.0.0" +fs-extra@^11.1.1: + version "11.2.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-11.2.0.tgz#e70e17dfad64232287d01929399e0ea7c86b0e5b" + integrity sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + fs-extra@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-8.1.0.tgz#49d43c45a88cd9677668cb7be1b46efdb8d2e1c0" @@ -1552,7 +1595,7 @@ fs-extra@^9.0.0, fs-extra@^9.0.1: jsonfile "^6.0.1" universalify "^2.0.0" -fs-minipass@^2.0.0: +fs-minipass@^2.0.0, fs-minipass@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb" integrity sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg== @@ -1574,6 +1617,20 @@ function-bind@^1.1.2: resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== +gauge@^4.0.3: + version "4.0.4" + resolved "https://registry.yarnpkg.com/gauge/-/gauge-4.0.4.tgz#52ff0652f2bbf607a989793d53b751bef2328dce" + integrity sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg== + dependencies: + aproba "^1.0.3 || ^2.0.0" + color-support "^1.1.3" + console-control-strings "^1.1.0" + has-unicode "^2.0.1" + signal-exit "^3.0.7" + string-width "^4.2.3" + strip-ansi "^6.0.1" + wide-align "^1.1.5" + get-caller-file@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" @@ -1621,18 +1678,18 @@ glob-parent@^6.0.2: dependencies: is-glob "^4.0.3" -glob@^10.3.10: - version "10.3.10" - resolved "https://registry.yarnpkg.com/glob/-/glob-10.3.10.tgz#0351ebb809fd187fe421ab96af83d3a70715df4b" - integrity sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g== +glob@^10.3.10, glob@^10.3.7: + 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== dependencies: foreground-child "^3.1.0" - jackspeak "^2.3.5" + jackspeak "^2.3.6" minimatch "^9.0.1" - minipass "^5.0.0 || ^6.0.2 || ^7.0.0" - path-scurry "^1.10.1" + minipass "^7.0.4" + path-scurry "^1.10.2" -glob@^7.0.0, glob@^7.1.3, glob@^7.1.6: +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" integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== @@ -1644,6 +1701,17 @@ glob@^7.0.0, glob@^7.1.3, glob@^7.1.6: once "^1.3.0" path-is-absolute "^1.0.0" +glob@^8.0.1: + version "8.1.0" + resolved "https://registry.yarnpkg.com/glob/-/glob-8.1.0.tgz#d388f656593ef708ee3e34640fdfb99a9fd1c33e" + integrity sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^5.0.1" + once "^1.3.0" + global-agent@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/global-agent/-/global-agent-3.0.0.tgz#ae7cd31bd3583b93c5a16437a1afe27cc33a1ab6" @@ -1664,11 +1732,12 @@ globals@^13.19.0: type-fest "^0.20.2" globalthis@^1.0.1: - version "1.0.3" - resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.3.tgz#5852882a52b80dc301b0660273e1ed082f0b6ccf" - integrity sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA== + 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.1.3" + define-properties "^1.2.1" + gopd "^1.0.1" globby@^11.1.0: version "11.1.0" @@ -1700,7 +1769,7 @@ gopd@^1.0.1: dependencies: get-intrinsic "^1.1.3" -got@^11.8.5: +got@^11.7.0, got@^11.8.5: version "11.8.6" resolved "https://registry.yarnpkg.com/got/-/got-11.8.6.tgz#276e827ead8772eddbcfc97170590b841823233a" integrity sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g== @@ -1717,7 +1786,7 @@ got@^11.8.5: p-cancelable "^2.0.0" responselike "^2.0.0" -graceful-fs@^4.1.6, graceful-fs@^4.2.0: +graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.6: version "4.2.11" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== @@ -1727,11 +1796,6 @@ graphemer@^1.4.0: resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== -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" @@ -1754,6 +1818,11 @@ has-symbols@^1.0.3: resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== +has-unicode@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" + integrity sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ== + hasown@^2.0.0: version "2.0.2" resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" @@ -1761,11 +1830,6 @@ hasown@^2.0.0: dependencies: function-bind "^1.1.2" -hosted-git-info@^2.1.4: - version "2.8.9" - resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.9.tgz#dffc0bf9a21c02209090f2aa69429e1414daf3f9" - integrity sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw== - hosted-git-info@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-4.1.0.tgz#827b82867e9ff1c8d0c4d9d53880397d2c86d224" @@ -1778,7 +1842,7 @@ html-entities@^2.5: resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-2.5.2.tgz#201a3cf95d3a15be7099521620d19dfb4f65359f" integrity sha512-K//PSRMQk4FZ78Kyau+mZurHn3FH0Vwr+H36eE0rPbeYkRRi9YxceYPhuN60UwWorxyKHhqoAJl2OFKa4BVtaA== -http-cache-semantics@^4.0.0: +http-cache-semantics@^4.0.0, http-cache-semantics@^4.1.0: version "4.1.1" resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz#abe02fcb2985460bf0323be664436ec3476a6d5a" integrity sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ== @@ -1815,6 +1879,13 @@ https-proxy-agent@^5.0.0, https-proxy-agent@^5.0.1: agent-base "6" debug "4" +humanize-ms@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/humanize-ms/-/humanize-ms-1.2.1.tgz#c46e3159a293f6b896da29316d8b6fe8bb79bbed" + integrity sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ== + dependencies: + ms "^2.0.0" + iconv-corefoundation@^1.1.7: version "1.1.7" resolved "https://registry.yarnpkg.com/iconv-corefoundation/-/iconv-corefoundation-1.1.7.tgz#31065e6ab2c9272154c8b0821151e2c88f1b002a" @@ -1853,6 +1924,16 @@ imurmurhash@^0.1.4: resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== +indent-string@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" + integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== + +infer-owner@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/infer-owner/-/infer-owner-1.0.4.tgz#c4cefcaa8e51051c2a40ba2ce8a3d27295af9467" + integrity sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A== + inflight@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" @@ -1861,7 +1942,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@^2.0.3: +inherits@2, inherits@^2.0.3, inherits@^2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -1871,10 +1952,13 @@ interpret@^1.0.0: resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e" integrity sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA== -is-arrayish@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" - integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg== +ip-address@^9.0.5: + version "9.0.5" + resolved "https://registry.yarnpkg.com/ip-address/-/ip-address-9.0.5.tgz#117a960819b08780c3bd1f14ef3c1cc1d3f3ea5a" + integrity sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g== + dependencies: + jsbn "1.1.0" + sprintf-js "^1.1.3" is-binary-path@~2.1.0: version "2.1.0" @@ -1914,6 +1998,16 @@ is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: dependencies: is-extglob "^2.1.1" +is-interactive@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-interactive/-/is-interactive-1.0.0.tgz#cea6e6ae5c870a7b0a0004070b7b587e0252912e" + integrity sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w== + +is-lambda@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-lambda/-/is-lambda-1.0.1.tgz#3d9877899e6a53efc0160504cde15f82e6f061d5" + integrity sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ== + is-number@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" @@ -1934,6 +2028,11 @@ is-plain-obj@^4.1.0: resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-4.1.0.tgz#d65025edec3657ce032fd7db63c97883eaed71f0" integrity sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg== +is-unicode-supported@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" + integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== + isbinaryfile@^4.0.8: version "4.0.10" resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-4.0.10.tgz#0c5b5e30c2557a2f06febd37b7322946aaee42b3" @@ -1949,12 +2048,12 @@ isexe@^2.0.0: resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== -jackspeak@^2.3.5: - version "2.3.6" - resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-2.3.6.tgz#647ecc472238aee4b06ac0e461acc21a8c505ca8" - integrity sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ== +jackspeak@2.1.1, jackspeak@^2.3.6: + version "2.1.1" + resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-2.1.1.tgz#2a42db4cfbb7e55433c28b6f75d8b796af9669cd" + integrity sha512-juf9stUEwUaILepraGOWIJTLwg48bUnBmRqd2ln2Os1sW987zeoj/hzhbvRB95oMuS2ZTpjULmdwHNX4rzZIZw== dependencies: - "@isaacs/cliui" "^8.0.2" + cliui "^8.0.1" optionalDependencies: "@pkgjs/parseargs" "^0.11.0" @@ -1973,19 +2072,6 @@ jpeg-js@^0.4: resolved "https://registry.yarnpkg.com/jpeg-js/-/jpeg-js-0.4.4.tgz#a9f1c6f1f9f0fa80cdb3484ed9635054d28936aa" integrity sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg== -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@^3.14.0: - version "3.14.1" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537" - integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g== - dependencies: - argparse "^1.0.7" - esprima "^4.0.0" - js-yaml@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" @@ -1993,16 +2079,16 @@ js-yaml@^4.1.0: dependencies: argparse "^2.0.1" +jsbn@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-1.1.0.tgz#b01307cb29b618a1ed26ec79e911f803c4da0040" + integrity sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A== + 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-parse-even-better-errors@^2.3.0: - version "2.3.1" - resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" - integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== - 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" @@ -2049,7 +2135,7 @@ jsonfile@^6.0.1: optionalDependencies: graceful-fs "^4.1.6" -keyv@^4.0.0: +keyv@^4.0.0, 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== @@ -2069,11 +2155,6 @@ levn@^0.4.1: prelude-ls "^1.2.1" type-check "~0.4.0" -lines-and-columns@^1.1.6: - version "1.2.4" - resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" - integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== - locate-path@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e" @@ -2082,13 +2163,6 @@ locate-path@^3.0.0: p-locate "^3.0.0" path-exists "^3.0.0" -locate-path@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" - integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== - dependencies: - p-locate "^4.1.0" - locate-path@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" @@ -2116,11 +2190,24 @@ lodash@^4.17.15, lodash@^4.17.21: resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== +log-symbols@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503" + integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg== + dependencies: + chalk "^4.1.0" + is-unicode-supported "^0.1.0" + lowercase-keys@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-2.0.0.tgz#2603e78b7b4b0006cbca2fbcc8a3202558ac9479" integrity sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA== +lru-cache@^10.2.0: + version "10.2.2" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.2.2.tgz#48206bc114c1252940c41b25b41af5b545aca878" + integrity sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ== + lru-cache@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" @@ -2128,10 +2215,32 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" -"lru-cache@^9.1.1 || ^10.0.0": - version "10.2.0" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.2.0.tgz#0bd445ca57363465900f4d1f9bd8db343a4d95c3" - integrity sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q== +lru-cache@^7.7.1: + version "7.18.3" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.18.3.tgz#f793896e0fd0e954a59dfdd82f0773808df6aa89" + integrity sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA== + +make-fetch-happen@^10.0.3: + version "10.2.1" + resolved "https://registry.yarnpkg.com/make-fetch-happen/-/make-fetch-happen-10.2.1.tgz#f5e3835c5e9817b617f2770870d9492d28678164" + integrity sha512-NgOPbRiaQM10DYXvN3/hhGVI2M5MtITFryzBGxHM5p4wnFxsVCbxkrBrDsk+EZ5OB4jEOT7AjDxtdF+KVEFT7w== + dependencies: + agentkeepalive "^4.2.1" + cacache "^16.1.0" + http-cache-semantics "^4.1.0" + http-proxy-agent "^5.0.0" + https-proxy-agent "^5.0.0" + is-lambda "^1.0.1" + lru-cache "^7.7.1" + minipass "^3.1.6" + minipass-collect "^1.0.2" + minipass-fetch "^2.0.3" + minipass-flush "^1.0.5" + minipass-pipeline "^1.2.4" + negotiator "^0.6.3" + promise-retry "^2.0.1" + socks-proxy-agent "^7.0.0" + ssri "^9.0.0" matcher@^3.0.0: version "3.0.0" @@ -2204,14 +2313,7 @@ minimatch@^5.0.1, minimatch@^5.1.1: dependencies: brace-expansion "^2.0.1" -minimatch@^9.0.1: - version "9.0.3" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.3.tgz#a6e00c3de44c3a542bfaae70abfc22420a6da825" - integrity sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg== - dependencies: - brace-expansion "^2.0.1" - -minimatch@^9.0.4: +minimatch@^9.0.1, minimatch@^9.0.3, 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== @@ -2223,7 +2325,46 @@ minimist@^1.2.3, minimist@^1.2.6: resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== -minipass@^3.0.0: +minipass-collect@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/minipass-collect/-/minipass-collect-1.0.2.tgz#22b813bf745dc6edba2576b940022ad6edc8c617" + integrity sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA== + dependencies: + minipass "^3.0.0" + +minipass-fetch@^2.0.3: + version "2.1.2" + resolved "https://registry.yarnpkg.com/minipass-fetch/-/minipass-fetch-2.1.2.tgz#95560b50c472d81a3bc76f20ede80eaed76d8add" + integrity sha512-LT49Zi2/WMROHYoqGgdlQIZh8mLPZmOrN2NdJjMXxYe4nkN6FUyuPuOAOedNJDrx0IRGg9+4guZewtp8hE6TxA== + dependencies: + minipass "^3.1.6" + minipass-sized "^1.0.3" + minizlib "^2.1.2" + optionalDependencies: + encoding "^0.1.13" + +minipass-flush@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/minipass-flush/-/minipass-flush-1.0.5.tgz#82e7135d7e89a50ffe64610a787953c4c4cbb373" + integrity sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw== + dependencies: + minipass "^3.0.0" + +minipass-pipeline@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz#68472f79711c084657c067c5c6ad93cddea8214c" + integrity sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A== + dependencies: + minipass "^3.0.0" + +minipass-sized@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/minipass-sized/-/minipass-sized-1.0.3.tgz#70ee5a7c5052070afacfbc22977ea79def353b70" + integrity sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g== + dependencies: + minipass "^3.0.0" + +minipass@^3.0.0, minipass@^3.1.1, minipass@^3.1.6: version "3.3.6" resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.3.6.tgz#7bba384db3a1520d18c9c0e5251c3444e95dd94a" integrity sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw== @@ -2235,12 +2376,12 @@ 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@^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== -minizlib@^2.1.1: +minizlib@^2.1.1, minizlib@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931" integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg== @@ -2248,6 +2389,14 @@ minizlib@^2.1.1: minipass "^3.0.0" yallist "^4.0.0" +minizlib@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-3.0.1.tgz#46d5329d1eb3c83924eff1d3b858ca0a31581012" + integrity sha512-umcy022ILvb5/3Djuu8LWeqUa8D68JaBzlttKeMWen48SjabqS3iY5w/vzeMzMUNhLDifyhbOwKDSznB1vvrwg== + dependencies: + minipass "^7.0.4" + rimraf "^5.0.5" + mkdirp@^0.5.1: version "0.5.6" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6" @@ -2255,45 +2404,88 @@ mkdirp@^0.5.1: dependencies: minimist "^1.2.6" -mkdirp@^1.0.3: +mkdirp@^1.0.3, mkdirp@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== +mkdirp@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-3.0.1.tgz#e44e4c5607fb279c168241713cc6e0fea9adcb50" + integrity sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg== + 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== +ms@^2.0.0: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + 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== +negotiator@^0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" + integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== + next-electron-server@^1: version "1.0.0" resolved "https://registry.yarnpkg.com/next-electron-server/-/next-electron-server-1.0.0.tgz#03e133ed64a5ef671b6c6409f908c4901b1828cb" integrity sha512-fTUaHwT0Jry2fbdUSIkAiIqgDAInI5BJFF4/j90/okvZCYlyx6yxpXB30KpzmOG6TN/ESwyvsFJVvS2WHT8PAA== +node-abi@^3.45.0: + version "3.62.0" + resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-3.62.0.tgz#017958ed120f89a3a14a7253da810f5d724e3f36" + integrity sha512-CPMcGa+y33xuL1E0TcNIu4YyaZCxnnvkVaEXrsosR3FxN+fV8xvb7Mzpb7IgKler10qeMkE6+Dp8qJhpzdq35g== + dependencies: + semver "^7.3.5" + node-addon-api@^1.6.3: version "1.7.2" resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-1.7.2.tgz#3df30b95720b53c24e59948b49532b662444f54d" integrity sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg== +node-api-version@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/node-api-version/-/node-api-version-0.2.0.tgz#5177441da2b1046a4d4547ab9e0972eed7b1ac1d" + integrity sha512-fthTTsi8CxaBXMaBAD7ST2uylwvsnYxh2PfaScwpMhos6KlSFajXQPcM4ogNE1q2s3Lbz9GCGqeIHC+C6OZnKg== + dependencies: + semver "^7.3.5" + +node-gyp@^9.0.0: + version "9.4.1" + resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-9.4.1.tgz#8a1023e0d6766ecb52764cc3a734b36ff275e185" + integrity sha512-OQkWKbjQKbGkMf/xqI1jjy3oCTgMKJac58G2+bjZb3fza6gW2YrCSdMQYaoTb70crvE//Gngr4f0AgVHmqHvBQ== + dependencies: + env-paths "^2.2.0" + exponential-backoff "^3.1.1" + glob "^7.1.4" + graceful-fs "^4.2.6" + make-fetch-happen "^10.0.3" + nopt "^6.0.0" + npmlog "^6.0.0" + rimraf "^3.0.2" + semver "^7.3.5" + tar "^6.1.2" + which "^2.0.2" + node-stream-zip@^1.15: version "1.15.0" resolved "https://registry.yarnpkg.com/node-stream-zip/-/node-stream-zip-1.15.0.tgz#158adb88ed8004c6c49a396b50a6a5de3bca33ea" integrity sha512-LN4fydt9TqhZhThkZIVQnF9cwjU3qmUH9h78Mx/K7d3VvfRqqwthLwJEUOEL0QPZ0XQmNN7be5Ggit5+4dq3Bw== -normalize-package-data@^2.5.0: - version "2.5.0" - resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" - integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA== +nopt@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-6.0.0.tgz#245801d8ebf409c6df22ab9d95b65e1309cdb16d" + integrity sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g== dependencies: - hosted-git-info "^2.1.4" - resolve "^1.10.0" - semver "2 || 3 || 4 || 5" - validate-npm-package-license "^3.0.1" + abbrev "^1.0.0" normalize-path@^3.0.0, normalize-path@~3.0.0: version "3.0.0" @@ -2305,6 +2497,16 @@ normalize-url@^6.0.1: resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-6.1.0.tgz#40d0885b535deffe3f3147bec877d05fe4c5668a" integrity sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A== +npmlog@^6.0.0: + version "6.0.2" + resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-6.0.2.tgz#c8166017a42f2dea92d6453168dd865186a70830" + integrity sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg== + dependencies: + are-we-there-yet "^3.0.0" + console-control-strings "^1.1.0" + gauge "^4.0.3" + set-blocking "^2.0.0" + object-keys@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" @@ -2317,50 +2519,66 @@ once@^1.3.0, once@^1.3.1, once@^1.4.0: dependencies: wrappy "1" -onetime@^5.1.2: +onetime@^5.1.0, onetime@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== dependencies: mimic-fn "^2.1.0" -onnxruntime-common@1.17.0: - version "1.17.0" - resolved "https://registry.yarnpkg.com/onnxruntime-common/-/onnxruntime-common-1.17.0.tgz#b2534ce021b1c1b19182bec39aaea8d547d2013e" - integrity sha512-Vq1remJbCPITjDMJ04DA7AklUTnbYUp4vbnm6iL7ukSt+7VErH0NGYfekRSTjxxurEtX7w41PFfnQlE6msjPJw== +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-node@^1.17: - version "1.17.0" - resolved "https://registry.yarnpkg.com/onnxruntime-node/-/onnxruntime-node-1.17.0.tgz#38af0ba527cb44c1afb639bdcb4e549edba029a1" - integrity sha512-pRxdqSP3a6wtiFVkVX1V3/gsEMwBRUA9D2oYmcN3cjF+j+ILS+SIY2L7KxdWapsG6z64i5rUn8ijFZdIvbojBg== + version "1.17.3" + resolved "https://registry.yarnpkg.com/onnxruntime-node/-/onnxruntime-node-1.17.3.tgz#53b8b7ef68bf3834bba9d7be592e4c2d718d2018" + integrity sha512-NtbN1pfApTSEjVq46LrJ396aPP2Gjhy+oYZi5Bu1leDXAEvVap/BQ8CZELiLs7z0UnXy3xjJW23HiB4P3//FIw== dependencies: - onnxruntime-common "1.17.0" + onnxruntime-common "1.17.3" + tar "^7.0.1" optionator@^0.9.3: - version "0.9.3" - resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.3.tgz#007397d44ed1872fdc6ed31360190f81814e2c64" - integrity sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg== + version "0.9.4" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.4.tgz#7ea1c1a5d91d764fb282139c88fe11e182a3a734" + integrity sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g== dependencies: - "@aashutoshrathi/word-wrap" "^1.2.3" 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" + +ora@^5.1.0: + version "5.4.1" + resolved "https://registry.yarnpkg.com/ora/-/ora-5.4.1.tgz#1b2678426af4ac4a509008e5e4ac9e9959db9e18" + integrity sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ== + dependencies: + bl "^4.1.0" + chalk "^4.1.0" + cli-cursor "^3.1.0" + cli-spinners "^2.5.0" + is-interactive "^1.0.0" + is-unicode-supported "^0.1.0" + log-symbols "^4.1.0" + strip-ansi "^6.0.0" + wcwidth "^1.0.1" p-cancelable@^2.0.0: version "2.1.1" resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-2.1.1.tgz#aab7fbd416582fa32a3db49859c122487c5ed2cf" integrity sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg== -p-limit@^2.0.0, p-limit@^2.2.0: +p-limit@^2.0.0: version "2.3.0" resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== dependencies: p-try "^2.0.0" -p-limit@^3.0.2: +p-limit@^3.0.2, "p-limit@^3.1.0 ": version "3.1.0" resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== @@ -2374,13 +2592,6 @@ p-locate@^3.0.0: dependencies: p-limit "^2.0.0" -p-locate@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" - integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== - dependencies: - p-limit "^2.2.0" - p-locate@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" @@ -2388,6 +2599,13 @@ p-locate@^5.0.0: dependencies: p-limit "^3.0.2" +p-map@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/p-map/-/p-map-4.0.0.tgz#bb2f95a5eda2ec168ec9274e06a747c3e2904d2b" + integrity sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ== + dependencies: + aggregate-error "^3.0.0" + p-try@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" @@ -2405,16 +2623,6 @@ parse-cache-control@^1.0.1: resolved "https://registry.yarnpkg.com/parse-cache-control/-/parse-cache-control-1.0.1.tgz#8eeab3e54fa56920fe16ba38f77fa21aacc2d74e" integrity sha512-60zvsJReQPX5/QP0Kzfd/VrpjScIQ7SHBW6bFCYfEP+fp0Eppr1SHhIO5nd1PjZtvclzSzES9D/p5nFJurwfWg== -parse-json@^5.0.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" - integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== - dependencies: - "@babel/code-frame" "^7.0.0" - error-ex "^1.3.1" - json-parse-even-better-errors "^2.3.0" - lines-and-columns "^1.1.6" - path-exists@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" @@ -2440,12 +2648,12 @@ 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.1: - version "1.10.1" - resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.10.1.tgz#9ba6bf5aa8500fe9fd67df4f0d9483b2b0bfc698" - integrity sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ== +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== dependencies: - lru-cache "^9.1.1 || ^10.0.0" + lru-cache "^10.2.0" minipass "^5.0.0 || ^6.0.2 || ^7.0.0" path-type@^4.0.0: @@ -2458,11 +2666,6 @@ pend@~1.2.0: resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" integrity sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg== -picocolors@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" - integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== - picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" @@ -2475,7 +2678,7 @@ pkg-up@^3.1.0: dependencies: find-up "^3.0.0" -plist@^3.0.4, plist@^3.0.5: +plist@^3.0.4, plist@^3.0.5, plist@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/plist/-/plist-3.1.0.tgz#797a516a93e62f5bde55e0b9cc9c967f860893c9" integrity sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ== @@ -2512,6 +2715,11 @@ progress@^2.0.3: resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== +promise-inflight@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3" + integrity sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g== + promise-retry@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/promise-retry/-/promise-retry-2.0.1.tgz#ff747a13620ab57ba688f5fc67855410c370da22" @@ -2543,6 +2751,13 @@ quick-lru@^5.1.1: resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932" integrity sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA== +read-binary-file-arch@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/read-binary-file-arch/-/read-binary-file-arch-1.0.6.tgz#959c4637daa932280a9b911b1a6766a7e44288fc" + integrity sha512-BNg9EN3DD3GsDXX7Aa8O4p92sryjkmzYYgmgTAc6CA4uGLEDzFfxOxugu21akOxpcXHiEgsYkC6nPsQvLLLmEg== + dependencies: + debug "^4.3.4" + read-config-file@6.3.2: version "6.3.2" resolved "https://registry.yarnpkg.com/read-config-file/-/read-config-file-6.3.2.tgz#556891aa6ffabced916ed57457cb192e61880411" @@ -2555,26 +2770,7 @@ read-config-file@6.3.2: json5 "^2.2.0" lazy-val "^1.0.4" -read-pkg-up@^7.0.0: - version "7.0.1" - resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-7.0.1.tgz#f3a6135758459733ae2b95638056e1854e7ef507" - integrity sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg== - dependencies: - find-up "^4.1.0" - read-pkg "^5.2.0" - type-fest "^0.8.1" - -read-pkg@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-5.2.0.tgz#7bf295438ca5a33e56cd30e053b34ee7250c93cc" - integrity sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg== - dependencies: - "@types/normalize-package-data" "^2.4.0" - normalize-package-data "^2.5.0" - parse-json "^5.0.0" - type-fest "^0.6.0" - -readable-stream@^3.0.2: +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" integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== @@ -2622,7 +2818,7 @@ resolve-from@^4.0.0: resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== -resolve@^1.1.6, resolve@^1.10.0: +resolve@^1.1.6: version "1.22.8" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== @@ -2638,6 +2834,14 @@ responselike@^2.0.0: dependencies: lowercase-keys "^2.0.0" +restore-cursor@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e" + integrity sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA== + dependencies: + onetime "^5.1.0" + signal-exit "^3.0.2" + retry@^0.12.0: version "0.12.0" resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b" @@ -2655,6 +2859,13 @@ rimraf@^3.0.2: dependencies: 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== + dependencies: + glob "^10.3.7" + roarr@^2.15.3: version "2.15.4" resolved "https://registry.yarnpkg.com/roarr/-/roarr-2.15.4.tgz#f5fe795b7b838ccfe35dc608e0282b9eba2e7afd" @@ -2708,11 +2919,6 @@ semver-compare@^1.0.0: resolved "https://registry.yarnpkg.com/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc" integrity sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow== -"semver@2 || 3 || 4 || 5": - version "5.7.2" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8" - integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g== - semver@^6.2.0: version "6.3.1" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" @@ -2732,6 +2938,11 @@ serialize-error@^7.0.1: dependencies: type-fest "^0.13.1" +set-blocking@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" + integrity sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw== + shebang-command@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" @@ -2766,6 +2977,11 @@ shx@^0.3: minimist "^1.2.3" shelljs "^0.8.5" +signal-exit@^3.0.2, signal-exit@^3.0.7: + version "3.0.7" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" + integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== + signal-exit@^4.0.1: version "4.1.0" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" @@ -2797,11 +3013,28 @@ slice-ansi@^3.0.0: astral-regex "^2.0.0" is-fullwidth-code-point "^3.0.0" -smart-buffer@^4.0.2: +smart-buffer@^4.0.2, smart-buffer@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.2.0.tgz#6e1d71fa4f18c05f7d0ff216dd16a481d0e8d9ae" integrity sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg== +socks-proxy-agent@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-7.0.0.tgz#dc069ecf34436621acb41e3efa66ca1b5fed15b6" + integrity sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww== + dependencies: + agent-base "^6.0.2" + debug "^4.3.3" + socks "^2.6.2" + +socks@^2.6.2: + version "2.8.3" + resolved "https://registry.yarnpkg.com/socks/-/socks-2.8.3.tgz#1ebd0f09c52ba95a09750afe3f3f9f724a800cb5" + integrity sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw== + dependencies: + ip-address "^9.0.5" + smart-buffer "^4.2.0" + 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" @@ -2839,48 +3072,24 @@ spawn-command@0.0.2: resolved "https://registry.yarnpkg.com/spawn-command/-/spawn-command-0.0.2.tgz#9544e1a43ca045f8531aac1a48cb29bdae62338e" integrity sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ== -spdx-correct@^3.0.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.2.0.tgz#4f5ab0668f0059e34f9c00dce331784a12de4e9c" - integrity sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA== - dependencies: - spdx-expression-parse "^3.0.0" - spdx-license-ids "^3.0.0" - -spdx-exceptions@^2.1.0: - version "2.5.0" - resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz#5d607d27fc806f66d7b64a766650fa890f04ed66" - integrity sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w== - -spdx-expression-parse@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz#cf70f50482eefdc98e3ce0a6833e4a53ceeba679" - integrity sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q== - dependencies: - spdx-exceptions "^2.1.0" - spdx-license-ids "^3.0.0" - -spdx-license-ids@^3.0.0: - version "3.0.17" - resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.17.tgz#887da8aa73218e51a1d917502d79863161a93f9c" - integrity sha512-sh8PWc/ftMqAAdFiBu6Fy6JUOYjqDJBJvIhpfDMyHrr0Rbp5liZqd4TjtQ/RgfLjKFZb+LMx5hpml5qOWy0qvg== - -sprintf-js@^1.1.2: +sprintf-js@^1.1.2, sprintf-js@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.1.3.tgz#4914b903a2f8b685d17fdf78a70e917e872e444a" integrity sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA== -sprintf-js@~1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" - integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g== +ssri@^9.0.0: + version "9.0.1" + resolved "https://registry.yarnpkg.com/ssri/-/ssri-9.0.1.tgz#544d4c357a8d7b71a19700074b6883fcb4eae057" + integrity sha512-o57Wcn66jMQvfHG1FlYbWeZWW/dHZhJXjpIcTfXldXEk5nz5lStPo3mK0OJQfGR3RbZUlbISexbljkJzuEj/8Q== + dependencies: + minipass "^3.1.1" stat-mode@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/stat-mode/-/stat-mode-1.0.0.tgz#68b55cb61ea639ff57136f36b216a291800d1465" integrity sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg== -"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -2889,15 +3098,6 @@ stat-mode@^1.0.0: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.1" -string-width@^5.0.1, string-width@^5.1.2: - version "5.1.2" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" - integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA== - dependencies: - eastasianwidth "^0.2.0" - emoji-regex "^9.2.2" - strip-ansi "^7.0.1" - string_decoder@^1.1.1: version "1.3.0" resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" @@ -2905,20 +3105,13 @@ string_decoder@^1.1.1: dependencies: safe-buffer "~5.2.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: +strip-ansi@^6.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-ansi@^7.0.1: - version "7.1.0" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" - integrity sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ== - dependencies: - ansi-regex "^6.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" @@ -2931,13 +3124,6 @@ sumchecker@^3.0.1: dependencies: debug "^4.1.0" -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" @@ -2965,7 +3151,7 @@ synckit@0.9.0: "@pkgr/core" "^0.1.0" tslib "^2.6.2" -tar@^6.1.12: +tar@^6.0.5, tar@^6.1.11, tar@^6.1.12, tar@^6.1.2: version "6.2.1" resolved "https://registry.yarnpkg.com/tar/-/tar-6.2.1.tgz#717549c541bc3c2af15751bea94b1dd068d4b03a" integrity sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A== @@ -2977,6 +3163,18 @@ tar@^6.1.12: mkdirp "^1.0.3" 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== + dependencies: + "@isaacs/fs-minipass" "^4.0.0" + chownr "^3.0.0" + minipass "^5.0.0" + minizlib "^3.0.1" + mkdirp "^3.0.1" + yallist "^5.0.0" + temp-file@^3.4.0: version "3.4.0" resolved "https://registry.yarnpkg.com/temp-file/-/temp-file-3.4.0.tgz#766ea28911c683996c248ef1a20eea04d51652c7" @@ -3031,12 +3229,7 @@ ts-api-utils@^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.1.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3" - integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ== - -tslib@^2.6.2: +tslib@^2.1.0, 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== @@ -3058,16 +3251,6 @@ type-fest@^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@^0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.6.0.tgz#8d2a2370d3df886eb5c90ada1c5bf6188acf838b" - integrity sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg== - -type-fest@^0.8.1: - version "0.8.1" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" - integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== - type-fest@^2.17.0: version "2.19.0" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.19.0.tgz#88068015bb33036a598b952e55e9311a60fd3a9b" @@ -3079,15 +3262,29 @@ typedarray@^0.0.6: integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA== typescript@^5, typescript@^5.3.3: - version "5.4.3" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.4.3.tgz#5c6fedd4c87bee01cd7a528a30145521f8e0feff" - integrity sha512-KrPd3PKaCLr78MalgiwJnA25Nm8HAmdwN3mYUYZgG/wizIo9EainNVQI9/yDavtVFRN2h3k8uf3GLHuhDMgEHg== + version "5.4.5" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.4.5.tgz#42ccef2c571fdbd0f6718b1d1f5e6e5ef006f611" + integrity sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ== undici-types@~5.26.4: version "5.26.5" resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== +unique-filename@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-2.0.1.tgz#e785f8675a9a7589e0ac77e0b5c34d2eaeac6da2" + integrity sha512-ODWHtkkdx3IAR+veKxFV+VBkUMcN+FaqzUUd7IZzt+0zhDZFPFxhlqwPF3YQvMHx1TD0tdgYl+kuPnJ8E6ql7A== + dependencies: + unique-slug "^3.0.0" + +unique-slug@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/unique-slug/-/unique-slug-3.0.0.tgz#6d347cf57c8a7a7a6044aabd0e2d74e4d76dc7c9" + integrity sha512-8EyMynh679x/0gqE9fT9oilG+qEt+ibFyqjuVTsZn1+CMxH+XLlpvr2UZx4nVcCwTpx81nICr2JQFkM+HPLq4w== + dependencies: + imurmurhash "^0.1.4" + universalify@^0.1.0: version "0.1.2" resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.2.tgz#b646f69be3942dabcecc9d6639c80dc105efaa66" @@ -3103,7 +3300,7 @@ untildify@^3.0.2: resolved "https://registry.yarnpkg.com/untildify/-/untildify-3.0.3.tgz#1e7b42b140bcfd922b22e70ca1265bfe3634c7c9" integrity sha512-iSk/J8efr8uPT/Z4eSUywnqyrQU7DSdMfdqK4iWEaUVVmcP5JcnpRqmVMwcwcnmI1ATFNgC5V90u09tBynNFKA== -uri-js@^4.2.2: +uri-js@^4.2.2, uri-js@^4.4.1: version "4.4.1" resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== @@ -3120,14 +3317,6 @@ util-deprecate@^1.0.1: resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== -validate-npm-package-license@^3.0.1: - version "3.0.4" - resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" - integrity sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew== - dependencies: - spdx-correct "^3.0.0" - spdx-expression-parse "^3.0.0" - verror@^1.10.0: version "1.10.1" resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.1.tgz#4bf09eeccf4563b109ed4b3d458380c972b0cdeb" @@ -3137,19 +3326,38 @@ verror@^1.10.0: core-util-is "1.0.2" extsprintf "^1.2.0" -which@^2.0.1: +wcwidth@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/wcwidth/-/wcwidth-1.0.1.tgz#f0b0dcf915bc5ff1528afadb2c0e17b532da2fe8" + integrity sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg== + dependencies: + defaults "^1.0.3" + +which@^2.0.1, which@^2.0.2: 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" +wide-align@^1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.5.tgz#df1d4c206854369ecf3c9a4898f1b23fbd9d15d3" + integrity sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg== + dependencies: + string-width "^1.0.2 || 2 || 3 || 4" + winreg@1.2.4: version "1.2.4" resolved "https://registry.yarnpkg.com/winreg/-/winreg-1.2.4.tgz#ba065629b7a925130e15779108cf540990e98d1b" integrity sha512-IHpzORub7kYlb8A43Iig3reOvlcBJGX9gZ0WycHhghHtA65X0LYnMRuJs+aH1abVnMJztQkvQNlltnbPi5aGIA== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.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== + +wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -3158,15 +3366,6 @@ winreg@1.2.4: string-width "^4.1.0" strip-ansi "^6.0.0" -wrap-ansi@^8.1.0: - version "8.1.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" - integrity sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ== - dependencies: - ansi-styles "^6.1.0" - string-width "^5.0.1" - strip-ansi "^7.0.1" - wrappy@1: version "1.0.2" resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" @@ -3187,12 +3386,17 @@ yallist@^4.0.0: resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== +yallist@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-5.0.0.tgz#00e2de443639ed0d78fd87de0d27469fbcffb533" + integrity sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw== + yargs-parser@^21.1.1: version "21.1.1" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== -yargs@^17.6.2, yargs@^17.7.2: +yargs@^17.0.1, yargs@^17.6.2, yargs@^17.7.2: version "17.7.2" resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269" integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w== diff --git a/docs/docs/self-hosting/guides/custom-server/index.md b/docs/docs/self-hosting/guides/custom-server/index.md index bf695af30876fb8a5050d94555959dba2460f362..8e16004a12a6ae4a8c4a7e27399278d9a2301eb7 100644 --- a/docs/docs/self-hosting/guides/custom-server/index.md +++ b/docs/docs/self-hosting/guides/custom-server/index.md @@ -25,10 +25,26 @@ configure the endpoint the app should be connecting to. > You can download the CLI from > [here](https://github.com/ente-io/ente/releases?q=tag%3Acli-v0) -Define a config.yaml and put it either in the same directory as CLI or path -defined in env variable `ENTE_CLI_CONFIG_PATH` +Define a config.yaml and put it either in the same directory as where you run +the CLI from ("current working directory"), or in the path defined in env +variable `ENTE_CLI_CONFIG_PATH`: ```yaml endpoint: api: "http://localhost:8080" ``` + +(Another [example](https://github.com/ente-io/ente/blob/main/cli/config.yaml.example)) + +## Web appps 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 +connect to. For example: + +```sh +NEXT_PUBLIC_ENTE_ENDPOINT=http://localhost:8080 yarn dev:photos +``` + +For more details, see [hosting the web +app](https://help.ente.io/self-hosting/guides/web-app). diff --git a/mobile/ios/Podfile.lock b/mobile/ios/Podfile.lock index 951d1f2f365d78a03140d354be387b99e9ec96d9..558a279108a7f1909a453f703afae0ee85467d12 100644 --- a/mobile/ios/Podfile.lock +++ b/mobile/ios/Podfile.lock @@ -110,8 +110,6 @@ PODS: - FlutterMacOS - integration_test (0.0.1): - Flutter - - isar_flutter_libs (1.0.0): - - Flutter - libwebp (1.3.2): - libwebp/demux (= 1.3.2) - libwebp/mux (= 1.3.2) @@ -249,7 +247,6 @@ DEPENDENCIES: - image_editor_common (from `.symlinks/plugins/image_editor_common/ios`) - in_app_purchase_storekit (from `.symlinks/plugins/in_app_purchase_storekit/darwin`) - integration_test (from `.symlinks/plugins/integration_test/ios`) - - isar_flutter_libs (from `.symlinks/plugins/isar_flutter_libs/ios`) - local_auth_darwin (from `.symlinks/plugins/local_auth_darwin/darwin`) - local_auth_ios (from `.symlinks/plugins/local_auth_ios/ios`) - media_extension (from `.symlinks/plugins/media_extension/ios`) @@ -346,8 +343,6 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/in_app_purchase_storekit/darwin" integration_test: :path: ".symlinks/plugins/integration_test/ios" - isar_flutter_libs: - :path: ".symlinks/plugins/isar_flutter_libs/ios" local_auth_darwin: :path: ".symlinks/plugins/local_auth_darwin/darwin" local_auth_ios: @@ -433,7 +428,6 @@ SPEC CHECKSUMS: image_editor_common: d6f6644ae4a6de80481e89fe6d0a8c49e30b4b43 in_app_purchase_storekit: 0e4b3c2e43ba1e1281f4f46dd71b0593ce529892 integration_test: 13825b8a9334a850581300559b8839134b124670 - isar_flutter_libs: b69f437aeab9c521821c3f376198c4371fa21073 libwebp: 1786c9f4ff8a279e4dac1e8f385004d5fc253009 local_auth_darwin: c7e464000a6a89e952235699e32b329457608d98 local_auth_ios: 5046a18c018dd973247a0564496c8898dbb5adf9 diff --git a/mobile/ios/Runner.xcodeproj/project.pbxproj b/mobile/ios/Runner.xcodeproj/project.pbxproj index 3b38f2ac3a88f346740ec0e5dc3e6110e46168d9..22d5e8e6817865944e07e0f25f6c7363e56fb74b 100644 --- a/mobile/ios/Runner.xcodeproj/project.pbxproj +++ b/mobile/ios/Runner.xcodeproj/project.pbxproj @@ -309,7 +309,6 @@ "${BUILT_PRODUCTS_DIR}/image_editor_common/image_editor_common.framework", "${BUILT_PRODUCTS_DIR}/in_app_purchase_storekit/in_app_purchase_storekit.framework", "${BUILT_PRODUCTS_DIR}/integration_test/integration_test.framework", - "${BUILT_PRODUCTS_DIR}/isar_flutter_libs/isar_flutter_libs.framework", "${BUILT_PRODUCTS_DIR}/libwebp/libwebp.framework", "${BUILT_PRODUCTS_DIR}/local_auth_darwin/local_auth_darwin.framework", "${BUILT_PRODUCTS_DIR}/local_auth_ios/local_auth_ios.framework", @@ -392,7 +391,6 @@ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/image_editor_common.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/in_app_purchase_storekit.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/integration_test.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/isar_flutter_libs.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/libwebp.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/local_auth_darwin.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/local_auth_ios.framework", diff --git a/mobile/ios/Runner/Info.plist b/mobile/ios/Runner/Info.plist index c8e29cdc7d83006593753cdbac5460ec336bb00b..fe571afeb1d83af7604441066459258126c5d7d6 100644 --- a/mobile/ios/Runner/Info.plist +++ b/mobile/ios/Runner/Info.plist @@ -105,5 +105,14 @@ UIApplicationSupportsIndirectInputEvents + NSBonjourServices + + _googlecast._tcp + _F5BCEC64._googlecast._tcp + + + NSLocalNetworkUsageDescription + ${PRODUCT_NAME} uses the local network to discover Cast-enabled devices on your WiFi + network. diff --git a/mobile/lib/db/embeddings_db.dart b/mobile/lib/db/embeddings_db.dart index a339d4d0d2846b05e820bc39f8aecf78a26ddda5..0eb1d3f6d7e1c087a39079da12a7ff083b5159b6 100644 --- a/mobile/lib/db/embeddings_db.dart +++ b/mobile/lib/db/embeddings_db.dart @@ -1,79 +1,167 @@ import "dart:io"; +import "dart:typed_data"; -import "package:isar/isar.dart"; +import "package:path/path.dart"; import 'package:path_provider/path_provider.dart'; import "package:photos/core/event_bus.dart"; import "package:photos/events/embedding_updated_event.dart"; import "package:photos/models/embedding.dart"; +import "package:sqlite_async/sqlite_async.dart"; class EmbeddingsDB { - late final Isar _isar; - EmbeddingsDB._privateConstructor(); static final EmbeddingsDB instance = EmbeddingsDB._privateConstructor(); + static const databaseName = "ente.embeddings.db"; + static const tableName = "embeddings"; + static const columnFileID = "file_id"; + static const columnModel = "model"; + static const columnEmbedding = "embedding"; + static const columnUpdationTime = "updation_time"; + + static Future? _dbFuture; + + Future get _database async { + _dbFuture ??= _initDatabase(); + return _dbFuture!; + } + Future init() async { final dir = await getApplicationDocumentsDirectory(); - _isar = await Isar.open( - [EmbeddingSchema], - directory: dir.path, - ); - await _clearDeprecatedStore(dir); + await _clearDeprecatedStores(dir); + } + + Future _initDatabase() async { + final Directory documentsDirectory = + await getApplicationDocumentsDirectory(); + final String path = join(documentsDirectory.path, databaseName); + final migrations = SqliteMigrations() + ..add( + SqliteMigration( + 1, + (tx) async { + await tx.execute( + 'CREATE TABLE $tableName ($columnFileID INTEGER NOT NULL, $columnModel INTEGER NOT NULL, $columnEmbedding BLOB NOT NULL, $columnUpdationTime INTEGER, UNIQUE ($columnFileID, $columnModel))', + ); + }, + ), + ); + final database = SqliteDatabase(path: path); + await migrations.migrate(database); + return database; } Future clearTable() async { - await _isar.writeTxn(() => _isar.clear()); + final db = await _database; + await db.execute('DELETE * FROM $tableName'); } Future> getAll(Model model) async { - return _isar.embeddings.filter().modelEqualTo(model).findAll(); + final db = await _database; + final results = await db.getAll('SELECT * FROM $tableName'); + return _convertToEmbeddings(results); } - Future put(Embedding embedding) { - return _isar.writeTxn(() async { - await _isar.embeddings.putByIndex(Embedding.index, embedding); - Bus.instance.fire(EmbeddingUpdatedEvent()); - }); + Future put(Embedding embedding) async { + final db = await _database; + await db.execute( + 'INSERT OR REPLACE INTO $tableName ($columnFileID, $columnModel, $columnEmbedding, $columnUpdationTime) VALUES (?, ?, ?, ?)', + _getRowFromEmbedding(embedding), + ); + Bus.instance.fire(EmbeddingUpdatedEvent()); } - Future putMany(List embeddings) { - return _isar.writeTxn(() async { - await _isar.embeddings.putAllByIndex(Embedding.index, embeddings); - Bus.instance.fire(EmbeddingUpdatedEvent()); - }); + Future putMany(List embeddings) async { + final db = await _database; + final inputs = embeddings.map((e) => _getRowFromEmbedding(e)).toList(); + await db.executeBatch( + 'INSERT OR REPLACE INTO $tableName ($columnFileID, $columnModel, $columnEmbedding, $columnUpdationTime) values(?, ?, ?, ?)', + inputs, + ); + Bus.instance.fire(EmbeddingUpdatedEvent()); } Future> getUnsyncedEmbeddings() async { - return await _isar.embeddings.filter().updationTimeEqualTo(null).findAll(); + final db = await _database; + final results = await db.getAll( + 'SELECT * FROM $tableName WHERE $columnUpdationTime IS NULL', + ); + return _convertToEmbeddings(results); } Future deleteEmbeddings(List fileIDs) async { - await _isar.writeTxn(() async { - final embeddings = []; - for (final fileID in fileIDs) { - embeddings.addAll( - await _isar.embeddings.filter().fileIDEqualTo(fileID).findAll(), - ); - } - await _isar.embeddings.deleteAll(embeddings.map((e) => e.id).toList()); - Bus.instance.fire(EmbeddingUpdatedEvent()); - }); + final db = await _database; + await db.execute( + 'DELETE FROM $tableName WHERE $columnFileID IN (${fileIDs.join(", ")})', + ); + Bus.instance.fire(EmbeddingUpdatedEvent()); } Future deleteAllForModel(Model model) async { - await _isar.writeTxn(() async { - final embeddings = - await _isar.embeddings.filter().modelEqualTo(model).findAll(); - await _isar.embeddings.deleteAll(embeddings.map((e) => e.id).toList()); - Bus.instance.fire(EmbeddingUpdatedEvent()); - }); - } - - Future _clearDeprecatedStore(Directory dir) async { - final deprecatedStore = Directory(dir.path + "/object-box-store"); - if (await deprecatedStore.exists()) { - await deprecatedStore.delete(recursive: true); + final db = await _database; + await db.execute( + 'DELETE FROM $tableName WHERE $columnModel = ?', + [modelToInt(model)!], + ); + Bus.instance.fire(EmbeddingUpdatedEvent()); + } + + List _convertToEmbeddings(List> results) { + final List embeddings = []; + for (final result in results) { + embeddings.add(_getEmbeddingFromRow(result)); + } + return embeddings; + } + + Embedding _getEmbeddingFromRow(Map row) { + final fileID = row[columnFileID]; + final model = intToModel(row[columnModel])!; + final bytes = row[columnEmbedding] as Uint8List; + final list = Float32List.view(bytes.buffer); + return Embedding(fileID: fileID, model: model, embedding: list); + } + + List _getRowFromEmbedding(Embedding embedding) { + return [ + embedding.fileID, + modelToInt(embedding.model)!, + Float32List.fromList(embedding.embedding).buffer.asUint8List(), + embedding.updationTime, + ]; + } + + Future _clearDeprecatedStores(Directory dir) async { + final deprecatedObjectBox = Directory(dir.path + "/object-box-store"); + if (await deprecatedObjectBox.exists()) { + await deprecatedObjectBox.delete(recursive: true); + } + final deprecatedIsar = File(dir.path + "/default.isar"); + if (await deprecatedIsar.exists()) { + await deprecatedIsar.delete(); + } + } + + int? modelToInt(Model model) { + switch (model) { + case Model.onnxClip: + return 1; + case Model.ggmlClip: + return 2; + default: + return null; + } + } + + Model? intToModel(int model) { + switch (model) { + case 1: + return Model.onnxClip; + case 2: + return Model.ggmlClip; + default: + return null; } } } diff --git a/mobile/lib/db/files_db.dart b/mobile/lib/db/files_db.dart index 0926276e45464367d8d950b4a4bdf88877b11813..bfb12efd7f2bb39e4802dba6a664ecfcb84b179b 100644 --- a/mobile/lib/db/files_db.dart +++ b/mobile/lib/db/files_db.dart @@ -455,6 +455,7 @@ class FilesDB { } Future insert(EnteFile file) async { + _logger.info("Inserting $file"); final db = await instance.database; return db.insert( filesTable, diff --git a/mobile/lib/gateways/cast_gw.dart b/mobile/lib/gateways/cast_gw.dart index fb342c1a90c805eece823c687a237bababaed6fa..63735d6782e49042e669e0fbf6c2d9dc8d8bd52a 100644 --- a/mobile/lib/gateways/cast_gw.dart +++ b/mobile/lib/gateways/cast_gw.dart @@ -12,10 +12,14 @@ class CastGateway { ); return response.data["publicKey"]; } catch (e) { - if (e is DioError && - e.response != null && - e.response!.statusCode == 404) { - return null; + if (e is DioError && e.response != null) { + if (e.response!.statusCode == 404) { + return null; + } else if (e.response!.statusCode == 403) { + throw CastIPMismatchException(); + } else { + rethrow; + } } rethrow; } @@ -48,3 +52,7 @@ class CastGateway { } } } + +class CastIPMismatchException implements Exception { + CastIPMismatchException(); +} diff --git a/mobile/lib/generated/intl/messages_en.dart b/mobile/lib/generated/intl/messages_en.dart index cd28878530e790f843c53e57d3aea9f566b30213..82019277d0b354a04d4ff99ddf8d7b9219440154 100644 --- a/mobile/lib/generated/intl/messages_en.dart +++ b/mobile/lib/generated/intl/messages_en.dart @@ -358,6 +358,13 @@ class MessageLookup extends MessageLookupByLibrary { "Authentication failed, please try again"), "authenticationSuccessful": MessageLookupByLibrary.simpleMessage("Authentication successful!"), + "autoCastDialogBody": MessageLookupByLibrary.simpleMessage( + "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."), + "autoPair": MessageLookupByLibrary.simpleMessage("Auto pair"), + "autoPairDesc": MessageLookupByLibrary.simpleMessage( + "Auto pair works only with devices that support Chromecast."), "available": MessageLookupByLibrary.simpleMessage("Available"), "backedUpFolders": MessageLookupByLibrary.simpleMessage("Backed up folders"), @@ -388,6 +395,10 @@ class MessageLookup extends MessageLookupByLibrary { "cannotAddMorePhotosAfterBecomingViewer": m9, "cannotDeleteSharedFiles": MessageLookupByLibrary.simpleMessage("Cannot delete shared files"), + "castIPMismatchBody": MessageLookupByLibrary.simpleMessage( + "Please make sure you are on the same network as the TV."), + "castIPMismatchTitle": + MessageLookupByLibrary.simpleMessage("Failed to cast album"), "castInstruction": MessageLookupByLibrary.simpleMessage( "Visit cast.ente.io on the device you want to pair.\n\nEnter the code below to play the album on your TV."), "centerPoint": MessageLookupByLibrary.simpleMessage("Center point"), @@ -461,6 +472,8 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Confirm recovery key"), "confirmYourRecoveryKey": MessageLookupByLibrary.simpleMessage("Confirm your recovery key"), + "connectToDevice": + MessageLookupByLibrary.simpleMessage("Connect to device"), "contactFamilyAdmin": m12, "contactSupport": MessageLookupByLibrary.simpleMessage("Contact support"), @@ -724,6 +737,8 @@ class MessageLookup extends MessageLookupByLibrary { "filesBackedUpFromDevice": m22, "filesBackedUpInAlbum": m23, "filesDeleted": MessageLookupByLibrary.simpleMessage("Files deleted"), + "filesSavedToGallery": + MessageLookupByLibrary.simpleMessage("Files saved to gallery"), "findPeopleByName": MessageLookupByLibrary.simpleMessage( "Find people quickly by searching by name"), "flip": MessageLookupByLibrary.simpleMessage("Flip"), @@ -907,6 +922,8 @@ class MessageLookup extends MessageLookupByLibrary { "manageParticipants": MessageLookupByLibrary.simpleMessage("Manage"), "manageSubscription": MessageLookupByLibrary.simpleMessage("Manage subscription"), + "manualPairDesc": MessageLookupByLibrary.simpleMessage( + "Pair with PIN works with any screen you wish to view your album on."), "map": MessageLookupByLibrary.simpleMessage("Map"), "maps": MessageLookupByLibrary.simpleMessage("Maps"), "mastodon": MessageLookupByLibrary.simpleMessage("Mastodon"), @@ -941,6 +958,8 @@ class MessageLookup extends MessageLookupByLibrary { "no": MessageLookupByLibrary.simpleMessage("No"), "noAlbumsSharedByYouYet": MessageLookupByLibrary.simpleMessage("No albums shared by you yet"), + "noDeviceFound": + MessageLookupByLibrary.simpleMessage("No device found"), "noDeviceLimit": MessageLookupByLibrary.simpleMessage("None"), "noDeviceThatCanBeDeleted": MessageLookupByLibrary.simpleMessage( "You\'ve no files on this device that can be deleted"), @@ -987,6 +1006,9 @@ class MessageLookup extends MessageLookupByLibrary { "orPickAnExistingOne": MessageLookupByLibrary.simpleMessage("Or pick an existing one"), "pair": MessageLookupByLibrary.simpleMessage("Pair"), + "pairWithPin": MessageLookupByLibrary.simpleMessage("Pair with PIN"), + "pairingComplete": + MessageLookupByLibrary.simpleMessage("Pairing complete"), "passkey": MessageLookupByLibrary.simpleMessage("Passkey"), "passkeyAuthTitle": MessageLookupByLibrary.simpleMessage("Passkey verification"), @@ -1336,6 +1358,10 @@ class MessageLookup extends MessageLookupByLibrary { "sparkleSuccess": MessageLookupByLibrary.simpleMessage("✨ Success"), "startBackup": MessageLookupByLibrary.simpleMessage("Start backup"), "status": MessageLookupByLibrary.simpleMessage("Status"), + "stopCastingBody": MessageLookupByLibrary.simpleMessage( + "Do you want to stop casting?"), + "stopCastingTitle": + MessageLookupByLibrary.simpleMessage("Stop casting"), "storage": MessageLookupByLibrary.simpleMessage("Storage"), "storageBreakupFamily": MessageLookupByLibrary.simpleMessage("Family"), "storageBreakupYou": MessageLookupByLibrary.simpleMessage("You"), diff --git a/mobile/lib/generated/intl/messages_nl.dart b/mobile/lib/generated/intl/messages_nl.dart index 7b59771cd5671fae55a19e9be759f83098f68325..f6987973c33f1eed184b16b2d8c926c6d7bacb8a 100644 --- a/mobile/lib/generated/intl/messages_nl.dart +++ b/mobile/lib/generated/intl/messages_nl.dart @@ -368,6 +368,14 @@ class MessageLookup extends MessageLookupByLibrary { "Verificatie mislukt, probeer het opnieuw"), "authenticationSuccessful": MessageLookupByLibrary.simpleMessage("Verificatie geslaagd!"), + "autoCastDialogBody": MessageLookupByLibrary.simpleMessage( + "Je zult de beschikbare Cast apparaten hier zien."), + "autoCastiOSPermission": MessageLookupByLibrary.simpleMessage( + "Zorg ervoor dat lokale netwerkrechten zijn ingeschakeld voor de Ente Photos app, in Instellingen."), + "autoPair": + MessageLookupByLibrary.simpleMessage("Automatisch koppelen"), + "autoPairDesc": MessageLookupByLibrary.simpleMessage( + "Automatisch koppelen werkt alleen met apparaten die Chromecast ondersteunen."), "available": MessageLookupByLibrary.simpleMessage("Beschikbaar"), "backedUpFolders": MessageLookupByLibrary.simpleMessage("Back-up mappen"), @@ -399,6 +407,10 @@ class MessageLookup extends MessageLookupByLibrary { "cannotAddMorePhotosAfterBecomingViewer": m9, "cannotDeleteSharedFiles": MessageLookupByLibrary.simpleMessage( "Kan gedeelde bestanden niet verwijderen"), + "castIPMismatchBody": MessageLookupByLibrary.simpleMessage( + "Zorg ervoor dat je op hetzelfde netwerk zit als de tv."), + "castIPMismatchTitle": + MessageLookupByLibrary.simpleMessage("Album casten mislukt"), "castInstruction": MessageLookupByLibrary.simpleMessage( "Bezoek cast.ente.io op het apparaat dat u wilt koppelen.\n\nVoer de code hieronder in om het album op uw TV af te spelen."), "centerPoint": MessageLookupByLibrary.simpleMessage("Middelpunt"), @@ -473,6 +485,8 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Bevestig herstelsleutel"), "confirmYourRecoveryKey": MessageLookupByLibrary.simpleMessage("Bevestig herstelsleutel"), + "connectToDevice": MessageLookupByLibrary.simpleMessage( + "Verbinding maken met apparaat"), "contactFamilyAdmin": m12, "contactSupport": MessageLookupByLibrary.simpleMessage("Contacteer klantenservice"), @@ -685,8 +699,6 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Voer wachtwoord in"), "enterPasswordToEncrypt": MessageLookupByLibrary.simpleMessage( "Voer een wachtwoord in dat we kunnen gebruiken om je gegevens te versleutelen"), - "enterPersonName": - MessageLookupByLibrary.simpleMessage("Enter person name"), "enterReferralCode": MessageLookupByLibrary.simpleMessage("Voer verwijzingscode in"), "enterThe6digitCodeFromnyourAuthenticatorApp": @@ -752,6 +764,8 @@ class MessageLookup extends MessageLookupByLibrary { "filesBackedUpInAlbum": m23, "filesDeleted": MessageLookupByLibrary.simpleMessage("Bestanden verwijderd"), + "filesSavedToGallery": MessageLookupByLibrary.simpleMessage( + "Bestand opgeslagen in galerij"), "flip": MessageLookupByLibrary.simpleMessage("Omdraaien"), "forYourMemories": MessageLookupByLibrary.simpleMessage("voor uw herinneringen"), @@ -940,6 +954,8 @@ class MessageLookup extends MessageLookupByLibrary { "manageParticipants": MessageLookupByLibrary.simpleMessage("Beheren"), "manageSubscription": MessageLookupByLibrary.simpleMessage("Abonnement beheren"), + "manualPairDesc": MessageLookupByLibrary.simpleMessage( + "Koppelen met de PIN werkt met elk scherm waarop je jouw album wilt zien."), "map": MessageLookupByLibrary.simpleMessage("Kaart"), "maps": MessageLookupByLibrary.simpleMessage("Kaarten"), "mastodon": MessageLookupByLibrary.simpleMessage("Mastodon"), @@ -976,6 +992,8 @@ class MessageLookup extends MessageLookupByLibrary { "no": MessageLookupByLibrary.simpleMessage("Nee"), "noAlbumsSharedByYouYet": MessageLookupByLibrary.simpleMessage( "Nog geen albums gedeeld door jou"), + "noDeviceFound": + MessageLookupByLibrary.simpleMessage("Geen apparaat gevonden"), "noDeviceLimit": MessageLookupByLibrary.simpleMessage("Geen"), "noDeviceThatCanBeDeleted": MessageLookupByLibrary.simpleMessage( "Je hebt geen bestanden op dit apparaat die verwijderd kunnen worden"), @@ -1025,6 +1043,9 @@ class MessageLookup extends MessageLookupByLibrary { "orPickAnExistingOne": MessageLookupByLibrary.simpleMessage("Of kies een bestaande"), "pair": MessageLookupByLibrary.simpleMessage("Koppelen"), + "pairWithPin": MessageLookupByLibrary.simpleMessage("Koppelen met PIN"), + "pairingComplete": + MessageLookupByLibrary.simpleMessage("Koppeling voltooid"), "passkey": MessageLookupByLibrary.simpleMessage("Passkey"), "passkeyAuthTitle": MessageLookupByLibrary.simpleMessage("Passkey verificatie"), @@ -1179,8 +1200,6 @@ class MessageLookup extends MessageLookupByLibrary { "removeParticipant": MessageLookupByLibrary.simpleMessage("Deelnemer verwijderen"), "removeParticipantBody": m43, - "removePersonLabel": - MessageLookupByLibrary.simpleMessage("Remove person label"), "removePublicLink": MessageLookupByLibrary.simpleMessage("Verwijder publieke link"), "removeShareItemsWarning": MessageLookupByLibrary.simpleMessage( @@ -1387,6 +1406,10 @@ class MessageLookup extends MessageLookupByLibrary { "sparkleSuccess": MessageLookupByLibrary.simpleMessage("✨ Succes"), "startBackup": MessageLookupByLibrary.simpleMessage("Back-up starten"), "status": MessageLookupByLibrary.simpleMessage("Status"), + "stopCastingBody": + MessageLookupByLibrary.simpleMessage("Wil je stoppen met casten?"), + "stopCastingTitle": + MessageLookupByLibrary.simpleMessage("Casten stoppen"), "storage": MessageLookupByLibrary.simpleMessage("Opslagruimte"), "storageBreakupFamily": MessageLookupByLibrary.simpleMessage("Familie"), "storageBreakupYou": MessageLookupByLibrary.simpleMessage("Jij"), diff --git a/mobile/lib/generated/intl/messages_pt.dart b/mobile/lib/generated/intl/messages_pt.dart index 7064b1d616bc4b4e3aebef8030b9852ca5187d06..ef6dc5e54eabf5ac946623ebbd14181126623766 100644 --- a/mobile/lib/generated/intl/messages_pt.dart +++ b/mobile/lib/generated/intl/messages_pt.dart @@ -280,7 +280,7 @@ class MessageLookup extends MessageLookupByLibrary { "allowAddingPhotos": MessageLookupByLibrary.simpleMessage("Permitir adicionar fotos"), "allowDownloads": - MessageLookupByLibrary.simpleMessage("Permitir transferências"), + MessageLookupByLibrary.simpleMessage("Permitir downloads"), "allowPeopleToAddPhotos": MessageLookupByLibrary.simpleMessage( "Permitir que pessoas adicionem fotos"), "androidBiometricHint": @@ -311,7 +311,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Aplicar código"), "appstoreSubscription": MessageLookupByLibrary.simpleMessage("Assinatura da AppStore"), - "archive": MessageLookupByLibrary.simpleMessage("Arquivado"), + "archive": MessageLookupByLibrary.simpleMessage("Arquivar"), "archiveAlbum": MessageLookupByLibrary.simpleMessage("Arquivar álbum"), "archiving": MessageLookupByLibrary.simpleMessage("Arquivando..."), "areYouSureThatYouWantToLeaveTheFamily": @@ -365,6 +365,14 @@ class MessageLookup extends MessageLookupByLibrary { "Falha na autenticação. Por favor, tente novamente"), "authenticationSuccessful": MessageLookupByLibrary.simpleMessage("Autenticação bem-sucedida!"), + "autoCastDialogBody": MessageLookupByLibrary.simpleMessage( + "Você verá dispositivos disponíveis para transmitir aqui."), + "autoCastiOSPermission": MessageLookupByLibrary.simpleMessage( + "Certifique-se de que as permissões de Rede local estão ativadas para o aplicativo de Fotos Ente, em Configurações."), + "autoPair": + MessageLookupByLibrary.simpleMessage("Pareamento automático"), + "autoPairDesc": MessageLookupByLibrary.simpleMessage( + "O pareamento automático funciona apenas com dispositivos que suportam o Chromecast."), "available": MessageLookupByLibrary.simpleMessage("Disponível"), "backedUpFolders": MessageLookupByLibrary.simpleMessage("Backup de pastas concluído"), @@ -397,6 +405,10 @@ class MessageLookup extends MessageLookupByLibrary { "cannotAddMorePhotosAfterBecomingViewer": m9, "cannotDeleteSharedFiles": MessageLookupByLibrary.simpleMessage( "Não é possível excluir arquivos compartilhados"), + "castIPMismatchBody": MessageLookupByLibrary.simpleMessage( + "Certifique-se de estar na mesma rede que a TV."), + "castIPMismatchTitle": + MessageLookupByLibrary.simpleMessage("Falha ao transmitir álbum"), "castInstruction": MessageLookupByLibrary.simpleMessage( "Visite cast.ente.io no dispositivo que você deseja parear.\n\ndigite o código abaixo para reproduzir o álbum em sua TV."), "centerPoint": MessageLookupByLibrary.simpleMessage("Ponto central"), @@ -470,6 +482,8 @@ class MessageLookup extends MessageLookupByLibrary { "Confirme a chave de recuperação"), "confirmYourRecoveryKey": MessageLookupByLibrary.simpleMessage( "Confirme sua chave de recuperação"), + "connectToDevice": + MessageLookupByLibrary.simpleMessage("Conectar ao dispositivo"), "contactFamilyAdmin": m12, "contactSupport": MessageLookupByLibrary.simpleMessage("Contate o suporte"), @@ -551,7 +565,7 @@ class MessageLookup extends MessageLookupByLibrary { "deleteFromDevice": MessageLookupByLibrary.simpleMessage("Excluir do dispositivo"), "deleteFromEnte": - MessageLookupByLibrary.simpleMessage("Excluir do ente"), + MessageLookupByLibrary.simpleMessage("Excluir do Ente"), "deleteItemCount": m14, "deleteLocation": MessageLookupByLibrary.simpleMessage("Excluir Local"), "deletePhotos": MessageLookupByLibrary.simpleMessage("Excluir fotos"), @@ -667,7 +681,7 @@ class MessageLookup extends MessageLookupByLibrary { "enterCode": MessageLookupByLibrary.simpleMessage("Coloque o código"), "enterCodeDescription": MessageLookupByLibrary.simpleMessage( "Digite o código fornecido pelo seu amigo para reivindicar o armazenamento gratuito para vocês dois"), - "enterEmail": MessageLookupByLibrary.simpleMessage("Digite o email"), + "enterEmail": MessageLookupByLibrary.simpleMessage("Insira o e-mail"), "enterFileName": MessageLookupByLibrary.simpleMessage("Digite o nome do arquivo"), "enterNewPasswordToEncrypt": MessageLookupByLibrary.simpleMessage( @@ -675,8 +689,6 @@ class MessageLookup extends MessageLookupByLibrary { "enterPassword": MessageLookupByLibrary.simpleMessage("Digite a senha"), "enterPasswordToEncrypt": MessageLookupByLibrary.simpleMessage( "Insira a senha para criptografar seus dados"), - "enterPersonName": - MessageLookupByLibrary.simpleMessage("Enter person name"), "enterReferralCode": MessageLookupByLibrary.simpleMessage( "Insira o código de referência"), "enterThe6digitCodeFromnyourAuthenticatorApp": @@ -740,8 +752,8 @@ class MessageLookup extends MessageLookupByLibrary { "filesBackedUpInAlbum": m23, "filesDeleted": MessageLookupByLibrary.simpleMessage("Arquivos excluídos"), - "findPeopleByName": MessageLookupByLibrary.simpleMessage( - "Find people quickly by searching by name"), + "filesSavedToGallery": + MessageLookupByLibrary.simpleMessage("Arquivos salvos na galeria"), "flip": MessageLookupByLibrary.simpleMessage("Inverter"), "forYourMemories": MessageLookupByLibrary.simpleMessage("para suas memórias"), @@ -825,7 +837,7 @@ class MessageLookup extends MessageLookupByLibrary { "A chave de recuperação que você digitou não é válida. Certifique-se de que contém 24 palavras e verifique a ortografia de cada uma.\n\nSe você inseriu um código de recuperação mais antigo, verifique se ele tem 64 caracteres e verifique cada um deles."), "invite": MessageLookupByLibrary.simpleMessage("Convidar"), "inviteToEnte": - MessageLookupByLibrary.simpleMessage("Convidar para o ente"), + MessageLookupByLibrary.simpleMessage("Convidar para o Ente"), "inviteYourFriends": MessageLookupByLibrary.simpleMessage("Convide seus amigos"), "inviteYourFriendsToEnte": @@ -933,6 +945,8 @@ class MessageLookup extends MessageLookupByLibrary { "manageParticipants": MessageLookupByLibrary.simpleMessage("Gerenciar"), "manageSubscription": MessageLookupByLibrary.simpleMessage("Gerenciar assinatura"), + "manualPairDesc": MessageLookupByLibrary.simpleMessage( + "Parear com o PIN funciona com qualquer tela que você deseja ver o seu álbum ativado."), "map": MessageLookupByLibrary.simpleMessage("Mapa"), "maps": MessageLookupByLibrary.simpleMessage("Mapas"), "mastodon": MessageLookupByLibrary.simpleMessage("Mastodon"), @@ -968,6 +982,8 @@ class MessageLookup extends MessageLookupByLibrary { "no": MessageLookupByLibrary.simpleMessage("Não"), "noAlbumsSharedByYouYet": MessageLookupByLibrary.simpleMessage( "Nenhum álbum compartilhado por você ainda"), + "noDeviceFound": MessageLookupByLibrary.simpleMessage( + "Nenhum dispositivo encontrado"), "noDeviceLimit": MessageLookupByLibrary.simpleMessage("Nenhum"), "noDeviceThatCanBeDeleted": MessageLookupByLibrary.simpleMessage( "Você não tem nenhum arquivo neste dispositivo que pode ser excluído"), @@ -1016,6 +1032,9 @@ class MessageLookup extends MessageLookupByLibrary { "orPickAnExistingOne": MessageLookupByLibrary.simpleMessage("Ou escolha um existente"), "pair": MessageLookupByLibrary.simpleMessage("Parear"), + "pairWithPin": MessageLookupByLibrary.simpleMessage("Parear com PIN"), + "pairingComplete": + MessageLookupByLibrary.simpleMessage("Pareamento concluído"), "passkey": MessageLookupByLibrary.simpleMessage("Chave de acesso"), "passkeyAuthTitle": MessageLookupByLibrary.simpleMessage( "Autenticação via Chave de acesso"), @@ -1170,8 +1189,6 @@ class MessageLookup extends MessageLookupByLibrary { "removeParticipant": MessageLookupByLibrary.simpleMessage("Remover participante"), "removeParticipantBody": m43, - "removePersonLabel": - MessageLookupByLibrary.simpleMessage("Remove person label"), "removePublicLink": MessageLookupByLibrary.simpleMessage("Remover link público"), "removeShareItemsWarning": MessageLookupByLibrary.simpleMessage( @@ -1386,6 +1403,10 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("✨ Bem-sucedido"), "startBackup": MessageLookupByLibrary.simpleMessage("Iniciar backup"), "status": MessageLookupByLibrary.simpleMessage("Estado"), + "stopCastingBody": MessageLookupByLibrary.simpleMessage( + "Você quer parar a transmissão?"), + "stopCastingTitle": + MessageLookupByLibrary.simpleMessage("Parar transmissão"), "storage": MessageLookupByLibrary.simpleMessage("Armazenamento"), "storageBreakupFamily": MessageLookupByLibrary.simpleMessage("Família"), "storageBreakupYou": MessageLookupByLibrary.simpleMessage("Você"), @@ -1431,7 +1452,7 @@ class MessageLookup extends MessageLookupByLibrary { "thankYouForSubscribing": MessageLookupByLibrary.simpleMessage("Obrigado por assinar!"), "theDownloadCouldNotBeCompleted": MessageLookupByLibrary.simpleMessage( - "Não foi possível concluir a transferência"), + "Não foi possível concluir o download"), "theRecoveryKeyYouEnteredIsIncorrect": MessageLookupByLibrary.simpleMessage( "A chave de recuperação inserida está incorreta"), @@ -1531,7 +1552,7 @@ class MessageLookup extends MessageLookupByLibrary { "verificationId": MessageLookupByLibrary.simpleMessage("ID de Verificação"), "verify": MessageLookupByLibrary.simpleMessage("Verificar"), - "verifyEmail": MessageLookupByLibrary.simpleMessage("Verificar email"), + "verifyEmail": MessageLookupByLibrary.simpleMessage("Verificar e-mail"), "verifyEmailID": m65, "verifyIDLabel": MessageLookupByLibrary.simpleMessage("Verificar"), "verifyPasskey": diff --git a/mobile/lib/generated/intl/messages_zh.dart b/mobile/lib/generated/intl/messages_zh.dart index 0a13bacaa02241cb22575490cee3a859080c5a53..80cc1356970d343895a8451e06ff312588c5c0f3 100644 --- a/mobile/lib/generated/intl/messages_zh.dart +++ b/mobile/lib/generated/intl/messages_zh.dart @@ -320,6 +320,13 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("身份验证失败,请重试"), "authenticationSuccessful": MessageLookupByLibrary.simpleMessage("验证成功"), + "autoCastDialogBody": + MessageLookupByLibrary.simpleMessage("您将在此处看到可用的 Cast 设备。"), + "autoCastiOSPermission": MessageLookupByLibrary.simpleMessage( + "请确保已在“设置”中为 Ente Photos 应用打开本地网络权限。"), + "autoPair": MessageLookupByLibrary.simpleMessage("自动配对"), + "autoPairDesc": + MessageLookupByLibrary.simpleMessage("自动配对仅适用于支持 Chromecast 的设备。"), "available": MessageLookupByLibrary.simpleMessage("可用"), "backedUpFolders": MessageLookupByLibrary.simpleMessage("已备份的文件夹"), "backup": MessageLookupByLibrary.simpleMessage("备份"), @@ -344,6 +351,9 @@ class MessageLookup extends MessageLookupByLibrary { "cannotAddMorePhotosAfterBecomingViewer": m9, "cannotDeleteSharedFiles": MessageLookupByLibrary.simpleMessage("无法删除共享文件"), + "castIPMismatchBody": + MessageLookupByLibrary.simpleMessage("请确保您的设备与电视处于同一网络。"), + "castIPMismatchTitle": MessageLookupByLibrary.simpleMessage("投放相册失败"), "castInstruction": MessageLookupByLibrary.simpleMessage( "在您要配对的设备上访问 cast.ente.io。\n输入下面的代码即可在电视上播放相册。"), "centerPoint": MessageLookupByLibrary.simpleMessage("中心点"), @@ -400,6 +410,7 @@ class MessageLookup extends MessageLookupByLibrary { "confirmRecoveryKey": MessageLookupByLibrary.simpleMessage("确认恢复密钥"), "confirmYourRecoveryKey": MessageLookupByLibrary.simpleMessage("确认您的恢复密钥"), + "connectToDevice": MessageLookupByLibrary.simpleMessage("连接到设备"), "contactFamilyAdmin": m12, "contactSupport": MessageLookupByLibrary.simpleMessage("联系支持"), "contactToManageSubscription": m13, @@ -563,8 +574,6 @@ class MessageLookup extends MessageLookupByLibrary { "enterPassword": MessageLookupByLibrary.simpleMessage("输入密码"), "enterPasswordToEncrypt": MessageLookupByLibrary.simpleMessage("输入我们可以用来加密您的数据的密码"), - "enterPersonName": - MessageLookupByLibrary.simpleMessage("Enter person name"), "enterReferralCode": MessageLookupByLibrary.simpleMessage("输入推荐代码"), "enterThe6digitCodeFromnyourAuthenticatorApp": MessageLookupByLibrary.simpleMessage("从你的身份验证器应用中\n输入6位数字代码"), @@ -612,6 +621,8 @@ class MessageLookup extends MessageLookupByLibrary { "filesBackedUpFromDevice": m22, "filesBackedUpInAlbum": m23, "filesDeleted": MessageLookupByLibrary.simpleMessage("文件已删除"), + "filesSavedToGallery": + MessageLookupByLibrary.simpleMessage("多个文件已保存到相册"), "flip": MessageLookupByLibrary.simpleMessage("上下翻转"), "forYourMemories": MessageLookupByLibrary.simpleMessage("为您的回忆"), "forgotPassword": MessageLookupByLibrary.simpleMessage("忘记密码"), @@ -767,6 +778,8 @@ class MessageLookup extends MessageLookupByLibrary { "manageLink": MessageLookupByLibrary.simpleMessage("管理链接"), "manageParticipants": MessageLookupByLibrary.simpleMessage("管理"), "manageSubscription": MessageLookupByLibrary.simpleMessage("管理订阅"), + "manualPairDesc": MessageLookupByLibrary.simpleMessage( + "用 PIN 码配对适用于您希望在其上查看相册的任何屏幕。"), "map": MessageLookupByLibrary.simpleMessage("地图"), "maps": MessageLookupByLibrary.simpleMessage("地图"), "mastodon": MessageLookupByLibrary.simpleMessage("Mastodon"), @@ -799,6 +812,7 @@ class MessageLookup extends MessageLookupByLibrary { "no": MessageLookupByLibrary.simpleMessage("否"), "noAlbumsSharedByYouYet": MessageLookupByLibrary.simpleMessage("您尚未共享任何相册"), + "noDeviceFound": MessageLookupByLibrary.simpleMessage("未发现设备"), "noDeviceLimit": MessageLookupByLibrary.simpleMessage("无"), "noDeviceThatCanBeDeleted": MessageLookupByLibrary.simpleMessage("您在此设备上没有可被删除的文件"), @@ -839,6 +853,8 @@ class MessageLookup extends MessageLookupByLibrary { "orPickAnExistingOne": MessageLookupByLibrary.simpleMessage("或者选择一个现有的"), "pair": MessageLookupByLibrary.simpleMessage("配对"), + "pairWithPin": MessageLookupByLibrary.simpleMessage("用 PIN 配对"), + "pairingComplete": MessageLookupByLibrary.simpleMessage("配对完成"), "passkey": MessageLookupByLibrary.simpleMessage("通行密钥"), "passkeyAuthTitle": MessageLookupByLibrary.simpleMessage("通行密钥认证"), "password": MessageLookupByLibrary.simpleMessage("密码"), @@ -954,8 +970,6 @@ class MessageLookup extends MessageLookupByLibrary { "removeLink": MessageLookupByLibrary.simpleMessage("移除链接"), "removeParticipant": MessageLookupByLibrary.simpleMessage("移除参与者"), "removeParticipantBody": m43, - "removePersonLabel": - MessageLookupByLibrary.simpleMessage("Remove person label"), "removePublicLink": MessageLookupByLibrary.simpleMessage("删除公开链接"), "removeShareItemsWarning": MessageLookupByLibrary.simpleMessage("您要删除的某些项目是由其他人添加的,您将无法访问它们"), @@ -1119,6 +1133,8 @@ class MessageLookup extends MessageLookupByLibrary { "sparkleSuccess": MessageLookupByLibrary.simpleMessage("✨ 成功"), "startBackup": MessageLookupByLibrary.simpleMessage("开始备份"), "status": MessageLookupByLibrary.simpleMessage("状态"), + "stopCastingBody": MessageLookupByLibrary.simpleMessage("您想停止投放吗?"), + "stopCastingTitle": MessageLookupByLibrary.simpleMessage("停止投放"), "storage": MessageLookupByLibrary.simpleMessage("存储空间"), "storageBreakupFamily": MessageLookupByLibrary.simpleMessage("家庭"), "storageBreakupYou": MessageLookupByLibrary.simpleMessage("您"), diff --git a/mobile/lib/generated/l10n.dart b/mobile/lib/generated/l10n.dart index f7a5295e074bdea7c0acf7c67b0c81d452cd99ec..4b86a18103a364dcba80b501ff435be853cd384e 100644 --- a/mobile/lib/generated/l10n.dart +++ b/mobile/lib/generated/l10n.dart @@ -5945,6 +5945,16 @@ class S { ); } + /// `Files saved to gallery` + String get filesSavedToGallery { + return Intl.message( + 'Files saved to gallery', + name: 'filesSavedToGallery', + desc: '', + args: [], + ); + } + /// `Failed to save file to gallery` String get fileFailedToSaveToGallery { return Intl.message( @@ -8613,6 +8623,136 @@ class S { args: [], ); } + + /// `Auto pair works only with devices that support Chromecast.` + String get autoPairDesc { + return Intl.message( + 'Auto pair works only with devices that support Chromecast.', + name: 'autoPairDesc', + desc: '', + args: [], + ); + } + + /// `Pair with PIN works with any screen you wish to view your album on.` + String get manualPairDesc { + return Intl.message( + 'Pair with PIN works with any screen you wish to view your album on.', + name: 'manualPairDesc', + desc: '', + args: [], + ); + } + + /// `Connect to device` + String get connectToDevice { + return Intl.message( + 'Connect to device', + name: 'connectToDevice', + desc: '', + args: [], + ); + } + + /// `You'll see available Cast devices here.` + String get autoCastDialogBody { + return Intl.message( + 'You\'ll see available Cast devices here.', + name: 'autoCastDialogBody', + desc: '', + args: [], + ); + } + + /// `Make sure Local Network permissions are turned on for the Ente Photos app, in Settings.` + String get autoCastiOSPermission { + return Intl.message( + 'Make sure Local Network permissions are turned on for the Ente Photos app, in Settings.', + name: 'autoCastiOSPermission', + desc: '', + args: [], + ); + } + + /// `No device found` + String get noDeviceFound { + return Intl.message( + 'No device found', + name: 'noDeviceFound', + desc: '', + args: [], + ); + } + + /// `Stop casting` + String get stopCastingTitle { + return Intl.message( + 'Stop casting', + name: 'stopCastingTitle', + desc: '', + args: [], + ); + } + + /// `Do you want to stop casting?` + String get stopCastingBody { + return Intl.message( + 'Do you want to stop casting?', + name: 'stopCastingBody', + desc: '', + args: [], + ); + } + + /// `Failed to cast album` + String get castIPMismatchTitle { + return Intl.message( + 'Failed to cast album', + name: 'castIPMismatchTitle', + desc: '', + args: [], + ); + } + + /// `Please make sure you are on the same network as the TV.` + String get castIPMismatchBody { + return Intl.message( + 'Please make sure you are on the same network as the TV.', + name: 'castIPMismatchBody', + desc: '', + args: [], + ); + } + + /// `Pairing complete` + String get pairingComplete { + return Intl.message( + 'Pairing complete', + name: 'pairingComplete', + desc: '', + args: [], + ); + } + + /// `Auto pair` + String get autoPair { + return Intl.message( + 'Auto pair', + name: 'autoPair', + desc: '', + args: [], + ); + } + + /// `Pair with PIN` + String get pairWithPin { + return Intl.message( + 'Pair with PIN', + name: 'pairWithPin', + desc: '', + args: [], + ); + } } class AppLocalizationDelegate extends LocalizationsDelegate { diff --git a/mobile/lib/l10n/intl_en.arb b/mobile/lib/l10n/intl_en.arb index 5689682c3fda2eabbd9d3fd7f85bb764e8f1e74f..498c6cf3f19eb182af76f251fca0717e25167be1 100644 --- a/mobile/lib/l10n/intl_en.arb +++ b/mobile/lib/l10n/intl_en.arb @@ -835,6 +835,7 @@ "close": "Close", "setAs": "Set as", "fileSavedToGallery": "File saved to gallery", + "filesSavedToGallery": "Files saved to gallery", "fileFailedToSaveToGallery": "Failed to save file to gallery", "download": "Download", "pressAndHoldToPlayVideo": "Press and hold to play video", @@ -1217,5 +1218,18 @@ "createCollaborativeLink": "Create collaborative link", "search": "Search", "enterPersonName": "Enter person name", - "removePersonLabel": "Remove person label" + "removePersonLabel": "Remove person label", + "autoPairDesc": "Auto pair works only with devices that support Chromecast.", + "manualPairDesc": "Pair with PIN works with any screen you wish to view your album on.", + "connectToDevice": "Connect to device", + "autoCastDialogBody": "You'll see available Cast devices here.", + "autoCastiOSPermission": "Make sure Local Network permissions are turned on for the Ente Photos app, in Settings.", + "noDeviceFound": "No device found", + "stopCastingTitle": "Stop casting", + "stopCastingBody": "Do you want to stop casting?", + "castIPMismatchTitle": "Failed to cast album", + "castIPMismatchBody": "Please make sure you are on the same network as the TV.", + "pairingComplete": "Pairing complete", + "autoPair": "Auto pair", + "pairWithPin": "Pair with PIN" } \ No newline at end of file diff --git a/mobile/lib/l10n/intl_nl.arb b/mobile/lib/l10n/intl_nl.arb index fdcee85f3957657227514472a92ed9105ddd9524..a8f854a4300ec0159e1da67e3f388efba8415eb7 100644 --- a/mobile/lib/l10n/intl_nl.arb +++ b/mobile/lib/l10n/intl_nl.arb @@ -835,6 +835,7 @@ "close": "Sluiten", "setAs": "Instellen als", "fileSavedToGallery": "Bestand opgeslagen in galerij", + "filesSavedToGallery": "Bestand opgeslagen in galerij", "fileFailedToSaveToGallery": "Opslaan van bestand naar galerij mislukt", "download": "Downloaden", "pressAndHoldToPlayVideo": "Ingedrukt houden om video af te spelen", @@ -1195,6 +1196,8 @@ "verifyPasskey": "Bevestig passkey", "playOnTv": "Album afspelen op TV", "pair": "Koppelen", + "autoPair": "Automatisch koppelen", + "pairWithPin": "Koppelen met PIN", "deviceNotFound": "Apparaat niet gevonden", "castInstruction": "Bezoek cast.ente.io op het apparaat dat u wilt koppelen.\n\nVoer de code hieronder in om het album op uw TV af te spelen.", "deviceCodeHint": "Voer de code in", @@ -1213,6 +1216,15 @@ "customEndpoint": "Verbonden met {endpoint}", "createCollaborativeLink": "Maak een gezamenlijke link", "search": "Zoeken", - "enterPersonName": "Enter person name", - "removePersonLabel": "Remove person label" + "autoPairDesc": "Automatisch koppelen werkt alleen met apparaten die Chromecast ondersteunen.", + "manualPairDesc": "Koppelen met de PIN werkt met elk scherm waarop je jouw album wilt zien.", + "connectToDevice": "Verbinding maken met apparaat", + "autoCastDialogBody": "Je zult de beschikbare Cast apparaten hier zien.", + "autoCastiOSPermission": "Zorg ervoor dat lokale netwerkrechten zijn ingeschakeld voor de Ente Photos app, in Instellingen.", + "noDeviceFound": "Geen apparaat gevonden", + "stopCastingTitle": "Casten stoppen", + "stopCastingBody": "Wil je stoppen met casten?", + "castIPMismatchTitle": "Album casten mislukt", + "castIPMismatchBody": "Zorg ervoor dat je op hetzelfde netwerk zit als de tv.", + "pairingComplete": "Koppeling voltooid" } \ No newline at end of file diff --git a/mobile/lib/l10n/intl_pt.arb b/mobile/lib/l10n/intl_pt.arb index c45bdc05350952fc7d3cd4ae974e07d247d6ab7f..08d932cdaa553a81346877a161150b9f05697faa 100644 --- a/mobile/lib/l10n/intl_pt.arb +++ b/mobile/lib/l10n/intl_pt.arb @@ -47,7 +47,7 @@ "noRecoveryKey": "Nenhuma chave de recuperação?", "sorry": "Desculpe", "noRecoveryKeyNoDecryption": "Devido à natureza do nosso protocolo de criptografia de ponta a ponta, seus dados não podem ser descriptografados sem sua senha ou chave de recuperação", - "verifyEmail": "Verificar email", + "verifyEmail": "Verificar e-mail", "toResetVerifyEmail": "Para redefinir a sua senha, por favor verifique o seu email primeiro.", "checkInboxAndSpamFolder": "Verifique sua caixa de entrada (e ‘spam’) para concluir a verificação", "tapToEnterCode": "Toque para inserir código", @@ -156,7 +156,7 @@ "addANewEmail": "Adicionar um novo email", "orPickAnExistingOne": "Ou escolha um existente", "collaboratorsCanAddPhotosAndVideosToTheSharedAlbum": "Os colaboradores podem adicionar fotos e vídeos ao álbum compartilhado.", - "enterEmail": "Digite o email", + "enterEmail": "Insira o e-mail", "albumOwner": "Proprietário", "@albumOwner": { "description": "Role of the album owner" @@ -186,7 +186,7 @@ "passwordLock": "Bloqueio de senha", "disableDownloadWarningTitle": "Observe", "disableDownloadWarningBody": "Os espectadores ainda podem tirar screenshots ou salvar uma cópia de suas fotos usando ferramentas externas", - "allowDownloads": "Permitir transferências", + "allowDownloads": "Permitir downloads", "linkDeviceLimit": "Limite do dispositivo", "noDeviceLimit": "Nenhum", "@noDeviceLimit": { @@ -334,12 +334,12 @@ "removeParticipantBody": "{userEmail} será removido deste álbum compartilhado\n\nQuaisquer fotos adicionadas por eles também serão removidas do álbum", "keepPhotos": "Manter fotos", "deletePhotos": "Excluir fotos", - "inviteToEnte": "Convidar para o ente", + "inviteToEnte": "Convidar para o Ente", "removePublicLink": "Remover link público", "disableLinkMessage": "Isso removerá o link público para acessar \"{albumName}\".", "sharing": "Compartilhando...", "youCannotShareWithYourself": "Você não pode compartilhar consigo mesmo", - "archive": "Arquivado", + "archive": "Arquivar", "createAlbumActionHint": "Pressione e segure para selecionar fotos e clique em + para criar um álbum", "importing": "Importando....", "failedToLoadAlbums": "Falha ao carregar álbuns", @@ -353,7 +353,7 @@ "singleFileInBothLocalAndRemote": "Este {fileType} está tanto no Ente quanto no seu dispositivo.", "singleFileInRemoteOnly": "Este {fileType} será excluído do Ente.", "singleFileDeleteFromDevice": "Este {fileType} será excluído do seu dispositivo.", - "deleteFromEnte": "Excluir do ente", + "deleteFromEnte": "Excluir do Ente", "yesDelete": "Sim, excluir", "movedToTrash": "Movido para a lixeira", "deleteFromDevice": "Excluir do dispositivo", @@ -473,7 +473,7 @@ "ignoreUpdate": "Ignorar", "downloading": "Baixando...", "cannotDeleteSharedFiles": "Não é possível excluir arquivos compartilhados", - "theDownloadCouldNotBeCompleted": "Não foi possível concluir a transferência", + "theDownloadCouldNotBeCompleted": "Não foi possível concluir o download", "retry": "Tentar novamente", "backedUpFolders": "Backup de pastas concluído", "backup": "Backup", @@ -835,6 +835,7 @@ "close": "Fechar", "setAs": "Definir como", "fileSavedToGallery": "Vídeo salvo na galeria", + "filesSavedToGallery": "Arquivos salvos na galeria", "fileFailedToSaveToGallery": "Falha ao salvar o arquivo na galeria", "download": "Baixar", "pressAndHoldToPlayVideo": "Pressione e segure para reproduzir o vídeo", @@ -1195,11 +1196,12 @@ "verifyPasskey": "Verificar chave de acesso", "playOnTv": "Reproduzir álbum na TV", "pair": "Parear", + "autoPair": "Pareamento automático", + "pairWithPin": "Parear com PIN", "deviceNotFound": "Dispositivo não encontrado", "castInstruction": "Visite cast.ente.io no dispositivo que você deseja parear.\n\ndigite o código abaixo para reproduzir o álbum em sua TV.", "deviceCodeHint": "Insira o código", "joinDiscord": "Junte-se ao Discord", - "findPeopleByName": "Find people quickly by searching by name", "locations": "Locais", "descriptions": "Descrições", "addViewers": "{count, plural, zero {Adicionar visualizador} one {Adicionar visualizador} other {Adicionar Visualizadores}}", @@ -1214,6 +1216,15 @@ "customEndpoint": "Conectado a {endpoint}", "createCollaborativeLink": "Criar link colaborativo", "search": "Pesquisar", - "enterPersonName": "Enter person name", - "removePersonLabel": "Remove person label" + "autoPairDesc": "O pareamento automático funciona apenas com dispositivos que suportam o Chromecast.", + "manualPairDesc": "Parear com o PIN funciona com qualquer tela que você deseja ver o seu álbum ativado.", + "connectToDevice": "Conectar ao dispositivo", + "autoCastDialogBody": "Você verá dispositivos disponíveis para transmitir aqui.", + "autoCastiOSPermission": "Certifique-se de que as permissões de Rede local estão ativadas para o aplicativo de Fotos Ente, em Configurações.", + "noDeviceFound": "Nenhum dispositivo encontrado", + "stopCastingTitle": "Parar transmissão", + "stopCastingBody": "Você quer parar a transmissão?", + "castIPMismatchTitle": "Falha ao transmitir álbum", + "castIPMismatchBody": "Certifique-se de estar na mesma rede que a TV.", + "pairingComplete": "Pareamento concluído" } \ No newline at end of file diff --git a/mobile/lib/l10n/intl_zh.arb b/mobile/lib/l10n/intl_zh.arb index ad4b31bfc0e0317ddb74bbf2b59d7310c3d568f9..9a854a4f0fa738f4a6b513ba9d28d50c739c8097 100644 --- a/mobile/lib/l10n/intl_zh.arb +++ b/mobile/lib/l10n/intl_zh.arb @@ -835,6 +835,7 @@ "close": "关闭", "setAs": "设置为", "fileSavedToGallery": "文件已保存到相册", + "filesSavedToGallery": "多个文件已保存到相册", "fileFailedToSaveToGallery": "无法将文件保存到相册", "download": "下载", "pressAndHoldToPlayVideo": "按住以播放视频", @@ -1195,6 +1196,8 @@ "verifyPasskey": "验证通行密钥", "playOnTv": "在电视上播放相册", "pair": "配对", + "autoPair": "自动配对", + "pairWithPin": "用 PIN 配对", "deviceNotFound": "未发现设备", "castInstruction": "在您要配对的设备上访问 cast.ente.io。\n输入下面的代码即可在电视上播放相册。", "deviceCodeHint": "输入代码", @@ -1213,6 +1216,15 @@ "customEndpoint": "已连接至 {endpoint}", "createCollaborativeLink": "创建协作链接", "search": "搜索", - "enterPersonName": "Enter person name", - "removePersonLabel": "Remove person label" + "autoPairDesc": "自动配对仅适用于支持 Chromecast 的设备。", + "manualPairDesc": "用 PIN 码配对适用于您希望在其上查看相册的任何屏幕。", + "connectToDevice": "连接到设备", + "autoCastDialogBody": "您将在此处看到可用的 Cast 设备。", + "autoCastiOSPermission": "请确保已在“设置”中为 Ente Photos 应用打开本地网络权限。", + "noDeviceFound": "未发现设备", + "stopCastingTitle": "停止投放", + "stopCastingBody": "您想停止投放吗?", + "castIPMismatchTitle": "投放相册失败", + "castIPMismatchBody": "请确保您的设备与电视处于同一网络。", + "pairingComplete": "配对完成" } \ No newline at end of file diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart index 9507090ffdad1141bfa659c5857a830afd7c8fe8..fb78547fa86f45f1af771c6219117ee6d8e4b4ca 100644 --- a/mobile/lib/main.dart +++ b/mobile/lib/main.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:io'; +import "dart:isolate"; import "package:adaptive_theme/adaptive_theme.dart"; import 'package:background_fetch/background_fetch.dart'; @@ -338,10 +339,15 @@ Future _killBGTask([String? taskId]) async { DateTime.now().microsecondsSinceEpoch, ); final prefs = await SharedPreferences.getInstance(); + await prefs.remove(kLastBGTaskHeartBeatTime); if (taskId != null) { BackgroundFetch.finish(taskId); } + + ///Band aid for background process not getting killed. Should migrate to using + ///workmanager instead of background_fetch. + Isolate.current.kill(); } Future _firebaseMessagingBackgroundHandler(RemoteMessage message) async { diff --git a/mobile/lib/models/embedding.dart b/mobile/lib/models/embedding.dart index 1f78687b91cf06c035169b26ac02241599eadbc7..c8f742caa933c2a4d2837480e45614cd510f3d0f 100644 --- a/mobile/lib/models/embedding.dart +++ b/mobile/lib/models/embedding.dart @@ -1,17 +1,7 @@ import "dart:convert"; -import "package:isar/isar.dart"; - -part 'embedding.g.dart'; - -@collection class Embedding { - static const index = 'unique_file_model_embedding'; - - Id id = Isar.autoIncrement; final int fileID; - @enumerated - @Index(name: index, composite: [CompositeIndex('fileID')], unique: true, replace: true) final Model model; final List embedding; int? updationTime; diff --git a/mobile/lib/models/embedding.g.dart b/mobile/lib/models/embedding.g.dart deleted file mode 100644 index ca041a0d0a424e59a0f9b8a0fb0da0e18b511659..0000000000000000000000000000000000000000 --- a/mobile/lib/models/embedding.g.dart +++ /dev/null @@ -1,1059 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'embedding.dart'; - -// ************************************************************************** -// IsarCollectionGenerator -// ************************************************************************** - -// coverage:ignore-file -// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types - -extension GetEmbeddingCollection on Isar { - IsarCollection get embeddings => this.collection(); -} - -const EmbeddingSchema = CollectionSchema( - name: r'Embedding', - id: -8064100183150254587, - properties: { - r'embedding': PropertySchema( - id: 0, - name: r'embedding', - type: IsarType.doubleList, - ), - r'fileID': PropertySchema( - id: 1, - name: r'fileID', - type: IsarType.long, - ), - r'model': PropertySchema( - id: 2, - name: r'model', - type: IsarType.byte, - enumMap: _EmbeddingmodelEnumValueMap, - ), - r'updationTime': PropertySchema( - id: 3, - name: r'updationTime', - type: IsarType.long, - ) - }, - estimateSize: _embeddingEstimateSize, - serialize: _embeddingSerialize, - deserialize: _embeddingDeserialize, - deserializeProp: _embeddingDeserializeProp, - idName: r'id', - indexes: { - r'unique_file_model_embedding': IndexSchema( - id: 6248303800853228628, - name: r'unique_file_model_embedding', - unique: true, - replace: true, - properties: [ - IndexPropertySchema( - name: r'model', - type: IndexType.value, - caseSensitive: false, - ), - IndexPropertySchema( - name: r'fileID', - type: IndexType.value, - caseSensitive: false, - ) - ], - ) - }, - links: {}, - embeddedSchemas: {}, - getId: _embeddingGetId, - getLinks: _embeddingGetLinks, - attach: _embeddingAttach, - version: '3.1.0+1', -); - -int _embeddingEstimateSize( - Embedding object, - List offsets, - Map> allOffsets, -) { - var bytesCount = offsets.last; - bytesCount += 3 + object.embedding.length * 8; - return bytesCount; -} - -void _embeddingSerialize( - Embedding object, - IsarWriter writer, - List offsets, - Map> allOffsets, -) { - writer.writeDoubleList(offsets[0], object.embedding); - writer.writeLong(offsets[1], object.fileID); - writer.writeByte(offsets[2], object.model.index); - writer.writeLong(offsets[3], object.updationTime); -} - -Embedding _embeddingDeserialize( - Id id, - IsarReader reader, - List offsets, - Map> allOffsets, -) { - final object = Embedding( - embedding: reader.readDoubleList(offsets[0]) ?? [], - fileID: reader.readLong(offsets[1]), - model: _EmbeddingmodelValueEnumMap[reader.readByteOrNull(offsets[2])] ?? - Model.onnxClip, - updationTime: reader.readLongOrNull(offsets[3]), - ); - object.id = id; - return object; -} - -P _embeddingDeserializeProp

( - IsarReader reader, - int propertyId, - int offset, - Map> allOffsets, -) { - switch (propertyId) { - case 0: - return (reader.readDoubleList(offset) ?? []) as P; - case 1: - return (reader.readLong(offset)) as P; - case 2: - return (_EmbeddingmodelValueEnumMap[reader.readByteOrNull(offset)] ?? - Model.onnxClip) as P; - case 3: - return (reader.readLongOrNull(offset)) as P; - default: - throw IsarError('Unknown property with id $propertyId'); - } -} - -const _EmbeddingmodelEnumValueMap = { - 'onnxClip': 0, - 'ggmlClip': 1, -}; -const _EmbeddingmodelValueEnumMap = { - 0: Model.onnxClip, - 1: Model.ggmlClip, -}; - -Id _embeddingGetId(Embedding object) { - return object.id; -} - -List> _embeddingGetLinks(Embedding object) { - return []; -} - -void _embeddingAttach(IsarCollection col, Id id, Embedding object) { - object.id = id; -} - -extension EmbeddingByIndex on IsarCollection { - Future getByModelFileID(Model model, int fileID) { - return getByIndex(r'unique_file_model_embedding', [model, fileID]); - } - - Embedding? getByModelFileIDSync(Model model, int fileID) { - return getByIndexSync(r'unique_file_model_embedding', [model, fileID]); - } - - Future deleteByModelFileID(Model model, int fileID) { - return deleteByIndex(r'unique_file_model_embedding', [model, fileID]); - } - - bool deleteByModelFileIDSync(Model model, int fileID) { - return deleteByIndexSync(r'unique_file_model_embedding', [model, fileID]); - } - - Future> getAllByModelFileID( - List modelValues, List fileIDValues) { - final len = modelValues.length; - assert(fileIDValues.length == len, - 'All index values must have the same length'); - final values = >[]; - for (var i = 0; i < len; i++) { - values.add([modelValues[i], fileIDValues[i]]); - } - - return getAllByIndex(r'unique_file_model_embedding', values); - } - - List getAllByModelFileIDSync( - List modelValues, List fileIDValues) { - final len = modelValues.length; - assert(fileIDValues.length == len, - 'All index values must have the same length'); - final values = >[]; - for (var i = 0; i < len; i++) { - values.add([modelValues[i], fileIDValues[i]]); - } - - return getAllByIndexSync(r'unique_file_model_embedding', values); - } - - Future deleteAllByModelFileID( - List modelValues, List fileIDValues) { - final len = modelValues.length; - assert(fileIDValues.length == len, - 'All index values must have the same length'); - final values = >[]; - for (var i = 0; i < len; i++) { - values.add([modelValues[i], fileIDValues[i]]); - } - - return deleteAllByIndex(r'unique_file_model_embedding', values); - } - - int deleteAllByModelFileIDSync( - List modelValues, List fileIDValues) { - final len = modelValues.length; - assert(fileIDValues.length == len, - 'All index values must have the same length'); - final values = >[]; - for (var i = 0; i < len; i++) { - values.add([modelValues[i], fileIDValues[i]]); - } - - return deleteAllByIndexSync(r'unique_file_model_embedding', values); - } - - Future putByModelFileID(Embedding object) { - return putByIndex(r'unique_file_model_embedding', object); - } - - Id putByModelFileIDSync(Embedding object, {bool saveLinks = true}) { - return putByIndexSync(r'unique_file_model_embedding', object, - saveLinks: saveLinks); - } - - Future> putAllByModelFileID(List objects) { - return putAllByIndex(r'unique_file_model_embedding', objects); - } - - List putAllByModelFileIDSync(List objects, - {bool saveLinks = true}) { - return putAllByIndexSync(r'unique_file_model_embedding', objects, - saveLinks: saveLinks); - } -} - -extension EmbeddingQueryWhereSort - on QueryBuilder { - QueryBuilder anyId() { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(const IdWhereClause.any()); - }); - } - - QueryBuilder anyModelFileID() { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - const IndexWhereClause.any(indexName: r'unique_file_model_embedding'), - ); - }); - } -} - -extension EmbeddingQueryWhere - on QueryBuilder { - QueryBuilder idEqualTo(Id id) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(IdWhereClause.between( - lower: id, - upper: id, - )); - }); - } - - QueryBuilder idNotEqualTo(Id id) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: false), - ) - .addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: false), - ); - } else { - return query - .addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: false), - ) - .addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: false), - ); - } - }); - } - - QueryBuilder idGreaterThan(Id id, - {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.greaterThan(lower: id, includeLower: include), - ); - }); - } - - QueryBuilder idLessThan(Id id, - {bool include = false}) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause( - IdWhereClause.lessThan(upper: id, includeUpper: include), - ); - }); - } - - QueryBuilder idBetween( - Id lowerId, - Id upperId, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(IdWhereClause.between( - lower: lowerId, - includeLower: includeLower, - upper: upperId, - includeUpper: includeUpper, - )); - }); - } - - QueryBuilder modelEqualToAnyFileID( - Model model) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(IndexWhereClause.equalTo( - indexName: r'unique_file_model_embedding', - value: [model], - )); - }); - } - - QueryBuilder - modelNotEqualToAnyFileID(Model model) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause(IndexWhereClause.between( - indexName: r'unique_file_model_embedding', - lower: [], - upper: [model], - includeUpper: false, - )) - .addWhereClause(IndexWhereClause.between( - indexName: r'unique_file_model_embedding', - lower: [model], - includeLower: false, - upper: [], - )); - } else { - return query - .addWhereClause(IndexWhereClause.between( - indexName: r'unique_file_model_embedding', - lower: [model], - includeLower: false, - upper: [], - )) - .addWhereClause(IndexWhereClause.between( - indexName: r'unique_file_model_embedding', - lower: [], - upper: [model], - includeUpper: false, - )); - } - }); - } - - QueryBuilder - modelGreaterThanAnyFileID( - Model model, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(IndexWhereClause.between( - indexName: r'unique_file_model_embedding', - lower: [model], - includeLower: include, - upper: [], - )); - }); - } - - QueryBuilder modelLessThanAnyFileID( - Model model, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(IndexWhereClause.between( - indexName: r'unique_file_model_embedding', - lower: [], - upper: [model], - includeUpper: include, - )); - }); - } - - QueryBuilder modelBetweenAnyFileID( - Model lowerModel, - Model upperModel, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(IndexWhereClause.between( - indexName: r'unique_file_model_embedding', - lower: [lowerModel], - includeLower: includeLower, - upper: [upperModel], - includeUpper: includeUpper, - )); - }); - } - - QueryBuilder modelFileIDEqualTo( - Model model, int fileID) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(IndexWhereClause.equalTo( - indexName: r'unique_file_model_embedding', - value: [model, fileID], - )); - }); - } - - QueryBuilder - modelEqualToFileIDNotEqualTo(Model model, int fileID) { - return QueryBuilder.apply(this, (query) { - if (query.whereSort == Sort.asc) { - return query - .addWhereClause(IndexWhereClause.between( - indexName: r'unique_file_model_embedding', - lower: [model], - upper: [model, fileID], - includeUpper: false, - )) - .addWhereClause(IndexWhereClause.between( - indexName: r'unique_file_model_embedding', - lower: [model, fileID], - includeLower: false, - upper: [model], - )); - } else { - return query - .addWhereClause(IndexWhereClause.between( - indexName: r'unique_file_model_embedding', - lower: [model, fileID], - includeLower: false, - upper: [model], - )) - .addWhereClause(IndexWhereClause.between( - indexName: r'unique_file_model_embedding', - lower: [model], - upper: [model, fileID], - includeUpper: false, - )); - } - }); - } - - QueryBuilder - modelEqualToFileIDGreaterThan( - Model model, - int fileID, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(IndexWhereClause.between( - indexName: r'unique_file_model_embedding', - lower: [model, fileID], - includeLower: include, - upper: [model], - )); - }); - } - - QueryBuilder - modelEqualToFileIDLessThan( - Model model, - int fileID, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(IndexWhereClause.between( - indexName: r'unique_file_model_embedding', - lower: [model], - upper: [model, fileID], - includeUpper: include, - )); - }); - } - - QueryBuilder - modelEqualToFileIDBetween( - Model model, - int lowerFileID, - int upperFileID, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addWhereClause(IndexWhereClause.between( - indexName: r'unique_file_model_embedding', - lower: [model, lowerFileID], - includeLower: includeLower, - upper: [model, upperFileID], - includeUpper: includeUpper, - )); - }); - } -} - -extension EmbeddingQueryFilter - on QueryBuilder { - QueryBuilder - embeddingElementEqualTo( - double value, { - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'embedding', - value: value, - epsilon: epsilon, - )); - }); - } - - QueryBuilder - embeddingElementGreaterThan( - double value, { - bool include = false, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.greaterThan( - include: include, - property: r'embedding', - value: value, - epsilon: epsilon, - )); - }); - } - - QueryBuilder - embeddingElementLessThan( - double value, { - bool include = false, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.lessThan( - include: include, - property: r'embedding', - value: value, - epsilon: epsilon, - )); - }); - } - - QueryBuilder - embeddingElementBetween( - double lower, - double upper, { - bool includeLower = true, - bool includeUpper = true, - double epsilon = Query.epsilon, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.between( - property: r'embedding', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - epsilon: epsilon, - )); - }); - } - - QueryBuilder - embeddingLengthEqualTo(int length) { - return QueryBuilder.apply(this, (query) { - return query.listLength( - r'embedding', - length, - true, - length, - true, - ); - }); - } - - QueryBuilder embeddingIsEmpty() { - return QueryBuilder.apply(this, (query) { - return query.listLength( - r'embedding', - 0, - true, - 0, - true, - ); - }); - } - - QueryBuilder - embeddingIsNotEmpty() { - return QueryBuilder.apply(this, (query) { - return query.listLength( - r'embedding', - 0, - false, - 999999, - true, - ); - }); - } - - QueryBuilder - embeddingLengthLessThan( - int length, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.listLength( - r'embedding', - 0, - true, - length, - include, - ); - }); - } - - QueryBuilder - embeddingLengthGreaterThan( - int length, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.listLength( - r'embedding', - length, - include, - 999999, - true, - ); - }); - } - - QueryBuilder - embeddingLengthBetween( - int lower, - int upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.listLength( - r'embedding', - lower, - includeLower, - upper, - includeUpper, - ); - }); - } - - QueryBuilder fileIDEqualTo( - int value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'fileID', - value: value, - )); - }); - } - - QueryBuilder fileIDGreaterThan( - int value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.greaterThan( - include: include, - property: r'fileID', - value: value, - )); - }); - } - - QueryBuilder fileIDLessThan( - int value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.lessThan( - include: include, - property: r'fileID', - value: value, - )); - }); - } - - QueryBuilder fileIDBetween( - int lower, - int upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.between( - property: r'fileID', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - )); - }); - } - - QueryBuilder idEqualTo( - Id value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'id', - value: value, - )); - }); - } - - QueryBuilder idGreaterThan( - Id value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.greaterThan( - include: include, - property: r'id', - value: value, - )); - }); - } - - QueryBuilder idLessThan( - Id value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.lessThan( - include: include, - property: r'id', - value: value, - )); - }); - } - - QueryBuilder idBetween( - Id lower, - Id upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.between( - property: r'id', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - )); - }); - } - - QueryBuilder modelEqualTo( - Model value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'model', - value: value, - )); - }); - } - - QueryBuilder modelGreaterThan( - Model value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.greaterThan( - include: include, - property: r'model', - value: value, - )); - }); - } - - QueryBuilder modelLessThan( - Model value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.lessThan( - include: include, - property: r'model', - value: value, - )); - }); - } - - QueryBuilder modelBetween( - Model lower, - Model upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.between( - property: r'model', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - )); - }); - } - - QueryBuilder - updationTimeIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(const FilterCondition.isNull( - property: r'updationTime', - )); - }); - } - - QueryBuilder - updationTimeIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(const FilterCondition.isNotNull( - property: r'updationTime', - )); - }); - } - - QueryBuilder updationTimeEqualTo( - int? value) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.equalTo( - property: r'updationTime', - value: value, - )); - }); - } - - QueryBuilder - updationTimeGreaterThan( - int? value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.greaterThan( - include: include, - property: r'updationTime', - value: value, - )); - }); - } - - QueryBuilder - updationTimeLessThan( - int? value, { - bool include = false, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.lessThan( - include: include, - property: r'updationTime', - value: value, - )); - }); - } - - QueryBuilder updationTimeBetween( - int? lower, - int? upper, { - bool includeLower = true, - bool includeUpper = true, - }) { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(FilterCondition.between( - property: r'updationTime', - lower: lower, - includeLower: includeLower, - upper: upper, - includeUpper: includeUpper, - )); - }); - } -} - -extension EmbeddingQueryObject - on QueryBuilder {} - -extension EmbeddingQueryLinks - on QueryBuilder {} - -extension EmbeddingQuerySortBy on QueryBuilder { - QueryBuilder sortByFileID() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'fileID', Sort.asc); - }); - } - - QueryBuilder sortByFileIDDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'fileID', Sort.desc); - }); - } - - QueryBuilder sortByModel() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'model', Sort.asc); - }); - } - - QueryBuilder sortByModelDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'model', Sort.desc); - }); - } - - QueryBuilder sortByUpdationTime() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'updationTime', Sort.asc); - }); - } - - QueryBuilder sortByUpdationTimeDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'updationTime', Sort.desc); - }); - } -} - -extension EmbeddingQuerySortThenBy - on QueryBuilder { - QueryBuilder thenByFileID() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'fileID', Sort.asc); - }); - } - - QueryBuilder thenByFileIDDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'fileID', Sort.desc); - }); - } - - QueryBuilder thenById() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.asc); - }); - } - - QueryBuilder thenByIdDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'id', Sort.desc); - }); - } - - QueryBuilder thenByModel() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'model', Sort.asc); - }); - } - - QueryBuilder thenByModelDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'model', Sort.desc); - }); - } - - QueryBuilder thenByUpdationTime() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'updationTime', Sort.asc); - }); - } - - QueryBuilder thenByUpdationTimeDesc() { - return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'updationTime', Sort.desc); - }); - } -} - -extension EmbeddingQueryWhereDistinct - on QueryBuilder { - QueryBuilder distinctByEmbedding() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'embedding'); - }); - } - - QueryBuilder distinctByFileID() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'fileID'); - }); - } - - QueryBuilder distinctByModel() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'model'); - }); - } - - QueryBuilder distinctByUpdationTime() { - return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'updationTime'); - }); - } -} - -extension EmbeddingQueryProperty - on QueryBuilder { - QueryBuilder idProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'id'); - }); - } - - QueryBuilder, QQueryOperations> embeddingProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'embedding'); - }); - } - - QueryBuilder fileIDProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'fileID'); - }); - } - - QueryBuilder modelProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'model'); - }); - } - - QueryBuilder updationTimeProperty() { - return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'updationTime'); - }); - } -} diff --git a/mobile/lib/models/file/file.dart b/mobile/lib/models/file/file.dart index ec52d7b9693c0a7287c4b9e7bbcb806434c10acf..9df25bb051016001a56cde270fcd8271f49aa9a6 100644 --- a/mobile/lib/models/file/file.dart +++ b/mobile/lib/models/file/file.dart @@ -314,7 +314,7 @@ class EnteFile { @override String toString() { return '''File(generatedID: $generatedID, localID: $localID, title: $title, - uploadedFileId: $uploadedFileID, modificationTime: $modificationTime, + type: $fileType, uploadedFileId: $uploadedFileID, modificationTime: $modificationTime, ownerID: $ownerID, collectionID: $collectionID, updationTime: $updationTime)'''; } diff --git a/mobile/lib/models/gallery_type.dart b/mobile/lib/models/gallery_type.dart index b711e0f74a84e78bfe2c88519f4eac19a902ed5d..bb02f1bbca74f0d7cdd756fb785a9ab6a1398892 100644 --- a/mobile/lib/models/gallery_type.dart +++ b/mobile/lib/models/gallery_type.dart @@ -35,12 +35,12 @@ extension GalleyTypeExtension on GalleryType { case GalleryType.quickLink: case GalleryType.uncategorized: case GalleryType.peopleTag: + case GalleryType.sharedCollection: return true; case GalleryType.hiddenSection: case GalleryType.hiddenOwnedCollection: case GalleryType.trash: - case GalleryType.sharedCollection: case GalleryType.cluster: return false; } diff --git a/mobile/lib/service_locator.dart b/mobile/lib/service_locator.dart index 0fec75b4657d2f7d54ab25cd56fb67aa03901253..397703761efb7c2aba28639e33c6f860b19769e6 100644 --- a/mobile/lib/service_locator.dart +++ b/mobile/lib/service_locator.dart @@ -1,4 +1,6 @@ import "package:dio/dio.dart"; +import "package:ente_cast/ente_cast.dart"; +import "package:ente_cast_normal/ente_cast_normal.dart"; import "package:ente_feature_flag/ente_feature_flag.dart"; import "package:shared_preferences/shared_preferences.dart"; @@ -26,3 +28,9 @@ FlagService get flagService { ); return _flagService!; } + +CastService? _castService; +CastService get castService { + _castService ??= CastServiceImpl(); + return _castService!; +} diff --git a/mobile/lib/services/collections_service.dart b/mobile/lib/services/collections_service.dart index 0981eb767e884ed340a46d4a155ee17fbd07520a..5b16bc70fb29d07d7754434f3e9d9c3497019403 100644 --- a/mobile/lib/services/collections_service.dart +++ b/mobile/lib/services/collections_service.dart @@ -30,7 +30,6 @@ import 'package:photos/models/collection/collection_items.dart'; import 'package:photos/models/file/file.dart'; import "package:photos/models/files_split.dart"; import "package:photos/models/metadata/collection_magic.dart"; -import "package:photos/service_locator.dart"; import 'package:photos/services/app_lifecycle_service.dart'; import "package:photos/services/favorites_service.dart"; import 'package:photos/services/file_magic_service.dart'; @@ -1179,9 +1178,6 @@ class CollectionsService { await _addToCollection(dstCollectionID, splitResult.ownedByCurrentUser); } if (splitResult.ownedByOtherUsers.isNotEmpty) { - if (!flagService.internalUser) { - throw ArgumentError('Cannot add files owned by other users'); - } late final List filesToCopy; late final List filesToAdd; (filesToAdd, filesToCopy) = (await _splitFilesToAddAndCopy( diff --git a/mobile/lib/services/machine_learning/semantic_search/embedding_store.dart b/mobile/lib/services/machine_learning/semantic_search/embedding_store.dart index f7d17f8b869ad0b99eabd1e832a76dd454870339..420b8c97f7fc0327a8f2f8080903a6008a931617 100644 --- a/mobile/lib/services/machine_learning/semantic_search/embedding_store.dart +++ b/mobile/lib/services/machine_learning/semantic_search/embedding_store.dart @@ -19,7 +19,7 @@ class EmbeddingStore { static final EmbeddingStore instance = EmbeddingStore._privateConstructor(); - static const kEmbeddingsSyncTimeKey = "sync_time_embeddings_v2"; + static const kEmbeddingsSyncTimeKey = "sync_time_embeddings_v3"; final _logger = Logger("EmbeddingStore"); final _dio = NetworkClient.instance.enteDio; diff --git a/mobile/lib/services/machine_learning/semantic_search/semantic_search_service.dart b/mobile/lib/services/machine_learning/semantic_search/semantic_search_service.dart index d1074053a291bcccd9a803da5c2fbe239991c709..337ca913ff7c5133e0ea851c2f2cecbe760db538 100644 --- a/mobile/lib/services/machine_learning/semantic_search/semantic_search_service.dart +++ b/mobile/lib/services/machine_learning/semantic_search/semantic_search_service.dart @@ -72,8 +72,8 @@ class SemanticSearchService { _mlFramework = _currentModel == Model.onnxClip ? ONNX(shouldDownloadOverMobileData) : GGML(shouldDownloadOverMobileData); - await EmbeddingsDB.instance.init(); await EmbeddingStore.instance.init(); + await EmbeddingsDB.instance.init(); await _loadEmbeddings(); Bus.instance.on().listen((event) { _embeddingLoaderDebouncer.run(() async { diff --git a/mobile/lib/services/update_service.dart b/mobile/lib/services/update_service.dart index e18d8548c67cd7d54eac8dc0401d90f4e339ea07..da01de828da33d7215bb90db98777016b8c36b7b 100644 --- a/mobile/lib/services/update_service.dart +++ b/mobile/lib/services/update_service.dart @@ -16,7 +16,7 @@ class UpdateService { static final UpdateService instance = UpdateService._privateConstructor(); static const kUpdateAvailableShownTimeKey = "update_available_shown_time_key"; static const changeLogVersionKey = "update_change_log_key"; - static const currentChangeLogVersion = 18; + static const currentChangeLogVersion = 19; LatestVersionInfo? _latestVersion; final _logger = Logger("UpdateService"); diff --git a/mobile/lib/ui/actions/collection/collection_sharing_actions.dart b/mobile/lib/ui/actions/collection/collection_sharing_actions.dart index 7993c43423b874bddd6cf9f1a1606ec53b57daff..3328722dbe9f414170755e7eecd848a8f963b7b2 100644 --- a/mobile/lib/ui/actions/collection/collection_sharing_actions.dart +++ b/mobile/lib/ui/actions/collection/collection_sharing_actions.dart @@ -439,7 +439,12 @@ class CollectionActions { ) async { final List files = await FilesDB.instance.getAllFilesCollection(collection.id); - await moveFilesFromCurrentCollection(bContext, collection, files); + await moveFilesFromCurrentCollection( + bContext, + collection, + files, + isHidden: collection.isHidden() && !collection.isDefaultHidden(), + ); // collection should be empty on server now await collectionsService.trashEmptyCollection(collection); } diff --git a/mobile/lib/ui/cast/auto.dart b/mobile/lib/ui/cast/auto.dart new file mode 100644 index 0000000000000000000000000000000000000000..34c97b34de7187069572f52a55623f8fe17f83d0 --- /dev/null +++ b/mobile/lib/ui/cast/auto.dart @@ -0,0 +1,133 @@ +import "dart:io"; + +import "package:ente_cast/ente_cast.dart"; +import "package:flutter/material.dart"; +import "package:photos/generated/l10n.dart"; +import "package:photos/service_locator.dart"; +import "package:photos/theme/ente_theme.dart"; +import "package:photos/ui/common/loading_widget.dart"; +import "package:photos/utils/dialog_util.dart"; + +class AutoCastDialog extends StatefulWidget { + // async method that takes string as input + // and returns void + final void Function(String) onConnect; + AutoCastDialog( + this.onConnect, { + Key? key, + }) : super(key: key) {} + + @override + State createState() => _AutoCastDialogState(); +} + +class _AutoCastDialogState extends State { + final bool doesUserExist = true; + final Set _isDeviceTapInProgress = {}; + + @override + Widget build(BuildContext context) { + final textStyle = getEnteTextTheme(context); + final AlertDialog alert = AlertDialog( + title: Text( + S.of(context).connectToDevice, + style: textStyle.largeBold, + ), + content: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + S.of(context).autoCastDialogBody, + style: textStyle.bodyMuted, + ), + if (Platform.isIOS) + Text( + S.of(context).autoCastiOSPermission, + style: textStyle.bodyMuted, + ), + const SizedBox(height: 16), + FutureBuilder>( + future: castService.searchDevices(), + builder: (_, snapshot) { + if (snapshot.hasError) { + return Center( + child: Text( + 'Error: ${snapshot.error.toString()}', + ), + ); + } else if (!snapshot.hasData) { + return const EnteLoadingWidget(); + } + + if (snapshot.data!.isEmpty) { + return Center(child: Text(S.of(context).noDeviceFound)); + } + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: snapshot.data!.map((result) { + final device = result.$2; + final name = result.$1; + return GestureDetector( + onTap: () async { + if (_isDeviceTapInProgress.contains(device)) { + return; + } + setState(() { + _isDeviceTapInProgress.add(device); + }); + try { + await _connectToYourApp(context, device); + } catch (e) { + if (mounted) { + setState(() { + _isDeviceTapInProgress.remove(device); + }); + showGenericErrorDialog(context: context, error: e) + .ignore(); + } + } + }, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Row( + children: [ + Expanded(child: Text(name)), + if (_isDeviceTapInProgress.contains(device)) + const EnteLoadingWidget(), + ], + ), + ), + ); + }).toList(), + ); + }, + ), + ], + ), + ); + return alert; + } + + Future _connectToYourApp( + BuildContext context, + Object castDevice, + ) async { + await castService.connectDevice( + context, + castDevice, + onMessage: (message) { + if (message.containsKey(CastMessageType.pairCode)) { + final code = message[CastMessageType.pairCode]!['code']; + widget.onConnect(code); + } + if (mounted) { + setState(() { + _isDeviceTapInProgress.remove(castDevice); + }); + } + }, + ); + } +} diff --git a/mobile/lib/ui/cast/choose.dart b/mobile/lib/ui/cast/choose.dart new file mode 100644 index 0000000000000000000000000000000000000000..bd4c9876de123ff363c0a7172b010b5bf05a8208 --- /dev/null +++ b/mobile/lib/ui/cast/choose.dart @@ -0,0 +1,76 @@ +import "package:flutter/material.dart"; +import "package:photos/generated/l10n.dart"; +import "package:photos/l10n/l10n.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"; + +class CastChooseDialog extends StatefulWidget { + const CastChooseDialog({ + Key? key, + }) : super(key: key); + + @override + State createState() => _CastChooseDialogState(); +} + +class _CastChooseDialogState extends State { + final bool doesUserExist = true; + + @override + Widget build(BuildContext context) { + final textStyle = getEnteTextTheme(context); + final AlertDialog alert = AlertDialog( + title: Text( + context.l10n.playOnTv, + style: textStyle.largeBold, + ), + content: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(height: 8), + Text( + S.of(context).autoPairDesc, + style: textStyle.bodyMuted, + ), + const SizedBox(height: 12), + ButtonWidget( + labelText: S.of(context).autoPair, + icon: Icons.cast_outlined, + buttonType: ButtonType.neutral, + buttonSize: ButtonSize.large, + shouldStickToDarkTheme: true, + buttonAction: ButtonAction.first, + shouldSurfaceExecutionStates: false, + isInAlert: true, + onTap: () async { + Navigator.of(context).pop(ButtonAction.first); + }, + ), + const SizedBox(height: 36), + Text( + S.of(context).manualPairDesc, + style: textStyle.bodyMuted, + ), + const SizedBox(height: 12), + ButtonWidget( + labelText: S.of(context).pairWithPin, + buttonType: ButtonType.neutral, + // icon for pairing with TV manually + icon: Icons.tv_outlined, + buttonSize: ButtonSize.large, + isInAlert: true, + onTap: () async { + Navigator.of(context).pop(ButtonAction.second); + }, + shouldStickToDarkTheme: true, + buttonAction: ButtonAction.second, + shouldSurfaceExecutionStates: false, + ), + ], + ), + ); + return alert; + } +} diff --git a/mobile/lib/ui/components/bottom_action_bar/selection_action_button_widget.dart b/mobile/lib/ui/components/bottom_action_bar/selection_action_button_widget.dart index 60db98cf4ff801c8c06ae8b0d7bb0ed35b2f1559..5ca6a25dcc4b89c3cd5d265ec6da89c370089274 100644 --- a/mobile/lib/ui/components/bottom_action_bar/selection_action_button_widget.dart +++ b/mobile/lib/ui/components/bottom_action_bar/selection_action_button_widget.dart @@ -132,14 +132,15 @@ class __BodyState extends State<_Body> { return maxWidth; } +//Todo: this doesn't give the correct width of the word, make it right double computeWidthOfWord(String text, TextStyle style) { final textPainter = TextPainter( text: TextSpan(text: text, style: style), maxLines: 1, textDirection: TextDirection.ltr, - textScaleFactor: MediaQuery.of(context).textScaleFactor, + textScaler: MediaQuery.textScalerOf(context), )..layout(); - - return textPainter.size.width; +//buffer of 8 added as width is shorter than actual text width + return textPainter.size.width + 8; } } diff --git a/mobile/lib/ui/notification/update/change_log_page.dart b/mobile/lib/ui/notification/update/change_log_page.dart index 289d84590df32a878339159cbd4bbd396cccc56b..90430fae25a0e39eafe8a6b148a1916c0e7d2542 100644 --- a/mobile/lib/ui/notification/update/change_log_page.dart +++ b/mobile/lib/ui/notification/update/change_log_page.dart @@ -1,5 +1,3 @@ -import "dart:async"; - import 'package:flutter/material.dart'; import "package:photos/generated/l10n.dart"; import 'package:photos/services/update_service.dart'; @@ -9,7 +7,6 @@ import 'package:photos/ui/components/divider_widget.dart'; import 'package:photos/ui/components/models/button_type.dart'; import 'package:photos/ui/components/title_bar_title_widget.dart'; import 'package:photos/ui/notification/update/change_log_entry.dart'; -import "package:url_launcher/url_launcher_string.dart"; class ChangeLogPage extends StatefulWidget { const ChangeLogPage({ @@ -81,31 +78,31 @@ class _ChangeLogPageState extends State { const SizedBox( height: 8, ), - ButtonWidget( - buttonType: ButtonType.trailingIconSecondary, - buttonSize: ButtonSize.large, - labelText: S.of(context).joinDiscord, - icon: Icons.discord_outlined, - iconColor: enteColorScheme.primary500, - onTap: () async { - unawaited( - launchUrlString( - "https://discord.com/invite/z2YVKkycX3", - mode: LaunchMode.externalApplication, - ), - ); - }, - ), // ButtonWidget( // buttonType: ButtonType.trailingIconSecondary, // buttonSize: ButtonSize.large, - // labelText: S.of(context).rateTheApp, - // icon: Icons.favorite_rounded, + // labelText: S.of(context).joinDiscord, + // icon: Icons.discord_outlined, // iconColor: enteColorScheme.primary500, // onTap: () async { - // await UpdateService.instance.launchReviewUrl(); + // unawaited( + // launchUrlString( + // "https://discord.com/invite/z2YVKkycX3", + // mode: LaunchMode.externalApplication, + // ), + // ); // }, // ), + ButtonWidget( + buttonType: ButtonType.trailingIconSecondary, + buttonSize: ButtonSize.large, + labelText: S.of(context).rateTheApp, + icon: Icons.favorite_rounded, + iconColor: enteColorScheme.primary500, + onTap: () async { + await UpdateService.instance.launchReviewUrl(); + }, + ), const SizedBox(height: 8), ], ), @@ -122,18 +119,20 @@ class _ChangeLogPageState extends State { final List items = []; items.addAll([ ChangeLogEntry( - "Improved Performance for Large Galleries ✨", - 'We\'ve made significant improvements to how quickly galleries load and' - ' with less stutter, especially for those with a lot of photos and videos.', + "Cast albums to TV ✨", + "View a slideshow of your albums on any big screen! Open an album and click on the Cast button to get started.", + ), + ChangeLogEntry( + "Organize shared photos", + "You can now add shared items to your favorites or to any of your personal albums. Ente will create a copy that is fully owned by you and can be organized to your liking.", ), ChangeLogEntry( - "Enhanced Functionality for Video Backups", - 'Even if video backups are disabled, you can now manually upload individual videos.', + "Download multiple items", + "You can now download multiple items to your gallery at once. Select the items you want to download and click on the download button.", ), ChangeLogEntry( - "Bug Fixes", - 'Many a bugs were squashed in this release.\n' - '\nIf you run into any, please write to team@ente.io, or let us know on Discord! 🙏', + "Performance improvements", + "This release also brings in major changes that should improve responsiveness. If you discover room for improvement, please let us know!", ), ]); diff --git a/mobile/lib/ui/viewer/actions/file_selection_actions_widget.dart b/mobile/lib/ui/viewer/actions/file_selection_actions_widget.dart index 46d817548b42607ab1ac3ba59f7caf2d3f92eff3..beeb9164d5898ed4516e7968b6a9361c20bb9623 100644 --- a/mobile/lib/ui/viewer/actions/file_selection_actions_widget.dart +++ b/mobile/lib/ui/viewer/actions/file_selection_actions_widget.dart @@ -4,6 +4,7 @@ import 'package:fast_base58/fast_base58.dart'; import "package:flutter/cupertino.dart"; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import "package:logging/logging.dart"; import "package:modal_bottom_sheet/modal_bottom_sheet.dart"; import 'package:photos/core/configuration.dart'; import "package:photos/core/event_bus.dart"; @@ -18,7 +19,6 @@ import 'package:photos/models/files_split.dart'; import 'package:photos/models/gallery_type.dart'; import "package:photos/models/metadata/common_keys.dart"; import 'package:photos/models/selected_files.dart'; -import "package:photos/service_locator.dart"; import 'package:photos/services/collections_service.dart'; import 'package:photos/services/hidden_service.dart'; import 'package:photos/services/machine_learning/face_ml/feedback/cluster_feedback.dart'; @@ -36,6 +36,8 @@ import 'package:photos/ui/sharing/manage_links_widget.dart'; import "package:photos/ui/tools/collage/collage_creator_page.dart"; import "package:photos/ui/viewer/location/update_location_data_widget.dart"; import 'package:photos/utils/delete_file_util.dart'; +import "package:photos/utils/dialog_util.dart"; +import "package:photos/utils/file_download_util.dart"; import 'package:photos/utils/magic_util.dart'; import 'package:photos/utils/navigation_util.dart'; import "package:photos/utils/share_util.dart"; @@ -66,11 +68,11 @@ class FileSelectionActionsWidget extends StatefulWidget { class _FileSelectionActionsWidgetState extends State { + static final _logger = Logger("FileSelectionActionsWidget"); late int currentUserID; late FilesSplit split; late CollectionActions collectionActions; late bool isCollectionOwner; - bool _isInternalUser = false; // _cachedCollectionForSharedLink is primarily used to avoid creating duplicate // links if user keeps on creating Create link button after selecting @@ -108,7 +110,6 @@ class _FileSelectionActionsWidgetState @override Widget build(BuildContext context) { - _isInternalUser = flagService.internalUser; final ownedFilesCount = split.ownedByCurrentUser.length; final ownedAndPendingUploadFilesCount = ownedFilesCount + split.pendingUploads.length; @@ -125,6 +126,8 @@ class _FileSelectionActionsWidgetState !widget.selectedFiles.files.any( (element) => element.fileType == FileType.video, ); + final showDownloadOption = + widget.selectedFiles.files.any((element) => element.localID == null); //To animate adding and removing of [SelectedActionButton], add all items //and set [shouldShow] to false for items that should not be shown and true @@ -171,14 +174,13 @@ class _FileSelectionActionsWidgetState final showUploadIcon = widget.type == GalleryType.localFolder && split.ownedByCurrentUser.isEmpty; - if (widget.type.showAddToAlbum() || - (_isInternalUser && widget.type == GalleryType.sharedCollection)) { + if (widget.type.showAddToAlbum()) { if (showUploadIcon) { items.add( SelectionActionButton( icon: Icons.cloud_upload_outlined, labelText: S.of(context).addToEnte, - onTap: (anyOwnedFiles || _isInternalUser) ? _addToAlbum : null, + onTap: _addToAlbum, ), ); } else { @@ -186,8 +188,7 @@ class _FileSelectionActionsWidgetState SelectionActionButton( icon: Icons.add_outlined, labelText: S.of(context).addToAlbum, - onTap: (anyOwnedFiles || _isInternalUser) ? _addToAlbum : null, - shouldShow: ownedAndPendingUploadFilesCount > 0 || _isInternalUser, + onTap: _addToAlbum, ), ); } @@ -394,6 +395,16 @@ class _FileSelectionActionsWidgetState ); } + if (showDownloadOption) { + items.add( + SelectionActionButton( + labelText: S.of(context).download, + icon: Icons.cloud_download_outlined, + onTap: () => _download(widget.selectedFiles.files.toList()), + ), + ); + } + items.add( SelectionActionButton( labelText: S.of(context).share, @@ -448,10 +459,8 @@ class _FileSelectionActionsWidgetState ), ), ); - } else { - // TODO: Return "Select All" here - return const SizedBox.shrink(); } + return const SizedBox(); } Future _moveFiles() async { @@ -477,10 +486,6 @@ class _FileSelectionActionsWidgetState } Future _addToAlbum() async { - if (split.ownedByOtherUsers.isNotEmpty && !_isInternalUser) { - widget.selectedFiles - .unSelectAll(split.ownedByOtherUsers.toSet(), skipNotify: true); - } showCollectionActionSheet(context, selectedFiles: widget.selectedFiles); } @@ -780,4 +785,29 @@ class _FileSelectionActionsWidgetState widget.selectedFiles.clearAll(); } } + + Future _download(List files) async { + final dialog = createProgressDialog( + context, + S.of(context).downloading, + isDismissible: true, + ); + await dialog.show(); + try { + final futures = []; + for (final file in files) { + if (file.localID == null) { + futures.add(downloadToGallery(file)); + } + } + await Future.wait(futures); + await dialog.hide(); + widget.selectedFiles.clearAll(); + showToast(context, S.of(context).filesSavedToGallery); + } catch (e) { + _logger.warning("Failed to save files", e); + await dialog.hide(); + await showGenericErrorDialog(context: context, error: e); + } + } } diff --git a/mobile/lib/ui/viewer/file/file_app_bar.dart b/mobile/lib/ui/viewer/file/file_app_bar.dart index e029aeb898ab161199c83f14d2fe1bea5af072bd..2f2c8d0614385a4ac7d6ed03d097e9f95863204b 100644 --- a/mobile/lib/ui/viewer/file/file_app_bar.dart +++ b/mobile/lib/ui/viewer/file/file_app_bar.dart @@ -1,33 +1,24 @@ import 'dart:io'; -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:logging/logging.dart'; import 'package:media_extension/media_extension.dart'; -import 'package:path/path.dart' as file_path; -import 'package:photo_manager/photo_manager.dart'; -import 'package:photos/core/event_bus.dart'; -import 'package:photos/db/files_db.dart'; -import 'package:photos/events/local_photos_updated_event.dart'; import "package:photos/generated/l10n.dart"; import "package:photos/l10n/l10n.dart"; import "package:photos/models/file/extensions/file_props.dart"; import 'package:photos/models/file/file.dart'; import 'package:photos/models/file/file_type.dart'; import 'package:photos/models/file/trash_file.dart'; -import 'package:photos/models/ignored_file.dart'; import "package:photos/models/metadata/common_keys.dart"; import 'package:photos/models/selected_files.dart'; -import "package:photos/service_locator.dart"; import 'package:photos/services/collections_service.dart'; import 'package:photos/services/hidden_service.dart'; -import 'package:photos/services/ignored_files_service.dart'; -import 'package:photos/services/local_sync_service.dart'; import 'package:photos/ui/collections/collection_action_sheet.dart'; import 'package:photos/ui/viewer/file/custom_app_bar.dart'; import "package:photos/ui/viewer/file_details/favorite_widget.dart"; import "package:photos/ui/viewer/file_details/upload_icon_widget.dart"; import 'package:photos/utils/dialog_util.dart'; +import "package:photos/utils/file_download_util.dart"; import 'package:photos/utils/file_util.dart'; import "package:photos/utils/magic_util.dart"; import 'package:photos/utils/toast_util.dart'; @@ -141,9 +132,7 @@ class FileAppBarState extends State { ); } // only show fav option for files owned by the user - if ((isOwnedByUser || flagService.internalUser) && - !isFileHidden && - isFileUploaded) { + if (!isFileHidden && isFileUploaded) { _actions.add(FavoriteWidget(widget.file)); } if (!isFileUploaded) { @@ -165,7 +154,7 @@ class FileAppBarState extends State { Icon( Platform.isAndroid ? Icons.download - : CupertinoIcons.cloud_download, + : Icons.cloud_download_outlined, color: Theme.of(context).iconTheme.color, ), const Padding( @@ -330,98 +319,16 @@ class FileAppBarState extends State { ); await dialog.show(); try { - final FileType type = file.fileType; - final bool downloadLivePhotoOnDroid = - type == FileType.livePhoto && Platform.isAndroid; - AssetEntity? savedAsset; - final File? fileToSave = await getFile(file); - //Disabling notifications for assets changing to insert the file into - //files db before triggering a sync. - await PhotoManager.stopChangeNotify(); - if (type == FileType.image) { - savedAsset = await PhotoManager.editor - .saveImageWithPath(fileToSave!.path, title: file.title!); - } else if (type == FileType.video) { - savedAsset = await PhotoManager.editor - .saveVideo(fileToSave!, title: file.title!); - } else if (type == FileType.livePhoto) { - final File? liveVideoFile = - await getFileFromServer(file, liveVideo: true); - if (liveVideoFile == null) { - throw AssertionError("Live video can not be null"); - } - if (downloadLivePhotoOnDroid) { - await _saveLivePhotoOnDroid(fileToSave!, liveVideoFile, file); - } else { - savedAsset = await PhotoManager.editor.darwin.saveLivePhoto( - imageFile: fileToSave!, - videoFile: liveVideoFile, - title: file.title!, - ); - } - } - - if (savedAsset != null) { - file.localID = savedAsset.id; - await FilesDB.instance.insert(file); - Bus.instance.fire( - LocalPhotosUpdatedEvent( - [file], - source: "download", - ), - ); - } else if (!downloadLivePhotoOnDroid && savedAsset == null) { - _logger.severe('Failed to save assert of type $type'); - } + await downloadToGallery(file); showToast(context, S.of(context).fileSavedToGallery); await dialog.hide(); } catch (e) { _logger.warning("Failed to save file", e); await dialog.hide(); await showGenericErrorDialog(context: context, error: e); - } finally { - await PhotoManager.startChangeNotify(); - LocalSyncService.instance.checkAndSync().ignore(); } } - Future _saveLivePhotoOnDroid( - File image, - File video, - EnteFile enteFile, - ) async { - debugPrint("Downloading LivePhoto on Droid"); - AssetEntity? savedAsset = await (PhotoManager.editor - .saveImageWithPath(image.path, title: enteFile.title!)); - if (savedAsset == null) { - throw Exception("Failed to save image of live photo"); - } - IgnoredFile ignoreVideoFile = IgnoredFile( - savedAsset.id, - savedAsset.title ?? '', - savedAsset.relativePath ?? 'remoteDownload', - "remoteDownload", - ); - await IgnoredFilesService.instance.cacheAndInsert([ignoreVideoFile]); - final videoTitle = file_path.basenameWithoutExtension(enteFile.title!) + - file_path.extension(video.path); - savedAsset = (await (PhotoManager.editor.saveVideo( - video, - title: videoTitle, - ))); - if (savedAsset == null) { - throw Exception("Failed to save video of live photo"); - } - - ignoreVideoFile = IgnoredFile( - savedAsset.id, - savedAsset.title ?? videoTitle, - savedAsset.relativePath ?? 'remoteDownload', - "remoteDownload", - ); - await IgnoredFilesService.instance.cacheAndInsert([ignoreVideoFile]); - } - Future _setAs(EnteFile file) async { final dialog = createProgressDialog(context, S.of(context).pleaseWait); await dialog.show(); diff --git a/mobile/lib/ui/viewer/gallery/gallery_app_bar_widget.dart b/mobile/lib/ui/viewer/gallery/gallery_app_bar_widget.dart index 83a55975f0045459062f21b5d15fd8fb16f6ae67..d2b7a6ec3db2664a6a7d43f661e995946f71c47d 100644 --- a/mobile/lib/ui/viewer/gallery/gallery_app_bar_widget.dart +++ b/mobile/lib/ui/viewer/gallery/gallery_app_bar_widget.dart @@ -24,6 +24,8 @@ import 'package:photos/services/collections_service.dart'; import 'package:photos/services/sync_service.dart'; import 'package:photos/services/update_service.dart'; import 'package:photos/ui/actions/collection/collection_sharing_actions.dart'; +import "package:photos/ui/cast/auto.dart"; +import "package:photos/ui/cast/choose.dart"; import "package:photos/ui/common/popup_item.dart"; import 'package:photos/ui/components/action_sheet_widget.dart'; import 'package:photos/ui/components/buttons/button_widget.dart'; @@ -88,16 +90,16 @@ class _GalleryAppBarWidgetState extends State { String? _appBarTitle; late CollectionActions collectionActions; bool isQuickLink = false; - late bool isInternalUser; late GalleryType galleryType; + final ValueNotifier castNotifier = ValueNotifier(0); + @override void initState() { super.initState(); _selectedFilesListener = () { setState(() {}); }; - isInternalUser = flagService.internalUser; collectionActions = CollectionActions(CollectionsService.instance); widget.selectedFiles.addListener(_selectedFilesListener); _userAuthEventSubscription = @@ -320,6 +322,27 @@ class _GalleryAppBarWidgetState extends State { ), ); } + + if (widget.collection != null && castService.isSupported) { + actions.add( + Tooltip( + message: "Cast album", + child: IconButton( + icon: ValueListenableBuilder( + valueListenable: castNotifier, + builder: (context, value, child) { + return castService.getActiveSessions().isNotEmpty + ? const Icon(Icons.cast_connected_rounded) + : const Icon(Icons.cast_outlined); + }, + ), + onPressed: () async { + await _castChoiceDialog(); + }, + ), + ), + ); + } final List> items = []; items.addAll([ if (galleryType.canRename()) @@ -391,7 +414,7 @@ class _GalleryAppBarWidgetState extends State { ? Icons.visibility_outlined : Icons.visibility_off_outlined, ), - if (widget.collection != null && isInternalUser) + if (widget.collection != null) EntePopupMenuItem( value: AlbumPopupAction.playOnTv, context.l10n.playOnTv, @@ -458,7 +481,7 @@ class _GalleryAppBarWidgetState extends State { } else if (value == AlbumPopupAction.leave) { await _leaveAlbum(context); } else if (value == AlbumPopupAction.playOnTv) { - await castAlbum(); + await _castChoiceDialog(); } else if (value == AlbumPopupAction.freeUpSpace) { await _deleteBackedUpFiles(context); } else if (value == AlbumPopupAction.setCover) { @@ -693,10 +716,62 @@ class _GalleryAppBarWidgetState extends State { setState(() {}); } - Future castAlbum() async { + Future _castChoiceDialog() async { final gw = CastGateway(NetworkClient.instance.enteDio); + if (castService.getActiveSessions().isNotEmpty) { + await showChoiceDialog( + context, + title: S.of(context).stopCastingTitle, + firstButtonLabel: S.of(context).yes, + secondButtonLabel: S.of(context).no, + body: S.of(context).stopCastingBody, + firstButtonOnTap: () async { + gw.revokeAllTokens().ignore(); + await castService.closeActiveCasts(); + }, + ); + castNotifier.value++; + return; + } + // stop any existing cast session gw.revokeAllTokens().ignore(); + if (!Platform.isAndroid) { + await _pairWithPin(gw, ''); + } else { + final result = await showDialog( + context: context, + barrierDismissible: true, + builder: (BuildContext context) { + return const CastChooseDialog(); + }, + ); + if (result == null) { + return; + } + // wait to allow the dialog to close + await Future.delayed(const Duration(milliseconds: 100)); + if (result == ButtonAction.first) { + await showDialog( + context: context, + barrierDismissible: true, + builder: (BuildContext bContext) { + return AutoCastDialog( + (device) async { + await _castPair(bContext, gw, device); + Navigator.pop(bContext); + }, + ); + }, + ); + } + if (result == ButtonAction.second) { + await _pairWithPin(gw, ''); + } + } + } + + Future _pairWithPin(CastGateway gw, String code) async { await showTextInputDialog( context, title: context.l10n.playOnTv, @@ -704,28 +779,63 @@ class _GalleryAppBarWidgetState extends State { submitButtonLabel: S.of(context).pair, textInputType: TextInputType.streetAddress, hintText: context.l10n.deviceCodeHint, + showOnlyLoadingState: true, + alwaysShowSuccessState: false, + initialValue: code, onSubmit: (String text) async { - try { - final code = text.trim(); - final String? publicKey = await gw.getPublicKey(code); - if (publicKey == null) { - showToast(context, S.of(context).deviceNotFound); - return; - } - final String castToken = const Uuid().v4().toString(); - final castPayload = CollectionsService.instance - .getCastData(castToken, widget.collection!, publicKey); - await gw.publishCastPayload( - code, - castPayload, - widget.collection!.id, - castToken, - ); - } catch (e, s) { - _logger.severe("Failed to cast album", e, s); - await showGenericErrorDialog(context: context, error: e); + final bool paired = await _castPair(context, gw, text); + if (!paired) { + Future.delayed(Duration.zero, () => _pairWithPin(gw, code)); } }, ); } + + String lastCode = ''; + Future _castPair( + BuildContext bContext, + CastGateway gw, + String code, + ) async { + try { + if (lastCode == code) { + return false; + } + lastCode = code; + _logger.info("Casting album to device with code $code"); + final String? publicKey = await gw.getPublicKey(code); + if (publicKey == null) { + showToast(context, S.of(context).deviceNotFound); + + return false; + } + final String castToken = const Uuid().v4().toString(); + final castPayload = CollectionsService.instance + .getCastData(castToken, widget.collection!, publicKey); + await gw.publishCastPayload( + code, + castPayload, + widget.collection!.id, + castToken, + ); + _logger.info("cast album completed"); + // showToast(bContext, S.of(context).pairingComplete); + castNotifier.value++; + return true; + } catch (e, s) { + lastCode = ''; + _logger.severe("Failed to cast album", e, s); + if (e is CastIPMismatchException) { + await showErrorDialog( + context, + S.of(context).castIPMismatchTitle, + S.of(context).castIPMismatchBody, + ); + } else { + await showGenericErrorDialog(context: bContext, error: e); + } + castNotifier.value++; + return false; + } + } } diff --git a/mobile/lib/utils/file_download_util.dart b/mobile/lib/utils/file_download_util.dart index 4a36fae6daf179165f5947c21bec7795726edaec..6db6ecbe093b35e0a3ccbc1292681ac7b3773f44 100644 --- a/mobile/lib/utils/file_download_util.dart +++ b/mobile/lib/utils/file_download_util.dart @@ -4,14 +4,23 @@ import "package:computer/computer.dart"; import 'package:dio/dio.dart'; import "package:flutter/foundation.dart"; import 'package:logging/logging.dart'; +import 'package:path/path.dart' as file_path; +import "package:photo_manager/photo_manager.dart"; import 'package:photos/core/configuration.dart'; +import "package:photos/core/event_bus.dart"; import 'package:photos/core/network/network.dart'; +import "package:photos/db/files_db.dart"; +import "package:photos/events/local_photos_updated_event.dart"; import 'package:photos/models/file/file.dart'; import "package:photos/models/file/file_type.dart"; +import "package:photos/models/ignored_file.dart"; import 'package:photos/services/collections_service.dart'; +import "package:photos/services/ignored_files_service.dart"; +import "package:photos/services/local_sync_service.dart"; import 'package:photos/utils/crypto_util.dart'; import "package:photos/utils/data_util.dart"; import "package:photos/utils/fake_progress.dart"; +import "package:photos/utils/file_util.dart"; final _logger = Logger("file_download_util"); @@ -116,6 +125,97 @@ Future getFileKeyUsingBgWorker(EnteFile file) async { ); } +Future downloadToGallery(EnteFile file) async { + try { + final FileType type = file.fileType; + final bool downloadLivePhotoOnDroid = + type == FileType.livePhoto && Platform.isAndroid; + AssetEntity? savedAsset; + final File? fileToSave = await getFile(file); + //Disabling notifications for assets changing to insert the file into + //files db before triggering a sync. + await PhotoManager.stopChangeNotify(); + if (type == FileType.image) { + savedAsset = await PhotoManager.editor + .saveImageWithPath(fileToSave!.path, title: file.title!); + } else if (type == FileType.video) { + savedAsset = + await PhotoManager.editor.saveVideo(fileToSave!, title: file.title!); + } else if (type == FileType.livePhoto) { + final File? liveVideoFile = + await getFileFromServer(file, liveVideo: true); + if (liveVideoFile == null) { + throw AssertionError("Live video can not be null"); + } + if (downloadLivePhotoOnDroid) { + await _saveLivePhotoOnDroid(fileToSave!, liveVideoFile, file); + } else { + savedAsset = await PhotoManager.editor.darwin.saveLivePhoto( + imageFile: fileToSave!, + videoFile: liveVideoFile, + title: file.title!, + ); + } + } + + if (savedAsset != null) { + file.localID = savedAsset.id; + await FilesDB.instance.insert(file); + Bus.instance.fire( + LocalPhotosUpdatedEvent( + [file], + source: "download", + ), + ); + } else if (!downloadLivePhotoOnDroid && savedAsset == null) { + _logger.severe('Failed to save assert of type $type'); + } + } catch (e) { + _logger.severe("Failed to save file", e); + rethrow; + } finally { + await PhotoManager.startChangeNotify(); + LocalSyncService.instance.checkAndSync().ignore(); + } +} + +Future _saveLivePhotoOnDroid( + File image, + File video, + EnteFile enteFile, +) async { + debugPrint("Downloading LivePhoto on Droid"); + AssetEntity? savedAsset = await (PhotoManager.editor + .saveImageWithPath(image.path, title: enteFile.title!)); + if (savedAsset == null) { + throw Exception("Failed to save image of live photo"); + } + IgnoredFile ignoreVideoFile = IgnoredFile( + savedAsset.id, + savedAsset.title ?? '', + savedAsset.relativePath ?? 'remoteDownload', + "remoteDownload", + ); + await IgnoredFilesService.instance.cacheAndInsert([ignoreVideoFile]); + final videoTitle = file_path.basenameWithoutExtension(enteFile.title!) + + file_path.extension(video.path); + savedAsset = (await (PhotoManager.editor.saveVideo( + video, + title: videoTitle, + ))); + if (savedAsset == null) { + throw Exception("Failed to save video of live photo"); + } + + ignoreVideoFile = IgnoredFile( + savedAsset.id, + savedAsset.title ?? videoTitle, + savedAsset.relativePath ?? 'remoteDownload', + "remoteDownload", + ); + await IgnoredFilesService.instance.cacheAndInsert([ignoreVideoFile]); +} + Uint8List _decryptFileKey(Map args) { final encryptedKey = CryptoUtil.base642bin(args["encryptedKey"]); final nonce = CryptoUtil.base642bin(args["keyDecryptionNonce"]); diff --git a/mobile/lib/utils/file_uploader.dart b/mobile/lib/utils/file_uploader.dart index d77bc95d7eab55a5ef582a3574dcf2b4adcafedb..bcd5bb121977128b983cd35b38ade63cc0a05cfb 100644 --- a/mobile/lib/utils/file_uploader.dart +++ b/mobile/lib/utils/file_uploader.dart @@ -357,10 +357,16 @@ class FileUploader { final List connections = await (Connectivity().checkConnectivity()); bool canUploadUnderCurrentNetworkConditions = true; - if (connections.any((element) => element == ConnectivityResult.mobile)) { - canUploadUnderCurrentNetworkConditions = - Configuration.instance.shouldBackupOverMobileData(); + if (!Configuration.instance.shouldBackupOverMobileData()) { + if (connections.any((element) => element == ConnectivityResult.mobile)) { + canUploadUnderCurrentNetworkConditions = false; + } else { + _logger.info( + "mobileBackupDisabled, backing up with connections: ${connections.map((e) => e.name).toString()}", + ); + } } + if (!canUploadUnderCurrentNetworkConditions) { throw WiFiUnavailableError(); } @@ -370,7 +376,13 @@ class FileUploader { if (Platform.isAndroid) { final bool hasPermission = await Permission.accessMediaLocation.isGranted; if (!hasPermission) { - throw NoMediaLocationAccessError(); + final permissionStatus = await Permission.accessMediaLocation.request(); + if (!permissionStatus.isGranted) { + _logger.severe( + "Media location access denied with permission status: ${permissionStatus.name}", + ); + throw NoMediaLocationAccessError(); + } } } } diff --git a/mobile/plugins/ente_cast/.metadata b/mobile/plugins/ente_cast/.metadata new file mode 100644 index 0000000000000000000000000000000000000000..9fc7ede54de693b637aba051190f314c5101318c --- /dev/null +++ b/mobile/plugins/ente_cast/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: 0b8abb4724aa590dd0f429683339b1e045a1594d + channel: stable + +project_type: plugin diff --git a/mobile/plugins/ente_cast/analysis_options.yaml b/mobile/plugins/ente_cast/analysis_options.yaml new file mode 100644 index 0000000000000000000000000000000000000000..f04c6cf0f30d4f8306eabe5dfd00f2a4c902b838 --- /dev/null +++ b/mobile/plugins/ente_cast/analysis_options.yaml @@ -0,0 +1 @@ +include: ../../analysis_options.yaml diff --git a/mobile/plugins/ente_cast/lib/ente_cast.dart b/mobile/plugins/ente_cast/lib/ente_cast.dart new file mode 100644 index 0000000000000000000000000000000000000000..f421a92970ae79e9cba2ffcc7dcfd2672d90820b --- /dev/null +++ b/mobile/plugins/ente_cast/lib/ente_cast.dart @@ -0,0 +1,2 @@ +export 'src/model.dart'; +export 'src/service.dart'; diff --git a/mobile/plugins/ente_cast/lib/src/model.dart b/mobile/plugins/ente_cast/lib/src/model.dart new file mode 100644 index 0000000000000000000000000000000000000000..e86582f76fa2705ac46c6c59a3cb7a36433dcb52 --- /dev/null +++ b/mobile/plugins/ente_cast/lib/src/model.dart @@ -0,0 +1,5 @@ +// create enum for type of message for cast +enum CastMessageType { + pairCode, + alreadyCasting, +} diff --git a/mobile/plugins/ente_cast/lib/src/service.dart b/mobile/plugins/ente_cast/lib/src/service.dart new file mode 100644 index 0000000000000000000000000000000000000000..2ab0961dbd6da7b2bb49fbee64474aec0408be43 --- /dev/null +++ b/mobile/plugins/ente_cast/lib/src/service.dart @@ -0,0 +1,18 @@ +import "package:ente_cast/src/model.dart"; +import "package:flutter/widgets.dart"; + +abstract class CastService { + bool get isSupported; + Future> searchDevices(); + Future connectDevice( + BuildContext context, + Object device, { + int? collectionID, + // callback that take a map of string, dynamic + void Function(Map>)? onMessage, + }); + // returns a map of sessionID to deviceNames + Map getActiveSessions(); + + Future closeActiveCasts(); +} diff --git a/mobile/plugins/ente_cast/pubspec.yaml b/mobile/plugins/ente_cast/pubspec.yaml new file mode 100644 index 0000000000000000000000000000000000000000..967e147e91a438f8a9f793420613a903bbe6e9d7 --- /dev/null +++ b/mobile/plugins/ente_cast/pubspec.yaml @@ -0,0 +1,19 @@ +name: ente_cast +version: 0.0.1 +publish_to: none + +environment: + sdk: '>=3.3.0 <4.0.0' + +dependencies: + collection: + dio: ^4.0.6 + flutter: + sdk: flutter + shared_preferences: ^2.0.5 + stack_trace: + +dev_dependencies: + flutter_lints: + +flutter: diff --git a/mobile/plugins/ente_cast_none/.metadata b/mobile/plugins/ente_cast_none/.metadata new file mode 100644 index 0000000000000000000000000000000000000000..9fc7ede54de693b637aba051190f314c5101318c --- /dev/null +++ b/mobile/plugins/ente_cast_none/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: 0b8abb4724aa590dd0f429683339b1e045a1594d + channel: stable + +project_type: plugin diff --git a/mobile/plugins/ente_cast_none/analysis_options.yaml b/mobile/plugins/ente_cast_none/analysis_options.yaml new file mode 100644 index 0000000000000000000000000000000000000000..f04c6cf0f30d4f8306eabe5dfd00f2a4c902b838 --- /dev/null +++ b/mobile/plugins/ente_cast_none/analysis_options.yaml @@ -0,0 +1 @@ +include: ../../analysis_options.yaml diff --git a/mobile/plugins/ente_cast_none/lib/ente_cast_none.dart b/mobile/plugins/ente_cast_none/lib/ente_cast_none.dart new file mode 100644 index 0000000000000000000000000000000000000000..66a7132d8d82e0a34604181fc543461d645858b5 --- /dev/null +++ b/mobile/plugins/ente_cast_none/lib/ente_cast_none.dart @@ -0,0 +1 @@ +export 'src/service.dart'; diff --git a/mobile/plugins/ente_cast_none/lib/src/service.dart b/mobile/plugins/ente_cast_none/lib/src/service.dart new file mode 100644 index 0000000000000000000000000000000000000000..c781889733c149236de513fe6b50394507ff74a4 --- /dev/null +++ b/mobile/plugins/ente_cast_none/lib/src/service.dart @@ -0,0 +1,35 @@ +import "package:ente_cast/ente_cast.dart"; +import "package:flutter/widgets.dart"; + +class CastServiceImpl extends CastService { + @override + Future connectDevice( + BuildContext context, + Object device, { + int? collectionID, + void Function(Map>)? onMessage, + }) { + throw UnimplementedError(); + } + + @override + bool get isSupported => false; + + @override + Future> searchDevices() { + // TODO: implement searchDevices + throw UnimplementedError(); + } + + @override + Future closeActiveCasts() { + // TODO: implement closeActiveCasts + throw UnimplementedError(); + } + + @override + Map getActiveSessions() { + // TODO: implement getActiveSessions + throw UnimplementedError(); + } +} diff --git a/mobile/plugins/ente_cast_none/pubspec.yaml b/mobile/plugins/ente_cast_none/pubspec.yaml new file mode 100644 index 0000000000000000000000000000000000000000..a4559fac5363ed3c7c29227be7627ca252fd3377 --- /dev/null +++ b/mobile/plugins/ente_cast_none/pubspec.yaml @@ -0,0 +1,18 @@ +name: ente_cast_none +version: 0.0.1 +publish_to: none + +environment: + sdk: '>=3.3.0 <4.0.0' + +dependencies: + ente_cast: + path: ../ente_cast + flutter: + sdk: flutter + stack_trace: + +dev_dependencies: + flutter_lints: + +flutter: diff --git a/mobile/plugins/ente_cast_normal/.metadata b/mobile/plugins/ente_cast_normal/.metadata new file mode 100644 index 0000000000000000000000000000000000000000..9fc7ede54de693b637aba051190f314c5101318c --- /dev/null +++ b/mobile/plugins/ente_cast_normal/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: 0b8abb4724aa590dd0f429683339b1e045a1594d + channel: stable + +project_type: plugin diff --git a/mobile/plugins/ente_cast_normal/analysis_options.yaml b/mobile/plugins/ente_cast_normal/analysis_options.yaml new file mode 100644 index 0000000000000000000000000000000000000000..f04c6cf0f30d4f8306eabe5dfd00f2a4c902b838 --- /dev/null +++ b/mobile/plugins/ente_cast_normal/analysis_options.yaml @@ -0,0 +1 @@ +include: ../../analysis_options.yaml diff --git a/mobile/plugins/ente_cast_normal/lib/ente_cast_normal.dart b/mobile/plugins/ente_cast_normal/lib/ente_cast_normal.dart new file mode 100644 index 0000000000000000000000000000000000000000..66a7132d8d82e0a34604181fc543461d645858b5 --- /dev/null +++ b/mobile/plugins/ente_cast_normal/lib/ente_cast_normal.dart @@ -0,0 +1 @@ +export 'src/service.dart'; diff --git a/mobile/plugins/ente_cast_normal/lib/src/service.dart b/mobile/plugins/ente_cast_normal/lib/src/service.dart new file mode 100644 index 0000000000000000000000000000000000000000..8a1f2aaf16b7b03d5f43d6dff418b5aa45c43818 --- /dev/null +++ b/mobile/plugins/ente_cast_normal/lib/src/service.dart @@ -0,0 +1,105 @@ +import "dart:developer" as dev; + +import "package:cast/cast.dart"; +import "package:ente_cast/ente_cast.dart"; +import "package:flutter/material.dart"; + +class CastServiceImpl extends CastService { + final String _appId = 'F5BCEC64'; + final String _pairRequestNamespace = 'urn:x-cast:pair-request'; + final Map collectionIDToSessions = {}; + + @override + Future connectDevice( + BuildContext context, + Object device, { + int? collectionID, + void Function(Map>)? onMessage, + }) async { + final CastDevice castDevice = device as CastDevice; + final session = await CastSessionManager().startSession(castDevice); + session.messageStream.listen((message) { + if (message['type'] == "RECEIVER_STATUS") { + dev.log( + "got RECEIVER_STATUS, Send request to pair", + name: "CastServiceImpl", + ); + session.sendMessage(_pairRequestNamespace, { + "collectionID": collectionID, + }); + } else { + if (onMessage != null && message.containsKey("code")) { + onMessage( + { + CastMessageType.pairCode: message, + }, + ); + } else { + print('receive message: $message'); + } + } + }); + + session.stateStream.listen((state) { + if (state == CastSessionState.connected) { + debugPrint("Send request to pair"); + session.sendMessage(_pairRequestNamespace, {}); + } else if (state == CastSessionState.closed) { + dev.log('Session closed', name: 'CastServiceImpl'); + } + }); + + debugPrint("Send request to launch"); + session.sendMessage(CastSession.kNamespaceReceiver, { + 'type': 'LAUNCH', + 'appId': _appId, // set the appId of your app here + }); + // session.sendMessage('urn:x-cast:pair-request', {}); + } + + @override + Future> searchDevices() { + return CastDiscoveryService() + .search(timeout: const Duration(seconds: 7)) + .then((devices) { + return devices.map((device) => (device.name, device)).toList(); + }); + } + + @override + bool get isSupported => true; + + @override + Future closeActiveCasts() { + final sessions = CastSessionManager().sessions; + for (final session in sessions) { + debugPrint("send close message for ${session.sessionId}"); + Future(() { + session.sendMessage(CastSession.kNamespaceConnection, { + 'type': 'CLOSE', + }); + }).timeout( + const Duration(seconds: 5), + onTimeout: () { + debugPrint('sendMessage timed out after 5 seconds'); + }, + ); + debugPrint("close session ${session.sessionId}"); + session.close(); + } + CastSessionManager().sessions.clear(); + return Future.value(); + } + + @override + Map getActiveSessions() { + final sessions = CastSessionManager().sessions; + final Map result = {}; + for (final session in sessions) { + if (session.state == CastSessionState.connected) { + result[session.sessionId] = session.state.toString(); + } + } + return result; + } +} diff --git a/mobile/plugins/ente_cast_normal/pubspec.lock b/mobile/plugins/ente_cast_normal/pubspec.lock new file mode 100644 index 0000000000000000000000000000000000000000..86051800c6b0a799819cc37f6836d8cd3928eed8 --- /dev/null +++ b/mobile/plugins/ente_cast_normal/pubspec.lock @@ -0,0 +1,333 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + async: + dependency: transitive + description: + name: async + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + url: "https://pub.dev" + source: hosted + version: "2.11.0" + cast: + dependency: "direct main" + description: + path: "." + ref: multicast_version + resolved-ref: "1f39cd4d6efa9363e77b2439f0317bae0c92dda1" + url: "https://github.com/guyluz11/flutter_cast.git" + source: git + version: "2.0.9" + characters: + dependency: transitive + description: + name: characters + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + url: "https://pub.dev" + source: hosted + version: "1.3.0" + collection: + dependency: transitive + description: + name: collection + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + url: "https://pub.dev" + source: hosted + version: "1.18.0" + dio: + dependency: transitive + description: + name: dio + sha256: "7d328c4d898a61efc3cd93655a0955858e29a0aa647f0f9e02d59b3bb275e2e8" + url: "https://pub.dev" + source: hosted + version: "4.0.6" + ente_cast: + dependency: "direct main" + description: + path: "../ente_cast" + relative: true + source: path + version: "0.0.1" + ffi: + dependency: transitive + description: + name: ffi + sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + file: + dependency: transitive + description: + name: file + sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" + url: "https://pub.dev" + source: hosted + version: "7.0.0" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "9e8c3858111da373efc5aa341de011d9bd23e2c5c5e0c62bccf32438e192d7b1" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + http: + dependency: transitive + description: + name: http + sha256: "761a297c042deedc1ffbb156d6e2af13886bb305c2a343a4d972504cd67dd938" + url: "https://pub.dev" + source: hosted + version: "1.2.1" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + url: "https://pub.dev" + source: hosted + version: "4.0.2" + lints: + dependency: transitive + description: + name: lints + sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290 + url: "https://pub.dev" + source: hosted + version: "3.0.0" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" + url: "https://pub.dev" + source: hosted + version: "0.8.0" + meta: + dependency: transitive + description: + name: meta + sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 + url: "https://pub.dev" + source: hosted + version: "1.11.0" + multicast_dns: + dependency: transitive + description: + name: multicast_dns + sha256: "316cc47a958d4bd3c67bd238fe8b44fdfb6133bad89cb191c0c3bd3edb14e296" + url: "https://pub.dev" + source: hosted + version: "0.3.2+6" + path: + dependency: transitive + description: + name: path + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + url: "https://pub.dev" + source: hosted + version: "1.9.0" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" + url: "https://pub.dev" + source: hosted + version: "2.2.1" + platform: + dependency: transitive + description: + name: platform + sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" + url: "https://pub.dev" + source: hosted + version: "3.1.4" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + protobuf: + dependency: transitive + description: + name: protobuf + sha256: "68645b24e0716782e58948f8467fd42a880f255096a821f9e7d0ec625b00c84d" + url: "https://pub.dev" + source: hosted + version: "3.1.0" + shared_preferences: + dependency: transitive + description: + name: shared_preferences + sha256: d3bbe5553a986e83980916ded2f0b435ef2e1893dfaa29d5a7a790d0eca12180 + url: "https://pub.dev" + source: hosted + version: "2.2.3" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: "1ee8bf911094a1b592de7ab29add6f826a7331fb854273d55918693d5364a1f2" + url: "https://pub.dev" + source: hosted + version: "2.2.2" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "7708d83064f38060c7b39db12aefe449cb8cdc031d6062280087bc4cdb988f5c" + url: "https://pub.dev" + source: hosted + version: "2.3.5" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "9f2cbcf46d4270ea8be39fa156d86379077c8a5228d9dfdb1164ae0bb93f1faa" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "22e2ecac9419b4246d7c22bfbbda589e3acf5c0351137d87dd2939d984d37c3b" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: "9aee1089b36bd2aafe06582b7d7817fd317ef05fc30e6ba14bff247d0933042a" + url: "https://pub.dev" + source: hosted + version: "2.3.0" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "841ad54f3c8381c480d0c9b508b89a34036f512482c407e6df7a9c4aa2ef8f59" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.99" + source_span: + dependency: transitive + description: + name: source_span + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + url: "https://pub.dev" + source: hosted + version: "1.10.0" + stack_trace: + dependency: "direct main" + description: + name: stack_trace + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + url: "https://pub.dev" + source: hosted + version: "1.11.1" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.dev" + source: hosted + version: "1.2.1" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + url: "https://pub.dev" + source: hosted + version: "1.3.2" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + web: + dependency: transitive + description: + name: web + sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27" + url: "https://pub.dev" + source: hosted + version: "0.5.1" + win32: + dependency: transitive + description: + name: win32 + sha256: "0a989dc7ca2bb51eac91e8fd00851297cfffd641aa7538b165c62637ca0eaa4a" + url: "https://pub.dev" + source: hosted + version: "5.4.0" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d + url: "https://pub.dev" + source: hosted + version: "1.0.4" +sdks: + dart: ">=3.3.0 <4.0.0" + flutter: ">=3.19.0" diff --git a/mobile/plugins/ente_cast_normal/pubspec.yaml b/mobile/plugins/ente_cast_normal/pubspec.yaml new file mode 100644 index 0000000000000000000000000000000000000000..c97d70a84bed4dc462b92be6657130386a04bd41 --- /dev/null +++ b/mobile/plugins/ente_cast_normal/pubspec.yaml @@ -0,0 +1,22 @@ +name: ente_cast_normal +version: 0.0.1 +publish_to: none + +environment: + sdk: '>=3.3.0 <4.0.0' + +dependencies: + cast: + git: + url: https://github.com/guyluz11/flutter_cast.git + ref: multicast_version + ente_cast: + path: ../ente_cast + flutter: + sdk: flutter + stack_trace: + +dev_dependencies: + flutter_lints: + +flutter: diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index 7ac3445c568d33b08a805d3df0530bc0b3ca1ae9..1d1082bfd3efbe63120b72e703baa86ce3ede404 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -209,6 +209,15 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.1" + cast: + dependency: transitive + description: + path: "." + ref: multicast_version + resolved-ref: "1f39cd4d6efa9363e77b2439f0317bae0c92dda1" + url: "https://github.com/guyluz11/flutter_cast.git" + source: git + version: "2.0.9" characters: dependency: transitive description: @@ -342,10 +351,10 @@ packages: dependency: "direct main" description: name: cupertino_icons - sha256: d57953e10f9f8327ce64a508a355f0b1ec902193f66288e8cb5070e7c47eeb2d + sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 url: "https://pub.dev" source: hosted - version: "1.0.6" + version: "1.0.8" dart_style: dependency: transitive description: @@ -362,14 +371,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.1" - dartx: - dependency: transitive - description: - name: dartx - sha256: "8b25435617027257d43e6508b5fe061012880ddfdaa75a71d607c3de2a13d244" - url: "https://pub.dev" - source: hosted - version: "1.2.0" dbus: dependency: transitive description: @@ -442,6 +443,20 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.17" + ente_cast: + dependency: "direct main" + description: + path: "plugins/ente_cast" + relative: true + source: path + version: "0.0.1" + ente_cast_normal: + dependency: "direct main" + description: + path: "plugins/ente_cast_normal" + relative: true + source: path + version: "0.0.1" ente_feature_flag: dependency: "direct main" description: @@ -1101,30 +1116,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.4" - isar: - dependency: "direct main" - description: - name: isar - sha256: "99165dadb2cf2329d3140198363a7e7bff9bbd441871898a87e26914d25cf1ea" - url: "https://pub.dev" - source: hosted - version: "3.1.0+1" - isar_flutter_libs: - dependency: "direct main" - description: - name: isar_flutter_libs - sha256: bc6768cc4b9c61aabff77152e7f33b4b17d2fc93134f7af1c3dd51500fe8d5e8 - url: "https://pub.dev" - source: hosted - version: "3.1.0+1" - isar_generator: - dependency: "direct dev" - description: - name: isar_generator - sha256: "76c121e1295a30423604f2f819bc255bc79f852f3bc8743a24017df6068ad133" - url: "https://pub.dev" - source: hosted - version: "3.1.0+1" js: dependency: transitive description: @@ -1439,6 +1430,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.2" + multicast_dns: + dependency: transitive + description: + name: multicast_dns + sha256: "316cc47a958d4bd3c67bd238fe8b44fdfb6133bad89cb191c0c3bd3edb14e296" + url: "https://pub.dev" + source: hosted + version: "0.3.2+6" nested: dependency: transitive description: @@ -2213,14 +2212,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.5.9" - time: - dependency: transitive - description: - name: time - sha256: ad8e018a6c9db36cb917a031853a1aae49467a93e0d464683e029537d848c221 - url: "https://pub.dev" - source: hosted - version: "2.1.4" timezone: dependency: transitive description: @@ -2590,14 +2581,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.1" - xxh3: - dependency: transitive - description: - name: xxh3 - sha256: a92b30944a9aeb4e3d4f3c3d4ddb3c7816ca73475cd603682c4f8149690f56d7 - url: "https://pub.dev" - source: hosted - version: "1.0.1" yaml: dependency: transitive description: diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index fbdcd92fa4c365b03af6501c32526393312cab6f..fc4a892647cb97b17bc0064b540dd6c86dd4534e 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.84+604 +version: 0.8.94+614 publish_to: none environment: @@ -48,6 +48,10 @@ dependencies: dotted_border: ^2.1.0 dropdown_button2: ^2.0.0 email_validator: ^2.0.1 + ente_cast: + path: plugins/ente_cast + ente_cast_normal: + path: plugins/ente_cast_normal ente_feature_flag: path: plugins/ente_feature_flag equatable: ^2.0.5 @@ -96,8 +100,6 @@ dependencies: image_editor: ^1.3.0 in_app_purchase: ^3.0.7 intl: ^0.18.0 - isar: ^3.1.0+1 - isar_flutter_libs: ^3.1.0+1 json_annotation: ^4.8.0 latlong2: ^0.9.0 like_button: ^2.0.5 @@ -193,7 +195,6 @@ dev_dependencies: freezed: ^2.5.2 integration_test: sdk: flutter - isar_generator: ^3.1.0+1 json_serializable: ^6.6.1 test: ^1.22.0 diff --git a/mobile/scripts/build_isar.sh b/mobile/scripts/build_isar.sh deleted file mode 100755 index 1bb1d38f6c4cb43236c84eb30a0e88461f704980..0000000000000000000000000000000000000000 --- a/mobile/scripts/build_isar.sh +++ /dev/null @@ -1,17 +0,0 @@ -# TODO: add `rustup@1.25.2` to `srclibs` -# TODO: verify if `gcc-multilib` or `libc-dev` is needed -$$rustup$$/rustup-init.sh -y -source $HOME/.cargo/env -cd thirdparty/isar/ -bash tool/build_android.sh x86 -bash tool/build_android.sh x64 -bash tool/build_android.sh armv7 -bash tool/build_android.sh arm64 -mv libisar_android_arm64.so libisar.so -mv libisar.so $PUB_CACHE/hosted/pub.dev/isar_flutter_libs-*/android/src/main/jniLibs/arm64-v8a/ -mv libisar_android_armv7.so libisar.so -mv libisar.so $PUB_CACHE/hosted/pub.dev/isar_flutter_libs-*/android/src/main/jniLibs/armeabi-v7a/ -mv libisar_android_x64.so libisar.so -mv libisar.so $PUB_CACHE/hosted/pub.dev/isar_flutter_libs-*/android/src/main/jniLibs/x86_64/ -mv libisar_android_x86.so libisar.so -mv libisar.so $PUB_CACHE/hosted/pub.dev/isar_flutter_libs-*/android/src/main/jniLibs/x86/ diff --git a/server/docs/publish.md b/server/docs/publish.md index de4849d900f7870ed252605f5c524b8799264ab6..3a49a47611cd92ee30feace40d8227e7c50192d8 100644 --- a/server/docs/publish.md +++ b/server/docs/publish.md @@ -39,3 +39,7 @@ combine both these steps too. Once the workflow completes, the resultant image will be available at `ghcr.io/ente-io/server`. The image will be tagged by the commit SHA. The latest image will also be tagged, well, "latest". + +The workflow will also tag the commit it used to build the image with +`server/ghcr`. This tag will be overwritten on each publish, and it'll point to +the code that was used in the most recent publish. diff --git a/server/ente/billing.go b/server/ente/billing.go index 20c37bdb5afcdad5024fbc7105f46b35e9f23513..f623a92e85cadbe4fa7f724930f3f8b3a570bb99 100644 --- a/server/ente/billing.go +++ b/server/ente/billing.go @@ -11,7 +11,7 @@ import ( const ( // FreePlanStorage is the amount of storage in free plan - FreePlanStorage = 1 * 1024 * 1024 * 1024 + FreePlanStorage = 5 * 1024 * 1024 * 1024 // FreePlanProductID is the product ID of free plan FreePlanProductID = "free" // FreePlanTransactionID is the dummy transaction ID for the free plan diff --git a/server/ente/cast/entity.go b/server/ente/cast/entity.go index deffa90b9734f27405eac7c98b0d17c9548c8b77..a54d109fcc8c21936deffe1fa73019d72e77bee5 100644 --- a/server/ente/cast/entity.go +++ b/server/ente/cast/entity.go @@ -9,8 +9,7 @@ type CastRequest struct { } type RegisterDeviceRequest struct { - DeviceCode *string `json:"deviceCode"` - PublicKey string `json:"publicKey" binding:"required"` + PublicKey string `json:"publicKey" binding:"required"` } type AuthContext struct { diff --git a/server/migrations/85_increase_free_storage.down.sql b/server/migrations/85_increase_free_storage.down.sql new file mode 100644 index 0000000000000000000000000000000000000000..9f7060a47f15d39a9d3eefbcd01ebf4cba2ba51c --- /dev/null +++ b/server/migrations/85_increase_free_storage.down.sql @@ -0,0 +1 @@ +-- no-op diff --git a/server/migrations/85_increase_free_storage.up.sql b/server/migrations/85_increase_free_storage.up.sql new file mode 100644 index 0000000000000000000000000000000000000000..395033c8dd3c0b4c814cd20f602874f58cc225ab --- /dev/null +++ b/server/migrations/85_increase_free_storage.up.sql @@ -0,0 +1 @@ +UPDATE subscriptions SET storage = 5368709120, expiry_time = 1749355117000000 where storage = 1073741824 and product_id = 'free'; diff --git a/server/pkg/api/cast.go b/server/pkg/api/cast.go index 62d5c94784f1a4d9926c061a922a9233b455ac55..9012624d3258d4dd2f382fea63097d651a466f03 100644 --- a/server/pkg/api/cast.go +++ b/server/pkg/api/cast.go @@ -1,16 +1,16 @@ package api import ( - entity "github.com/ente-io/museum/ente/cast" - "github.com/ente-io/museum/pkg/controller/cast" - "net/http" - "strconv" - "github.com/ente-io/museum/ente" + entity "github.com/ente-io/museum/ente/cast" "github.com/ente-io/museum/pkg/controller" + "github.com/ente-io/museum/pkg/controller/cast" "github.com/ente-io/museum/pkg/utils/handler" "github.com/ente-io/stacktrace" "github.com/gin-gonic/gin" + "net/http" + "strconv" + "strings" ) // CastHandler exposes request handlers for publicly accessible collections @@ -126,7 +126,7 @@ func (h *CastHandler) GetDiff(c *gin.Context) { } func getDeviceCode(c *gin.Context) string { - return c.Param("deviceCode") + return strings.ToUpper(c.Param("deviceCode")) } func (h *CastHandler) getFileForType(c *gin.Context, objectType ente.ObjectType) { diff --git a/server/pkg/controller/cast/controller.go b/server/pkg/controller/cast/controller.go index 4432e149ffb9721d8da9a5bc04a0a4b6955b14db..2bb002f81d404b6aa20a0184cf0a85d8662d3f0c 100644 --- a/server/pkg/controller/cast/controller.go +++ b/server/pkg/controller/cast/controller.go @@ -2,7 +2,6 @@ package cast import ( "context" - "github.com/ente-io/museum/ente" "github.com/ente-io/museum/ente/cast" "github.com/ente-io/museum/pkg/controller/access" castRepo "github.com/ente-io/museum/pkg/repo/cast" @@ -28,7 +27,7 @@ func NewController(castRepo *castRepo.Repository, } func (c *Controller) RegisterDevice(ctx *gin.Context, request *cast.RegisterDeviceRequest) (string, error) { - return c.CastRepo.AddCode(ctx, request.DeviceCode, request.PublicKey, network.GetClientIP(ctx)) + return c.CastRepo.AddCode(ctx, request.PublicKey, network.GetClientIP(ctx)) } func (c *Controller) GetPublicKey(ctx *gin.Context, deviceCode string) (string, error) { @@ -42,7 +41,6 @@ func (c *Controller) GetPublicKey(ctx *gin.Context, deviceCode string) (string, "ip": ip, "clientIP": network.GetClientIP(ctx), }).Warn("GetPublicKey: IP mismatch") - return "", &ente.ErrCastIPMismatch } return pubKey, nil } diff --git a/server/pkg/controller/embedding/controller.go b/server/pkg/controller/embedding/controller.go index d6e78209fa3cac5b6702251b6a4b4dd747a39cd9..342411ea31083473a82932846afea4632d69584a 100644 --- a/server/pkg/controller/embedding/controller.go +++ b/server/pkg/controller/embedding/controller.go @@ -350,6 +350,10 @@ func (c *Controller) getEmbeddingObjectsParallelV2(userID int64, dbEmbeddingRows } func (c *Controller) getEmbeddingObject(objectKey string, downloader *s3manager.Downloader) (ente.EmbeddingObject, error) { + return c.getEmbeddingObjectWithRetries(objectKey, downloader, 3) +} + +func (c *Controller) getEmbeddingObjectWithRetries(objectKey string, downloader *s3manager.Downloader, retryCount int) (ente.EmbeddingObject, error) { var obj ente.EmbeddingObject buff := &aws.WriteAtBuffer{} _, err := downloader.Download(buff, &s3.GetObjectInput{ @@ -358,6 +362,9 @@ func (c *Controller) getEmbeddingObject(objectKey string, downloader *s3manager. }) if err != nil { log.Error(err) + if retryCount > 0 { + return c.getEmbeddingObjectWithRetries(objectKey, downloader, retryCount-1) + } return obj, stacktrace.Propagate(err, "") } err = json.Unmarshal(buff.Bytes(), &obj) diff --git a/server/pkg/controller/storagebonus/referral.go b/server/pkg/controller/storagebonus/referral.go index b452484f412191714f1dc8222f8beb7526d504ab..5bdd951f8d7ccaf0ac285aa6de92f6c8c2c7781d 100644 --- a/server/pkg/controller/storagebonus/referral.go +++ b/server/pkg/controller/storagebonus/referral.go @@ -3,7 +3,7 @@ package storagebonus import ( "database/sql" "errors" - "fmt" + "github.com/ente-io/museum/pkg/utils/random" "github.com/ente-io/museum/ente" entity "github.com/ente-io/museum/ente/storagebonus" @@ -119,7 +119,7 @@ func (c *Controller) GetOrCreateReferralCode(ctx *gin.Context, userID int64) (*s if !errors.Is(err, sql.ErrNoRows) { return nil, stacktrace.Propagate(err, "failed to get storagebonus code") } - code, err := generateAlphaNumString(codeLength) + code, err := random.GenerateAlphaNumString(codeLength) if err != nil { return nil, stacktrace.Propagate(err, "") } @@ -131,30 +131,3 @@ func (c *Controller) GetOrCreateReferralCode(ctx *gin.Context, userID int64) (*s } return referralCode, nil } - -// generateAlphaNumString returns AlphaNumeric code of given length -// which exclude number 0 and letter O. The code always starts with an -// alphabet -func generateAlphaNumString(length int) (string, error) { - // Define the alphabet and numbers to be used in the string. - alphabet := "ABCDEFGHIJKLMNPQRSTUVWXYZ" - // Define the alphabet and numbers to be used in the string. - alphaNum := fmt.Sprintf("%s123456789", alphabet) - // Allocate a byte slice with the desired length. - result := make([]byte, length) - // Generate the first letter as an alphabet. - r0, err := auth.GenerateRandomInt(int64(len(alphabet))) - if err != nil { - return "", stacktrace.Propagate(err, "") - } - result[0] = alphabet[r0] - // Generate the remaining characters as alphanumeric. - for i := 1; i < length; i++ { - ri, err := auth.GenerateRandomInt(int64(len(alphaNum))) - if err != nil { - return "", stacktrace.Propagate(err, "") - } - result[i] = alphaNum[ri] - } - return string(result), nil -} diff --git a/server/pkg/repo/cast/repo.go b/server/pkg/repo/cast/repo.go index 89ebc408382b1e775bdfc28924a1844e4d610e6c..2f4446c9d0c5e4fcb0ab3acc4e75174726c7667a 100644 --- a/server/pkg/repo/cast/repo.go +++ b/server/pkg/repo/cast/repo.go @@ -8,23 +8,16 @@ import ( "github.com/ente-io/stacktrace" "github.com/google/uuid" log "github.com/sirupsen/logrus" - "strings" ) type Repository struct { DB *sql.DB } -func (r *Repository) AddCode(ctx context.Context, code *string, pubKey string, ip string) (string, error) { - var codeValue string - var err error - if code == nil || *code == "" { - codeValue, err = random.GenerateSixDigitOtp() - if err != nil { - return "", stacktrace.Propagate(err, "") - } - } else { - codeValue = strings.TrimSpace(*code) +func (r *Repository) AddCode(ctx context.Context, pubKey string, ip string) (string, error) { + codeValue, err := random.GenerateAlphaNumString(6) + if err != nil { + return "", err } _, err = r.DB.ExecContext(ctx, "INSERT INTO casting (code, public_key, id, ip) VALUES ($1, $2, $3, $4)", codeValue, pubKey, uuid.New(), ip) if err != nil { diff --git a/server/pkg/repo/user.go b/server/pkg/repo/user.go index 596d24c64c6fdd359aad692a4b3b1e72297eb7d2..f35a47e1f9975486148318d120af74e5c3c49f5b 100644 --- a/server/pkg/repo/user.go +++ b/server/pkg/repo/user.go @@ -194,8 +194,8 @@ func (repo *UserRepository) UpdateEmail(userID int64, encryptedEmail ente.Encryp // GetUserIDWithEmail returns the userID associated with a provided email func (repo *UserRepository) GetUserIDWithEmail(email string) (int64, error) { - trimmedEmail := strings.TrimSpace(email) - emailHash, err := crypto.GetHash(trimmedEmail, repo.HashingKey) + sanitizedEmail := strings.ToLower(strings.TrimSpace(email)) + emailHash, err := crypto.GetHash(sanitizedEmail, repo.HashingKey) if err != nil { return -1, stacktrace.Propagate(err, "") } diff --git a/server/pkg/utils/random/generate.go b/server/pkg/utils/random/generate.go index 47932b66032f63b1da23026a7994f0ec0c63fde0..75a811c8e1789cf160f8e423875877b9ca351fa7 100644 --- a/server/pkg/utils/random/generate.go +++ b/server/pkg/utils/random/generate.go @@ -13,3 +13,30 @@ func GenerateSixDigitOtp() (string, error) { } return fmt.Sprintf("%06d", n), nil } + +// GenerateAlphaNumString returns AlphaNumeric code of given length +// which exclude number 0 and letter O. The code always starts with an +// alphabet +func GenerateAlphaNumString(length int) (string, error) { + // Define the alphabet and numbers to be used in the string. + alphabet := "ABCDEFGHIJKLMNPQRSTUVWXYZ" + // Define the alphabet and numbers to be used in the string. + alphaNum := fmt.Sprintf("%s123456789", alphabet) + // Allocate a byte slice with the desired length. + result := make([]byte, length) + // Generate the first letter as an alphabet. + r0, err := auth.GenerateRandomInt(int64(len(alphabet))) + if err != nil { + return "", stacktrace.Propagate(err, "") + } + result[0] = alphabet[r0] + // Generate the remaining characters as alphanumeric. + for i := 1; i < length; i++ { + ri, err := auth.GenerateRandomInt(int64(len(alphaNum))) + if err != nil { + return "", stacktrace.Propagate(err, "") + } + result[i] = alphaNum[ri] + } + return string(result), nil +} diff --git a/web/apps/cast/package.json b/web/apps/cast/package.json index 012148969ad057ffa89adac120c671a573eb64cd..4f774662ad17dc28b0de9e6bcb6b7e70dac83026 100644 --- a/web/apps/cast/package.json +++ b/web/apps/cast/package.json @@ -8,5 +8,8 @@ "@ente/accounts": "*", "@ente/eslint-config": "*", "@ente/shared": "*" + }, + "devDependencies": { + "@types/chromecast-caf-receiver": "^6.0.14" } } diff --git a/web/apps/cast/src/components/FilledCircleCheck.tsx b/web/apps/cast/src/components/FilledCircleCheck.tsx index c0635f138afe9e93b34a4ea98c1136d527745367..ba2292922ebbf615d84e32380e74c5923c329d61 100644 --- a/web/apps/cast/src/components/FilledCircleCheck.tsx +++ b/web/apps/cast/src/components/FilledCircleCheck.tsx @@ -1,6 +1,6 @@ import { styled } from "@mui/material"; -const FilledCircleCheck = () => { +export const FilledCircleCheck: React.FC = () => { return ( @@ -11,8 +11,6 @@ const FilledCircleCheck = () => { ); }; -export default FilledCircleCheck; - const Container = styled("div")` width: 100px; height: 100px; diff --git a/web/apps/cast/src/components/PairedSuccessfullyOverlay.tsx b/web/apps/cast/src/components/PairedSuccessfullyOverlay.tsx deleted file mode 100644 index 845416fedc9113092a63b73a5c42846e8415f558..0000000000000000000000000000000000000000 --- a/web/apps/cast/src/components/PairedSuccessfullyOverlay.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import FilledCircleCheck from "./FilledCircleCheck"; - -export default function PairedSuccessfullyOverlay() { - return ( -
-
- -

- Pairing Complete -

-

- We're preparing your album. -
This should only take a few seconds. -

-
-
- ); -} diff --git a/web/apps/cast/src/components/LargeType.tsx b/web/apps/cast/src/components/PairingCode.tsx similarity index 74% rename from web/apps/cast/src/components/LargeType.tsx rename to web/apps/cast/src/components/PairingCode.tsx index ecf7a201bbeea024acbc20598898c596c5e648eb..fa1474bafc34c02d9261f1a89a8368d8b2a988a8 100644 --- a/web/apps/cast/src/components/LargeType.tsx +++ b/web/apps/cast/src/components/PairingCode.tsx @@ -1,6 +1,6 @@ import { styled } from "@mui/material"; -const colourPool = [ +const colors = [ "#87CEFA", // Light Blue "#90EE90", // Light Green "#F08080", // Light Coral @@ -23,27 +23,34 @@ const colourPool = [ "#808000", // Light Olive ]; -export default function LargeType({ chars }: { chars: string[] }) { +interface PairingCodeProps { + code: string; +} + +export const PairingCode: React.FC = ({ code }) => { return ( - - {chars.map((char, i) => ( + + {code.split("").map((char, i) => ( {char} ))} - + ); -} +}; + +const PairingCode_ = styled("div")` + border-radius: 10px; + overflow: hidden; -const Container = styled("div")` font-size: 4rem; font-weight: bold; font-family: monospace; diff --git a/web/apps/cast/src/components/PhotoAuditorium.tsx b/web/apps/cast/src/components/Slide.tsx similarity index 70% rename from web/apps/cast/src/components/PhotoAuditorium.tsx rename to web/apps/cast/src/components/Slide.tsx index 6aa2c3990b081b499ac636667a232b71689eb775..8309f8bc2c410c903771f40fb968e92019297e8f 100644 --- a/web/apps/cast/src/components/PhotoAuditorium.tsx +++ b/web/apps/cast/src/components/Slide.tsx @@ -1,25 +1,17 @@ -import { useEffect } from "react"; - -interface PhotoAuditoriumProps { +interface SlideViewProps { + /** The URL of the image to show. */ url: string; - nextSlideUrl: string; - showNextSlide: () => void; + /** The URL of the next image that we will transition to. */ + nextURL: string; } -export const PhotoAuditorium: React.FC = ({ - url, - nextSlideUrl, - showNextSlide, -}) => { - useEffect(() => { - const timeoutId = window.setTimeout(() => { - showNextSlide(); - }, 10000); - - return () => { - if (timeoutId) clearTimeout(timeoutId); - }; - }, [showNextSlide]); +/** + * Show the image at {@link url} in a full screen view. + * + * Also show {@link nextURL} in an hidden image view to prepare the browser for + * an imminent transition to it. + */ +export const SlideView: React.FC = ({ url, nextURL }) => { return (
= ({ }} > { - const array = new Uint8Array(length); - window.crypto.getRandomValues(array); - // Modulo operation to ensure each byte is a single digit - for (let i = 0; i < length; i++) { - array[i] = array[i] % 10; - } - return array; -}; - -const convertDataToDecimalString = (data: Uint8Array): string => { - let decimalString = ""; - for (let i = 0; i < data.length; i++) { - decimalString += data[i].toString(); // No need to pad, as each value is a single digit - } - return decimalString; -}; - -export default function PairingMode() { - const [digits, setDigits] = useState([]); - const [publicKeyB64, setPublicKeyB64] = useState(""); - const [privateKeyB64, setPrivateKeyB64] = useState(""); - const [codePending, setCodePending] = useState(true); - const [isCastReady, setIsCastReady] = useState(false); +export default function Index() { + const [publicKeyB64, setPublicKeyB64] = useState(); + const [privateKeyB64, setPrivateKeyB64] = useState(); + const [pairingCode, setPairingCode] = useState(); - const { cast } = useCastReceiver(); + // Keep a boolean flag to ensure that Cast Receiver starts only once even if + // pairing codes change. + const [haveInitializedCast, setHaveInitializedCast] = useState(false); - useEffect(() => { - init(); - }, []); + const router = useRouter(); useEffect(() => { - if (!cast) { - return; - } - if (isCastReady) { - return; - } - const context = cast.framework.CastReceiverContext.getInstance(); - - try { - const options = new cast.framework.CastReceiverOptions(); - options.maxInactivity = 3600; - options.customNamespaces = Object.assign({}); - options.customNamespaces["urn:x-cast:pair-request"] = - cast.framework.system.MessageType.JSON; - - options.disableIdleTimeout = true; - context.set; - - context.addCustomMessageListener( - "urn:x-cast:pair-request", - messageReceiveHandler, - ); - - // listen to close request and stop the context - context.addEventListener( - cast.framework.system.EventType.SENDER_DISCONNECTED, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - (_) => { - context.stop(); - }, - ); - context.start(options); - setIsCastReady(true); - } catch (e) { - log.error("failed to create cast context", e); + if (!pairingCode) { + register().then((r) => { + setPublicKeyB64(r.publicKeyB64); + setPrivateKeyB64(r.privateKeyB64); + setPairingCode(r.pairingCode); + }); + } else { + if (!haveInitializedCast) { + castReceiverLoadingIfNeeded().then((cast) => { + setHaveInitializedCast(true); + advertiseCode(cast, () => pairingCode); + }); + } } + }, [pairingCode]); - return () => { - // context.stop(); - }; - }, [cast]); - - const messageReceiveHandler = (message: { - type: string; - senderId: string; - data: any; - }) => { - try { - cast.framework.CastReceiverContext.getInstance().sendCustomMessage( - "urn:x-cast:pair-request", - message.senderId, - { - code: digits.join(""), - }, - ); - } catch (e) { - log.error("failed to send message", e); - } - }; - - const init = async () => { - try { - const data = generateSecureData(6); - setDigits(convertDataToDecimalString(data).split("")); - const keypair = await generateKeyPair(); - setPublicKeyB64(await toB64(keypair.publicKey)); - setPrivateKeyB64(await toB64(keypair.privateKey)); - } catch (e) { - log.error("failed to generate keypair", e); - throw e; - } - }; - - const generateKeyPair = async () => { - await _sodium.ready; - - const keypair = _sodium.crypto_box_keypair(); + useEffect(() => { + if (!publicKeyB64 || !privateKeyB64 || !pairingCode) return; - return keypair; - }; + const interval = setInterval(pollTick, 2000); + return () => clearInterval(interval); + }, [publicKeyB64, privateKeyB64, pairingCode]); - const pollForCastData = async () => { - if (codePending) { - return; - } - // see if we were acknowledged on the client. - // the client will send us the encrypted payload using our public key that we advertised. - // then, we can decrypt this and store all the necessary info locally so we can play the collection slideshow. - let devicePayload = ""; + const pollTick = async () => { + const registration = { publicKeyB64, privateKeyB64, pairingCode }; try { - const encDastData = await castGateway.getCastData( - `${digits.join("")}`, - ); - if (!encDastData) return; - devicePayload = encDastData; - } catch (e) { - setCodePending(true); - init(); - return; - } - - const decryptedPayload = await boxSealOpen( - devicePayload, - publicKeyB64, - privateKeyB64, - ); - - const decryptedPayloadObj = JSON.parse(atob(decryptedPayload)); - - return decryptedPayloadObj; - }; + const data = await getCastData(registration); + if (!data) { + // No one has connected yet. + return; + } - const advertisePublicKey = async (publicKeyB64: string) => { - // hey client, we exist! - try { - await castGateway.registerDevice( - `${digits.join("")}`, - publicKeyB64, - ); - setCodePending(false); + log.info("Pairing complete"); + storeCastData(data); + await router.push("/slideshow"); } catch (e) { - // schedule re-try after 5 seconds - setTimeout(() => { - init(); - }, 5000); - return; + // The pairing code becomes invalid after an hour, which will cause + // `getCastData` to fail. There might be other reasons this might + // fail too, but in all such cases, it is a reasonable idea to start + // again from the beginning. + log.warn("Failed to get cast data", e); + setPairingCode(undefined); } }; - const router = useRouter(); - - useEffect(() => { - if (digits.length < 1 || !publicKeyB64 || !privateKeyB64) return; - - const interval = setInterval(async () => { - const data = await pollForCastData(); - if (!data) return; - storeCastData(data); - await router.push("/slideshow"); - }, 1000); - - return () => { - clearInterval(interval); - }; - }, [digits, publicKeyB64, privateKeyB64, codePending]); - - useEffect(() => { - if (!publicKeyB64) return; - advertisePublicKey(publicKeyB64); - }, [publicKeyB64]); - return ( - <> -
-
- -

- Enter this code on ente to pair this TV -

-
- {codePending ? ( - - ) : ( - <> - - - )} -
-

- Visit{" "} - - ente.io/cast - {" "} - for help -

-
-
- + + +

+ Enter this code on Ente Photos to pair this screen +

+ {pairingCode ? : } +

+ Visit{" "} + + ente.io/cast + {" "} + for help +

+
); } + +const Container = styled("div")` + height: 100%; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + text-align: center; + + h1 { + font-weight: normal; + } + + p { + font-size: 1.2rem; + } + a { + text-decoration: none; + color: #87cefa; + font-weight: bold; + } +`; + +const Spinner: React.FC = () => ( + + + +); + +const Spinner_ = styled("div")` + /* Roughly same height as the pairing code section to roduce layout shift */ + margin-block: 1.7rem; +`; diff --git a/web/apps/cast/src/pages/slideshow.tsx b/web/apps/cast/src/pages/slideshow.tsx index 99b2209decc482eed5428580fd2212c309addfaa..d117f6da7d143960d4e02d41282e09b27cc1a6f0 100644 --- a/web/apps/cast/src/pages/slideshow.tsx +++ b/web/apps/cast/src/pages/slideshow.tsx @@ -1,153 +1,112 @@ -import { FILE_TYPE } from "@/media/file-type"; import log from "@/next/log"; -import PairedSuccessfullyOverlay from "components/PairedSuccessfullyOverlay"; -import { PhotoAuditorium } from "components/PhotoAuditorium"; +import { styled } from "@mui/material"; +import { FilledCircleCheck } from "components/FilledCircleCheck"; +import { SlideView } from "components/Slide"; import { useRouter } from "next/router"; import { useEffect, useState } from "react"; -import { - getCastCollection, - getLocalFiles, - syncPublicFiles, -} from "services/cast/castService"; -import { Collection } from "types/collection"; -import { EnteFile } from "types/file"; -import { getPreviewableImage, isRawFileFromFileName } from "utils/file"; - -const renderableFileURLCache = new Map(); +import { readCastData, renderableImageURLs } from "services/cast"; export default function Slideshow() { const [loading, setLoading] = useState(true); - const [castToken, setCastToken] = useState(""); - const [castCollection, setCastCollection] = useState< - Collection | undefined - >(); - const [collectionFiles, setCollectionFiles] = useState([]); - const [currentFileId, setCurrentFileId] = useState(); - const [currentFileURL, setCurrentFileURL] = useState(); - const [nextFileURL, setNextFileURL] = useState(); + const [imageURL, setImageURL] = useState(); + const [nextImageURL, setNextImageURL] = useState(); + const [isEmpty, setIsEmpty] = useState(false); const router = useRouter(); - const syncCastFiles = async (token: string) => { - try { - const castToken = window.localStorage.getItem("castToken"); - const requestedCollectionKey = - window.localStorage.getItem("collectionKey"); - const collection = await getCastCollection( - castToken, - requestedCollectionKey, - ); - if ( - castCollection === undefined || - castCollection.updationTime !== collection.updationTime - ) { - setCastCollection(collection); - await syncPublicFiles(token, collection, () => {}); - const files = await getLocalFiles(String(collection.id)); - setCollectionFiles( - files.filter((file) => isFileEligibleForCast(file)), - ); - } - } catch (e) { - log.error("error during sync", e); - router.push("/"); - } - }; + /** Go back to pairing page */ + const pair = () => router.push("/"); useEffect(() => { - if (castToken) { - const intervalId = setInterval(() => { - syncCastFiles(castToken); - }, 10000); - syncCastFiles(castToken); - - return () => clearInterval(intervalId); - } - }, [castToken]); + let stop = false; - const isFileEligibleForCast = (file: EnteFile) => { - const fileType = file.metadata.fileType; - if (fileType !== FILE_TYPE.IMAGE && fileType !== FILE_TYPE.LIVE_PHOTO) - return false; - - if (file.info.fileSize > 100 * 1024 * 1024) return false; - - if (isRawFileFromFileName(file.metadata.title)) return false; + const loop = async () => { + try { + const urlGenerator = renderableImageURLs(readCastData()); + while (!stop) { + const { value: urls, done } = await urlGenerator.next(); + if (done) { + // No items in this callection can be shown. + setIsEmpty(true); + // Go back to pairing screen after 3 seconds. + setTimeout(pair, 5000); + return; + } + + setImageURL(urls[0]); + setNextImageURL(urls[1]); + setLoading(false); + } + } catch (e) { + log.error("Failed to prepare generator", e); + pair(); + } + }; - return true; - }; + void loop(); - useEffect(() => { - try { - const castToken = window.localStorage.getItem("castToken"); - // Wait 2 seconds to ensure the green tick and the confirmation - // message remains visible for at least 2 seconds before we start - // the slideshow. - const timeoutId = setTimeout(() => { - setCastToken(castToken); - }, 2000); - - return () => clearTimeout(timeoutId); - } catch (e) { - log.error("error during sync", e); - router.push("/"); - } + return () => { + stop = true; + }; }, []); - useEffect(() => { - if (collectionFiles.length < 1) return; - showNextSlide(); - }, [collectionFiles]); + console.log("Rendering slideshow", { loading, imageURL, nextImageURL }); - const showNextSlide = async () => { - const currentIndex = collectionFiles.findIndex( - (file) => file.id === currentFileId, - ); + if (loading) return ; + if (isEmpty) return ; - const nextIndex = (currentIndex + 1) % collectionFiles.length; - const nextNextIndex = (nextIndex + 1) % collectionFiles.length; + return ; +} - const nextFile = collectionFiles[nextIndex]; - const nextNextFile = collectionFiles[nextNextIndex]; +const PairingComplete: React.FC = () => { + return ( + + +

Pairing Complete

+

+ We're preparing your album. +
This should only take a few seconds. +

+
+ ); +}; - let nextURL = renderableFileURLCache.get(nextFile.id); - let nextNextURL = renderableFileURLCache.get(nextNextFile.id); +const Message: React.FC = ({ children }) => { + return ( + + {children} + + ); +}; - if (!nextURL) { - try { - const blob = await getPreviewableImage(nextFile, castToken); - const url = URL.createObjectURL(blob); - renderableFileURLCache.set(nextFile.id, url); - nextURL = url; - } catch (e) { - return; - } - } +const Message_ = styled("div")` + display: flex; + min-height: 100svh; + justify-content: center; + align-items: center; - if (!nextNextURL) { - try { - const blob = await getPreviewableImage(nextNextFile, castToken); - const url = URL.createObjectURL(blob); - renderableFileURLCache.set(nextNextFile.id, url); - nextNextURL = url; - } catch (e) { - return; - } - } + line-height: 1.5rem; - setLoading(false); - setCurrentFileId(nextFile.id); - setCurrentFileURL(nextURL); - setNextFileURL(nextNextURL); - }; + h2 { + margin-block-end: 0; + } +`; - if (loading) return ; +const MessageItems = styled("div")` + display: flex; + flex-direction: column; + align-items: center; + text-align: center; +`; +const NoItems: React.FC = () => { return ( - + +

Try another album

+

+ This album has no photos that can be shown here +
Please try another album +

+
); -} +}; diff --git a/web/apps/cast/src/services/cast.ts b/web/apps/cast/src/services/cast.ts new file mode 100644 index 0000000000000000000000000000000000000000..38f203db24c3e81de20572560726f41ed6afbd45 --- /dev/null +++ b/web/apps/cast/src/services/cast.ts @@ -0,0 +1,336 @@ +import { FILE_TYPE } from "@/media/file-type"; +import { isNonWebImageFileExtension } from "@/media/formats"; +import { decodeLivePhoto } from "@/media/live-photo"; +import { nameAndExtension } from "@/next/file"; +import log from "@/next/log"; +import { shuffled } from "@/utils/array"; +import { ensure, ensureString } from "@/utils/ensure"; +import ComlinkCryptoWorker from "@ente/shared/crypto"; +import HTTPService from "@ente/shared/network/HTTPService"; +import { getCastFileURL, getEndpoint } from "@ente/shared/network/api"; +import { wait } from "@ente/shared/utils"; +import { detectMediaMIMEType } from "services/detect-type"; +import { + EncryptedEnteFile, + EnteFile, + FileMagicMetadata, + FilePublicMagicMetadata, +} from "types/file"; + +/** + * Save the data received after pairing with a sender into local storage. + * + * We will read in back when we start the slideshow. + */ +export const storeCastData = (payload: unknown) => { + if (!payload || typeof payload != "object") + throw new Error("Unexpected cast data"); + + // Iterate through all the keys of the payload object and save them to + // localStorage. We don't validate here, we'll validate when we read these + // values back in `readCastData`. + for (const key in payload) { + window.localStorage.setItem(key, payload[key]); + } +}; + +interface CastData { + /** A key to decrypt the collection we are casting. */ + collectionKey: string; + /** A credential to use for fetching media files for this cast session. */ + castToken: string; +} + +/** + * Read back the cast data we got after pairing. + * + * Sibling of {@link storeCastData}. It throws an error if the expected data is + * not present in localStorage. + */ +export const readCastData = (): CastData => { + const collectionKey = ensureString(localStorage.getItem("collectionKey")); + const castToken = ensureString(localStorage.getItem("castToken")); + return { collectionKey, castToken }; +}; + +type RenderableImageURLPair = [url: string, nextURL: string]; + +/** + * An async generator function that loops through all the files in the + * collection, returning renderable URLs to each that can be displayed in a + * slideshow. + * + * Each time it resolves with a pair of URLs (a {@link RenderableImageURLPair}), + * one for the next slideshow image, and one for the slideshow image that will + * be displayed after that. It also pre-fetches the next to next URL each time. + * + * If there are no renderable image in the collection, the sequence ends by + * yielding `{done: true}`. + * + * Otherwise when the generator reaches the end of the collection, it starts + * from the beginning again. So the sequence will continue indefinitely for + * non-empty collections. + * + * The generator ignores errors in the fetching and decoding of individual + * images in the collection, skipping the erroneous ones and moving onward to + * the next one. It will however throw if there are errors when getting the + * collection itself. This can happen both the first time, or when we are about + * to loop around to the start of the collection. + * + * @param castData The collection to show and credentials to fetch the files + * within it. + */ +export const renderableImageURLs = async function* (castData: CastData) { + const { collectionKey, castToken } = castData; + + /** + * Keep a FIFO queue of the URLs that we've vended out recently so that we + * can revoke those that are not being shown anymore. + */ + const previousURLs: string[] = []; + + /** The URL pair that we will yield */ + const urls: string[] = []; + + /** Number of milliseconds to keep the slide on the screen. */ + const slideDuration = 10000; /* 10 s */ + + /** + * Time when we last yielded. + * + * We use this to keep an roughly periodic spacing between yields that + * accounts for the time we spend fetching and processing the images. + */ + let lastYieldTime = Date.now(); + + // The first time around regress the lastYieldTime into the past so that + // we don't wait around too long for the first slide (we do want to wait a + // bit, for the user to see the checkmark animation as reassurance). + lastYieldTime -= slideDuration - 2500; /* wait at most 2.5 s */ + + while (true) { + const encryptedFiles = shuffled( + await getEncryptedCollectionFiles(castToken), + ); + + let haveEligibleFiles = false; + + for (const encryptedFile of encryptedFiles) { + const file = await decryptEnteFile(encryptedFile, collectionKey); + + if (!isFileEligibleForCast(file)) continue; + + console.log("will start createRenderableURL", new Date()); + try { + urls.push(await createRenderableURL(castToken, file)); + haveEligibleFiles = true; + } catch (e) { + log.error("Skipping unrenderable file", e); + continue; + } + + console.log("did end createRenderableURL", new Date()); + + // Need at least a pair. + // + // There are two scenarios: + // + // - First run: urls will initially be empty, so gobble two. + // + // - Subsequently, urls will have the "next" / "preloaded" URL left + // over from the last time. We'll promote that to being the one + // that'll get displayed, and preload another one. + // if (urls.length < 2) continue; + + // The last element of previousURLs is the URL that is currently + // being shown on screen. + // + // The last to last element is the one that was shown prior to that, + // and now can be safely revoked. + if (previousURLs.length > 1) + URL.revokeObjectURL(previousURLs.shift()); + + // The URL that'll now get displayed on screen. + const url = ensure(urls.shift()); + // The URL that we're preloading for next time around. + const nextURL = ""; //ensure(urls[0]); + + previousURLs.push(url); + + const urlPair: RenderableImageURLPair = [url, nextURL]; + + const elapsedTime = Date.now() - lastYieldTime; + if (elapsedTime > 0 && elapsedTime < slideDuration) { + console.log("waiting", slideDuration - elapsedTime); + await wait(slideDuration - elapsedTime); + } + + lastYieldTime = Date.now(); + yield urlPair; + } + + // This collection does not have any files that we can show. + if (!haveEligibleFiles) return; + } +}; + +/** + * Fetch the list of non-deleted files in the given collection. + * + * The returned files are not decrypted yet, so their metadata will not be + * readable. + */ +const getEncryptedCollectionFiles = async ( + castToken: string, +): Promise => { + let files: EncryptedEnteFile[] = []; + let sinceTime = 0; + let resp; + do { + resp = await HTTPService.get( + `${getEndpoint()}/cast/diff`, + { sinceTime }, + { + "Cache-Control": "no-cache", + "X-Cast-Access-Token": castToken, + }, + ); + const diff = resp.data.diff; + files = files.concat(diff.filter((file: EnteFile) => !file.isDeleted)); + sinceTime = diff.reduce( + (max: number, file: EnteFile) => Math.max(max, file.updationTime), + sinceTime, + ); + } while (resp.data.hasMore); + return files; +}; + +/** + * Decrypt the given {@link EncryptedEnteFile}, returning a {@link EnteFile}. + */ +const decryptEnteFile = async ( + encryptedFile: EncryptedEnteFile, + collectionKey: string, +): Promise => { + const worker = await ComlinkCryptoWorker.getInstance(); + const { + encryptedKey, + keyDecryptionNonce, + metadata, + magicMetadata, + pubMagicMetadata, + ...restFileProps + } = encryptedFile; + const fileKey = await worker.decryptB64( + encryptedKey, + keyDecryptionNonce, + collectionKey, + ); + const fileMetadata = await worker.decryptMetadata( + metadata.encryptedData, + metadata.decryptionHeader, + fileKey, + ); + let fileMagicMetadata: FileMagicMetadata; + let filePubMagicMetadata: FilePublicMagicMetadata; + if (magicMetadata?.data) { + fileMagicMetadata = { + ...encryptedFile.magicMetadata, + data: await worker.decryptMetadata( + magicMetadata.data, + magicMetadata.header, + fileKey, + ), + }; + } + if (pubMagicMetadata?.data) { + filePubMagicMetadata = { + ...pubMagicMetadata, + data: await worker.decryptMetadata( + pubMagicMetadata.data, + pubMagicMetadata.header, + fileKey, + ), + }; + } + const file = { + ...restFileProps, + key: fileKey, + metadata: fileMetadata, + magicMetadata: fileMagicMetadata, + pubMagicMetadata: filePubMagicMetadata, + }; + if (file.pubMagicMetadata?.data.editedTime) { + file.metadata.creationTime = file.pubMagicMetadata.data.editedTime; + } + if (file.pubMagicMetadata?.data.editedName) { + file.metadata.title = file.pubMagicMetadata.data.editedName; + } + return file; +}; + +const isFileEligibleForCast = (file: EnteFile) => { + if (!isImageOrLivePhoto(file)) return false; + if (file.info.fileSize > 100 * 1024 * 1024) return false; + + const [, extension] = nameAndExtension(file.metadata.title); + if (isNonWebImageFileExtension(extension)) return false; + + return true; +}; + +const isImageOrLivePhoto = (file: EnteFile) => { + const fileType = file.metadata.fileType; + return fileType == FILE_TYPE.IMAGE || fileType == FILE_TYPE.LIVE_PHOTO; +}; + +/** + * Create and return a new data URL that can be used to show the given + * {@link file} in our slideshow image viewer. + * + * Once we're done showing the file, the URL should be revoked using + * {@link URL.revokeObjectURL} to free up browser resources. + */ +const createRenderableURL = async (castToken: string, file: EnteFile) => + URL.createObjectURL(await renderableImageBlob(castToken, file)); + +const renderableImageBlob = async (castToken: string, file: EnteFile) => { + const fileName = file.metadata.title; + let blob = await downloadFile(castToken, file); + if (file.metadata.fileType === FILE_TYPE.LIVE_PHOTO) { + const { imageData } = await decodeLivePhoto(fileName, blob); + blob = new Blob([imageData]); + } + const mimeType = await detectMediaMIMEType(new File([blob], fileName)); + if (!mimeType) + throw new Error(`Could not detect MIME type for file ${fileName}`); + return new Blob([blob], { type: mimeType }); +}; + +const downloadFile = async (castToken: string, file: EnteFile) => { + if (!isImageOrLivePhoto(file)) + throw new Error("Can only cast images and live photos"); + + const url = getCastFileURL(file.id); + // TODO(MR): Remove if usused eventually + // const url = getCastThumbnailURL(file.id); + const resp = await HTTPService.get( + url, + null, + { + "X-Cast-Access-Token": castToken, + }, + { responseType: "arraybuffer" }, + ); + if (resp.data === undefined) throw new Error(`Failed to get ${url}`); + + const cryptoWorker = await ComlinkCryptoWorker.getInstance(); + const decrypted = await cryptoWorker.decryptFile( + new Uint8Array(resp.data), + await cryptoWorker.fromB64(file.file.decryptionHeader), + // TODO(MR): Remove if usused eventually + // await cryptoWorker.fromB64(file.thumbnail.decryptionHeader), + file.key, + ); + return new Response(decrypted).blob(); +}; diff --git a/web/apps/cast/src/services/cast/castService.ts b/web/apps/cast/src/services/cast/castService.ts deleted file mode 100644 index 84636d3a15b7b125b3b988ab4a4102ee2abe296d..0000000000000000000000000000000000000000 --- a/web/apps/cast/src/services/cast/castService.ts +++ /dev/null @@ -1,304 +0,0 @@ -import log from "@/next/log"; -import ComlinkCryptoWorker from "@ente/shared/crypto"; -import { CustomError, parseSharingErrorCodes } from "@ente/shared/error"; -import HTTPService from "@ente/shared/network/HTTPService"; -import { getEndpoint } from "@ente/shared/network/api"; -import localForage from "@ente/shared/storage/localForage"; -import { Collection, CollectionPublicMagicMetadata } from "types/collection"; -import { EncryptedEnteFile, EnteFile } from "types/file"; -import { decryptFile, mergeMetadata, sortFiles } from "utils/file"; - -export interface SavedCollectionFiles { - collectionLocalID: string; - files: EnteFile[]; -} -const ENDPOINT = getEndpoint(); -const COLLECTION_FILES_TABLE = "collection-files"; -const COLLECTIONS_TABLE = "collections"; - -const getLastSyncKey = (collectionUID: string) => `${collectionUID}-time`; - -export const getLocalFiles = async ( - collectionUID: string, -): Promise => { - const localSavedcollectionFiles = - (await localForage.getItem( - COLLECTION_FILES_TABLE, - )) || []; - const matchedCollection = localSavedcollectionFiles.find( - (item) => item.collectionLocalID === collectionUID, - ); - return matchedCollection?.files || []; -}; - -const savecollectionFiles = async ( - collectionUID: string, - files: EnteFile[], -) => { - const collectionFiles = - (await localForage.getItem( - COLLECTION_FILES_TABLE, - )) || []; - await localForage.setItem( - COLLECTION_FILES_TABLE, - dedupeCollectionFiles([ - { collectionLocalID: collectionUID, files }, - ...collectionFiles, - ]), - ); -}; - -export const getLocalCollections = async (collectionKey: string) => { - const localCollections = - (await localForage.getItem(COLLECTIONS_TABLE)) || []; - const collection = - localCollections.find( - (localSavedPublicCollection) => - localSavedPublicCollection.key === collectionKey, - ) || null; - return collection; -}; - -const saveCollection = async (collection: Collection) => { - const collections = - (await localForage.getItem(COLLECTIONS_TABLE)) ?? []; - await localForage.setItem( - COLLECTIONS_TABLE, - dedupeCollections([collection, ...collections]), - ); -}; - -const dedupeCollections = (collections: Collection[]) => { - const keySet = new Set([]); - return collections.filter((collection) => { - if (!keySet.has(collection.key)) { - keySet.add(collection.key); - return true; - } else { - return false; - } - }); -}; - -const dedupeCollectionFiles = (collectionFiles: SavedCollectionFiles[]) => { - const keySet = new Set([]); - return collectionFiles.filter(({ collectionLocalID: collectionUID }) => { - if (!keySet.has(collectionUID)) { - keySet.add(collectionUID); - return true; - } else { - return false; - } - }); -}; - -async function getSyncTime(collectionUID: string): Promise { - const lastSyncKey = getLastSyncKey(collectionUID); - const lastSyncTime = await localForage.getItem(lastSyncKey); - return lastSyncTime ?? 0; -} - -const updateSyncTime = async (collectionUID: string, time: number) => - await localForage.setItem(getLastSyncKey(collectionUID), time); - -export const syncPublicFiles = async ( - token: string, - collection: Collection, - setPublicFiles: (files: EnteFile[]) => void, -) => { - try { - let files: EnteFile[] = []; - const sortAsc = collection?.pubMagicMetadata?.data.asc ?? false; - const collectionUID = String(collection.id); - const localFiles = await getLocalFiles(collectionUID); - files = [...files, ...localFiles]; - try { - const lastSyncTime = await getSyncTime(collectionUID); - if (collection.updationTime === lastSyncTime) { - return sortFiles(files, sortAsc); - } - const fetchedFiles = await fetchFiles( - token, - collection, - lastSyncTime, - files, - setPublicFiles, - ); - - files = [...files, ...fetchedFiles]; - const latestVersionFiles = new Map(); - files.forEach((file) => { - const uid = `${file.collectionID}-${file.id}`; - if ( - !latestVersionFiles.has(uid) || - latestVersionFiles.get(uid).updationTime < file.updationTime - ) { - latestVersionFiles.set(uid, file); - } - }); - files = []; - // eslint-disable-next-line @typescript-eslint/no-unused-vars - for (const [_, file] of latestVersionFiles) { - if (file.isDeleted) { - continue; - } - files.push(file); - } - await savecollectionFiles(collectionUID, files); - await updateSyncTime(collectionUID, collection.updationTime); - setPublicFiles([...sortFiles(mergeMetadata(files), sortAsc)]); - } catch (e) { - const parsedError = parseSharingErrorCodes(e); - log.error("failed to sync shared collection files", e); - if (parsedError.message === CustomError.TOKEN_EXPIRED) { - throw e; - } - } - return [...sortFiles(mergeMetadata(files), sortAsc)]; - } catch (e) { - log.error("failed to get local or sync shared collection files", e); - throw e; - } -}; - -const fetchFiles = async ( - castToken: string, - collection: Collection, - sinceTime: number, - files: EnteFile[], - setPublicFiles: (files: EnteFile[]) => void, -): Promise => { - try { - let decryptedFiles: EnteFile[] = []; - let time = sinceTime; - let resp; - const sortAsc = collection?.pubMagicMetadata?.data.asc ?? false; - do { - if (!castToken) { - break; - } - resp = await HTTPService.get( - `${ENDPOINT}/cast/diff`, - { - sinceTime: time, - }, - { - "Cache-Control": "no-cache", - "X-Cast-Access-Token": castToken, - }, - ); - decryptedFiles = [ - ...decryptedFiles, - ...(await Promise.all( - resp.data.diff.map(async (file: EncryptedEnteFile) => { - if (!file.isDeleted) { - return await decryptFile(file, collection.key); - } else { - return file; - } - }) as Promise[], - )), - ]; - - if (resp.data.diff.length) { - time = resp.data.diff.slice(-1)[0].updationTime; - } - setPublicFiles( - sortFiles( - mergeMetadata( - [...(files || []), ...decryptedFiles].filter( - (item) => !item.isDeleted, - ), - ), - sortAsc, - ), - ); - } while (resp.data.hasMore); - return decryptedFiles; - } catch (e) { - log.error("Get cast files failed", e); - throw e; - } -}; - -export const getCastCollection = async ( - castToken: string, - collectionKey: string, -): Promise => { - try { - const resp = await HTTPService.get(`${ENDPOINT}/cast/info`, null, { - "Cache-Control": "no-cache", - "X-Cast-Access-Token": castToken, - }); - const fetchedCollection = resp.data.collection; - - const cryptoWorker = await ComlinkCryptoWorker.getInstance(); - - const collectionName = (fetchedCollection.name = - fetchedCollection.name || - (await cryptoWorker.decryptToUTF8( - fetchedCollection.encryptedName, - fetchedCollection.nameDecryptionNonce, - collectionKey, - ))); - - let collectionPublicMagicMetadata: CollectionPublicMagicMetadata; - if (fetchedCollection.pubMagicMetadata?.data) { - collectionPublicMagicMetadata = { - ...fetchedCollection.pubMagicMetadata, - data: await cryptoWorker.decryptMetadata( - fetchedCollection.pubMagicMetadata.data, - fetchedCollection.pubMagicMetadata.header, - collectionKey, - ), - }; - } - - const collection = { - ...fetchedCollection, - name: collectionName, - key: collectionKey, - pubMagicMetadata: collectionPublicMagicMetadata, - }; - await saveCollection(collection); - return collection; - } catch (e) { - log.error("failed to get cast collection", e); - throw e; - } -}; - -export const removeCollection = async ( - collectionUID: string, - collectionKey: string, -) => { - const collections = - (await localForage.getItem(COLLECTIONS_TABLE)) || []; - await localForage.setItem( - COLLECTIONS_TABLE, - collections.filter((collection) => collection.key !== collectionKey), - ); - await removeCollectionFiles(collectionUID); -}; - -export const removeCollectionFiles = async (collectionUID: string) => { - await localForage.removeItem(getLastSyncKey(collectionUID)); - const collectionFiles = - (await localForage.getItem( - COLLECTION_FILES_TABLE, - )) ?? []; - await localForage.setItem( - COLLECTION_FILES_TABLE, - collectionFiles.filter( - (collectionFiles) => - collectionFiles.collectionLocalID !== collectionUID, - ), - ); -}; - -export const storeCastData = (payloadObj: Object) => { - // iterate through all the keys in the payload object and set them in localStorage. - for (const key in payloadObj) { - window.localStorage.setItem(key, payloadObj[key]); - } -}; diff --git a/web/apps/cast/src/services/castDownloadManager.ts b/web/apps/cast/src/services/castDownloadManager.ts deleted file mode 100644 index 2314ed54ea4fa620b27a650ba8ffcfa09f8faa79..0000000000000000000000000000000000000000 --- a/web/apps/cast/src/services/castDownloadManager.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { FILE_TYPE } from "@/media/file-type"; -import ComlinkCryptoWorker from "@ente/shared/crypto"; -import { CustomError } from "@ente/shared/error"; -import HTTPService from "@ente/shared/network/HTTPService"; -import { getCastFileURL } from "@ente/shared/network/api"; -import { EnteFile } from "types/file"; -import { generateStreamFromArrayBuffer } from "utils/file"; - -class CastDownloadManager { - async downloadFile(castToken: string, file: EnteFile) { - const cryptoWorker = await ComlinkCryptoWorker.getInstance(); - - if ( - file.metadata.fileType === FILE_TYPE.IMAGE || - file.metadata.fileType === FILE_TYPE.LIVE_PHOTO - ) { - const resp = await HTTPService.get( - getCastFileURL(file.id), - null, - { - "X-Cast-Access-Token": castToken, - }, - { responseType: "arraybuffer" }, - ); - if (typeof resp.data === "undefined") { - throw Error(CustomError.REQUEST_FAILED); - } - const decrypted = await cryptoWorker.decryptFile( - new Uint8Array(resp.data), - await cryptoWorker.fromB64(file.file.decryptionHeader), - file.key, - ); - return generateStreamFromArrayBuffer(decrypted); - } - const resp = await fetch(getCastFileURL(file.id), { - headers: { - "X-Cast-Access-Token": castToken, - }, - }); - const reader = resp.body.getReader(); - - const stream = new ReadableStream({ - async start(controller) { - const decryptionHeader = await cryptoWorker.fromB64( - file.file.decryptionHeader, - ); - const fileKey = await cryptoWorker.fromB64(file.key); - const { pullState, decryptionChunkSize } = - await cryptoWorker.initChunkDecryption( - decryptionHeader, - fileKey, - ); - let data = new Uint8Array(); - // The following function handles each data chunk - function push() { - // "done" is a Boolean and value a "Uint8Array" - reader.read().then(async ({ done, value }) => { - // Is there more data to read? - if (!done) { - const buffer = new Uint8Array( - data.byteLength + value.byteLength, - ); - buffer.set(new Uint8Array(data), 0); - buffer.set(new Uint8Array(value), data.byteLength); - if (buffer.length > decryptionChunkSize) { - const fileData = buffer.slice( - 0, - decryptionChunkSize, - ); - const { decryptedData } = - await cryptoWorker.decryptFileChunk( - fileData, - pullState, - ); - controller.enqueue(decryptedData); - data = buffer.slice(decryptionChunkSize); - } else { - data = buffer; - } - push(); - } else { - if (data) { - const { decryptedData } = - await cryptoWorker.decryptFileChunk( - data, - pullState, - ); - controller.enqueue(decryptedData); - data = null; - } - controller.close(); - } - }); - } - - push(); - }, - }); - return stream; - } -} - -export default new CastDownloadManager(); diff --git a/web/apps/cast/src/services/pair.ts b/web/apps/cast/src/services/pair.ts new file mode 100644 index 0000000000000000000000000000000000000000..893681d32bcb257dab50da5cf08b73a554045c73 --- /dev/null +++ b/web/apps/cast/src/services/pair.ts @@ -0,0 +1,234 @@ +import log from "@/next/log"; +import { boxSealOpen, toB64 } from "@ente/shared/crypto/internal/libsodium"; +import castGateway from "@ente/shared/network/cast"; +import { wait } from "@ente/shared/utils"; +import _sodium from "libsodium-wrappers"; +import { type Cast } from "../utils/cast-receiver"; + +export interface Registration { + /** A pairing code shown on the screen. A client can use this to connect. */ + pairingCode: string; + /** The public part of the keypair we registered with the server. */ + publicKeyB64: string; + /** The private part of the keypair we registered with the server. */ + privateKeyB64: string; +} + +/** + * Register a keypair with the server and return a pairing code that can be used + * to connect to us. Phase 1 of the pairing protocol. + * + * [Note: Pairing protocol] + * + * The Chromecast Framework (represented here by our handle to the Chromecast + * Web SDK, {@link cast}) itself is used for only the initial handshake, none of + * the data, even encrypted passes over it thereafter. + * + * The pairing happens in two phases: + * + * Phase 1 - {@link register} + * + * 1. We (the receiver) generate a public/private keypair. and register the + * public part of it with museum. + * + * 2. Museum gives us a pairing "code" in lieu. Show this on the screen. + * + * Phase 2 - {@link advertiseCode} + * + * There are two ways the client can connect - either by sending us a blank + * message over the Chromecast protocol (to which we'll reply with the pairing + * code), or by the user manually entering the pairing code on their screen. + * + * 3. Listen for incoming messages over the Chromecast connection. + * + * 4. The client (our Web or mobile app) will connect using the "sender" + * Chromecast SDK. This will result in a bi-directional channel between us + * ("receiver") and the Ente client app ("sender"). + * + * 5. Thereafter, if at any time the sender disconnects, close the Chromecast + * context. This effectively shuts us down, causing the entire page to get + * reloaded. + * + * 6. After connecting, the sender sends an (empty) message. We reply by sending + * them a message containing the pairing code. This exchange is the only data + * that traverses over the Chromecast connection. + * + * Once the client gets the pairing code (via Chromecast or manual entry), + * they'll let museum know. So in parallel with Phase 2, we perform Phase 3. + * + * Phase 3 - {@link getCastData} in a setInterval. + * + * 7. Keep polling museum to ask it if anyone has claimed that code we vended + * out and used that to send us an payload encrypted using our public key. + * + * 8. When that happens, decrypt that data with our private key, and return this + * payload. It is a JSON object that contains the data we need to initiate a + * slideshow for a particular Ente collection. + * + * Phase 1 (Steps 1 and 2) are done by the {@link register} function, which + * returns a {@link Registration}. + * + * At this time we start showing the pairing code on the UI, and start phase 2, + * {@link advertiseCode} to vend out the pairing code to Chromecast connections. + * + * In parallel, we start Phase 3, calling {@link getCastData} in a loop. Once we + * get a response, we decrypt it to get the data we need to start the slideshow. + */ +export const register = async (): Promise => { + // Generate keypair. + const keypair = await generateKeyPair(); + const publicKeyB64 = await toB64(keypair.publicKey); + const privateKeyB64 = await toB64(keypair.privateKey); + + // Register keypair with museum to get a pairing code. + let pairingCode: string; + // eslint has fixed this spurious warning, but we're not on the latest + // version yet, so add a disable. + // https://github.com/eslint/eslint/pull/18286 + /* eslint-disable no-constant-condition */ + while (true) { + try { + pairingCode = await castGateway.registerDevice(publicKeyB64); + } catch (e) { + log.error("Failed to register public key with server", e); + } + if (pairingCode) break; + // Schedule retry after 10 seconds. + await wait(10000); + } + + return { pairingCode, publicKeyB64, privateKeyB64 }; +}; + +/** + * Listen for incoming messages on the given {@link cast} receiver, replying to + * each of them with a pairing code obtained using the given {@link pairingCode} + * callback. Phase 2 of the pairing protocol. + * + * See: [Note: Pairing protocol]. + */ +export const advertiseCode = ( + cast: Cast, + pairingCode: () => string | undefined, +) => { + // Prepare the Chromecast "context". + const context = cast.framework.CastReceiverContext.getInstance(); + const namespace = "urn:x-cast:pair-request"; + + const options = new cast.framework.CastReceiverOptions(); + // We don't use the media features of the Cast SDK. + options.skipPlayersLoad = true; + // Do not stop the casting if the receiver is unreachable. A user should be + // able to start a cast on their phone and then put it away, leaving the + // cast running on their big screen. + options.disableIdleTimeout = true; + + // The collection ID with which we paired. If we get another connection + // request for a different collection ID, restart the app to allow them to + // reconnect using a freshly generated pairing code. + // + // If the request does not have a collectionID, forego this check. + let pairedCollectionID: string | undefined; + + type ListenerProps = { + senderId: string; + data: unknown; + }; + + // Reply with the code that we have if anyone asks over Chromecast. + const incomingMessageListener = ({ senderId, data }: ListenerProps) => { + const restart = (reason: string) => { + log.error(`Restarting app because ${reason}`); + // context.stop will close the tab but it'll get reopened again + // immediately since the client app will reconnect in the scenarios + // where we're calling this function. + context.stop(); + }; + + const collectionID = + data && + typeof data == "object" && + typeof data["collectionID"] == "string" + ? data["collectionID"] + : undefined; + + if (pairedCollectionID && pairedCollectionID != collectionID) { + restart(`incoming request for a new collection ${collectionID}`); + return; + } + + pairedCollectionID = collectionID; + + const code = pairingCode(); + if (!code) { + // Our caller waits until it has a pairing code before it calls + // `advertiseCode`, but there is still an edge case where we can + // find ourselves without a pairing code: + // + // 1. The current pairing code expires. We start the process to get + // a new one. + // + // 2. But before that happens, someone connects. + // + // The window where this can happen is short, so if we do find + // ourselves in this scenario, + restart("we got a pairing request when refreshing pairing codes"); + return; + } + + context.sendCustomMessage(namespace, senderId, { code }); + }; + + context.addCustomMessageListener( + namespace, + // We need to cast, the `senderId` is present in the message we get but + // not present in the TypeScript type. + incomingMessageListener as unknown as SystemEventHandler, + ); + + // Close the (chromecast) tab if the sender disconnects. + // + // Chromecast does a "shutdown" of our cast app when we call `context.stop`. + // This translates into it closing the tab where it is showing our app. + context.addEventListener( + cast.framework.system.EventType.SENDER_DISCONNECTED, + () => context.stop(), + ); + + // Start listening for Chromecast connections. + context.start(options); +}; + +/** + * Ask museum if anyone has sent a (encrypted) payload corresponding to the + * given pairing code. If so, decrypt it using our private key and return the + * JSON payload. Phase 3 of the pairing protocol. + * + * Returns `undefined` if there hasn't been any data obtained yet. + * + * See: [Note: Pairing protocol]. + */ +export const getCastData = async (registration: Registration) => { + const { pairingCode, publicKeyB64, privateKeyB64 } = registration; + + // The client will send us the encrypted payload using our public key that + // we registered with museum. + const encryptedCastData = await castGateway.getCastData(pairingCode); + if (!encryptedCastData) return; + + // Decrypt it using the private key of the pair and return the plaintext + // payload, which'll be a JSON object containing the data we need to start a + // slideshow for some collection. + const decryptedCastData = await boxSealOpen( + encryptedCastData, + publicKeyB64, + privateKeyB64, + ); + + return JSON.parse(atob(decryptedCastData)); +}; + +const generateKeyPair = async () => { + await _sodium.ready; + return _sodium.crypto_box_keypair(); +}; diff --git a/web/apps/cast/src/types/collection.ts b/web/apps/cast/src/types/collection.ts deleted file mode 100644 index c495937ae009468150781d6efbb54ecdf310f867..0000000000000000000000000000000000000000 --- a/web/apps/cast/src/types/collection.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { EnteFile } from "types/file"; -import { - EncryptedMagicMetadata, - MagicMetadataCore, - SUB_TYPE, - VISIBILITY_STATE, -} from "types/magicMetadata"; - -export enum COLLECTION_ROLE { - VIEWER = "VIEWER", - OWNER = "OWNER", - COLLABORATOR = "COLLABORATOR", - UNKNOWN = "UNKNOWN", -} - -export interface CollectionUser { - id: number; - email: string; - role: COLLECTION_ROLE; -} - -enum CollectionType { - folder = "folder", - favorites = "favorites", - album = "album", - uncategorized = "uncategorized", -} - -export interface EncryptedCollection { - id: number; - owner: CollectionUser; - // collection name was unencrypted in the past, so we need to keep it as optional - name?: string; - encryptedKey: string; - keyDecryptionNonce: string; - encryptedName: string; - nameDecryptionNonce: string; - type: CollectionType; - attributes: collectionAttributes; - sharees: CollectionUser[]; - publicURLs?: unknown; - updationTime: number; - isDeleted: boolean; - magicMetadata: EncryptedMagicMetadata; - pubMagicMetadata: EncryptedMagicMetadata; - sharedMagicMetadata: EncryptedMagicMetadata; -} - -export interface Collection - extends Omit< - EncryptedCollection, - | "encryptedKey" - | "keyDecryptionNonce" - | "encryptedName" - | "nameDecryptionNonce" - | "magicMetadata" - | "pubMagicMetadata" - | "sharedMagicMetadata" - > { - key: string; - name: string; - magicMetadata: CollectionMagicMetadata; - pubMagicMetadata: CollectionPublicMagicMetadata; - sharedMagicMetadata: CollectionShareeMagicMetadata; -} - -// define a method on Collection interface to return the sync key as collection.id-time -// this is used to store the last sync time of a collection in local storage - -export interface collectionAttributes { - encryptedPath?: string; - pathDecryptionNonce?: string; -} - -export type CollectionToFileMap = Map; - -export interface CollectionMagicMetadataProps { - visibility?: VISIBILITY_STATE; - subType?: SUB_TYPE; - order?: number; -} - -export type CollectionMagicMetadata = - MagicMetadataCore; - -export interface CollectionShareeMetadataProps { - visibility?: VISIBILITY_STATE; -} -export type CollectionShareeMagicMetadata = - MagicMetadataCore; - -export interface CollectionPublicMagicMetadataProps { - asc?: boolean; - coverID?: number; -} - -export type CollectionPublicMagicMetadata = - MagicMetadataCore; - -export type CollectionFilesCount = Map; diff --git a/web/apps/cast/src/utils/cast-receiver.tsx b/web/apps/cast/src/utils/cast-receiver.tsx new file mode 100644 index 0000000000000000000000000000000000000000..666a085edc7eb586911a1f93ae2666c0ccbe78df --- /dev/null +++ b/web/apps/cast/src/utils/cast-receiver.tsx @@ -0,0 +1,32 @@ +/// + +export type Cast = typeof cast; + +let _cast: Cast | undefined; +let _loader: Promise | undefined; + +/** + * Load the Chromecast Web Receiver SDK and return a reference to the `cast` + * global object that the SDK attaches to the window. + * + * Calling this function multiple times is fine, once the Chromecast SDK is + * loaded it'll thereafter return the reference to the same object always. + * + * https://developers.google.com/cast/docs/web_receiver/basic + */ +export const castReceiverLoadingIfNeeded = async (): Promise => { + if (_cast) return _cast; + if (_loader) return await _loader; + + _loader = new Promise((resolve) => { + const script = document.createElement("script"); + script.src = + "https://www.gstatic.com/cast/sdk/libs/caf_receiver/v3/cast_receiver_framework.js"; + + script.addEventListener("load", () => resolve(cast)); + document.body.appendChild(script); + }); + const c = await _loader; + _cast = c; + return c; +}; diff --git a/web/apps/cast/src/utils/file.ts b/web/apps/cast/src/utils/file.ts deleted file mode 100644 index 91961b7becf4ac734a515553e21e080f2afa10f3..0000000000000000000000000000000000000000 --- a/web/apps/cast/src/utils/file.ts +++ /dev/null @@ -1,144 +0,0 @@ -import { FILE_TYPE } from "@/media/file-type"; -import { decodeLivePhoto } from "@/media/live-photo"; -import log from "@/next/log"; -import ComlinkCryptoWorker from "@ente/shared/crypto"; -import { RAW_FORMATS } from "constants/upload"; -import CastDownloadManager from "services/castDownloadManager"; -import { detectMediaMIMEType } from "services/detect-type"; -import { - EncryptedEnteFile, - EnteFile, - FileMagicMetadata, - FilePublicMagicMetadata, -} from "types/file"; - -export function sortFiles(files: EnteFile[], sortAsc = false) { - // sort based on the time of creation time of the file, - // for files with same creation time, sort based on the time of last modification - const factor = sortAsc ? -1 : 1; - return files.sort((a, b) => { - if (a.metadata.creationTime === b.metadata.creationTime) { - return ( - factor * - (b.metadata.modificationTime - a.metadata.modificationTime) - ); - } - return factor * (b.metadata.creationTime - a.metadata.creationTime); - }); -} - -export async function decryptFile( - file: EncryptedEnteFile, - collectionKey: string, -): Promise { - try { - const worker = await ComlinkCryptoWorker.getInstance(); - const { - encryptedKey, - keyDecryptionNonce, - metadata, - magicMetadata, - pubMagicMetadata, - ...restFileProps - } = file; - const fileKey = await worker.decryptB64( - encryptedKey, - keyDecryptionNonce, - collectionKey, - ); - const fileMetadata = await worker.decryptMetadata( - metadata.encryptedData, - metadata.decryptionHeader, - fileKey, - ); - let fileMagicMetadata: FileMagicMetadata; - let filePubMagicMetadata: FilePublicMagicMetadata; - if (magicMetadata?.data) { - fileMagicMetadata = { - ...file.magicMetadata, - data: await worker.decryptMetadata( - magicMetadata.data, - magicMetadata.header, - fileKey, - ), - }; - } - if (pubMagicMetadata?.data) { - filePubMagicMetadata = { - ...pubMagicMetadata, - data: await worker.decryptMetadata( - pubMagicMetadata.data, - pubMagicMetadata.header, - fileKey, - ), - }; - } - return { - ...restFileProps, - key: fileKey, - metadata: fileMetadata, - magicMetadata: fileMagicMetadata, - pubMagicMetadata: filePubMagicMetadata, - }; - } catch (e) { - log.error("file decryption failed", e); - throw e; - } -} - -export function generateStreamFromArrayBuffer(data: Uint8Array) { - return new ReadableStream({ - async start(controller: ReadableStreamDefaultController) { - controller.enqueue(data); - controller.close(); - }, - }); -} - -export function isRawFileFromFileName(fileName: string) { - for (const rawFormat of RAW_FORMATS) { - if (fileName.toLowerCase().endsWith(rawFormat)) { - return true; - } - } - return false; -} - -export function mergeMetadata(files: EnteFile[]): EnteFile[] { - return files.map((file) => { - if (file.pubMagicMetadata?.data.editedTime) { - file.metadata.creationTime = file.pubMagicMetadata.data.editedTime; - } - if (file.pubMagicMetadata?.data.editedName) { - file.metadata.title = file.pubMagicMetadata.data.editedName; - } - - return file; - }); -} - -export const getPreviewableImage = async ( - file: EnteFile, - castToken: string, -): Promise => { - try { - let fileBlob = await new Response( - await CastDownloadManager.downloadFile(castToken, file), - ).blob(); - if (file.metadata.fileType === FILE_TYPE.LIVE_PHOTO) { - const { imageData } = await decodeLivePhoto( - file.metadata.title, - fileBlob, - ); - fileBlob = new Blob([imageData]); - } - const mimeType = await detectMediaMIMEType( - new File([fileBlob], file.metadata.title), - ); - if (!mimeType) return undefined; - fileBlob = new Blob([fileBlob], { type: mimeType }); - return fileBlob; - } catch (e) { - log.error("failed to download file", e); - } -}; diff --git a/web/apps/cast/src/utils/useCastReceiver.tsx b/web/apps/cast/src/utils/useCastReceiver.tsx deleted file mode 100644 index ff17b0910fdfead54ba85330c5b908b53df94093..0000000000000000000000000000000000000000 --- a/web/apps/cast/src/utils/useCastReceiver.tsx +++ /dev/null @@ -1,43 +0,0 @@ -declare const cast: any; - -import { useEffect, useState } from "react"; - -type Receiver = { - cast: typeof cast; -}; - -const load = (() => { - let promise: Promise | null = null; - - return () => { - if (promise === null) { - promise = new Promise((resolve) => { - const script = document.createElement("script"); - script.src = - "https://www.gstatic.com/cast/sdk/libs/caf_receiver/v3/cast_receiver_framework.js"; - - script.addEventListener("load", () => { - resolve({ - cast, - }); - }); - document.body.appendChild(script); - }); - } - return promise; - }; -})(); - -export const useCastReceiver = () => { - const [receiver, setReceiver] = useState({ - cast: null, - }); - - useEffect(() => { - load().then((receiver) => { - setReceiver(receiver); - }); - }); - - return receiver; -}; diff --git a/web/apps/photos/package.json b/web/apps/photos/package.json index 1196b4ddf7a386acacf4edc8961312556bf1c54a..0aa09f101fc5e14da0ddf721f36173bbd635ebb1 100644 --- a/web/apps/photos/package.json +++ b/web/apps/photos/package.json @@ -37,7 +37,7 @@ "photoswipe": "file:./thirdparty/photoswipe", "piexifjs": "^1.0.6", "pure-react-carousel": "^1.30.1", - "react-dropzone": "^11.2.4", + "react-dropzone": "^14.2", "react-otp-input": "^2.3.1", "react-select": "^4.3.1", "react-top-loading-bar": "^2.0.1", diff --git a/web/apps/photos/src/components/Collections/CollectionOptions/AlbumCastDialog.tsx b/web/apps/photos/src/components/Collections/CollectionOptions/AlbumCastDialog.tsx index fdabffe8468b1bf4f44eb26fd9ca292b926224de..3d9d061663d3e51b593e8dc04f2cbb6d4b592346 100644 --- a/web/apps/photos/src/components/Collections/CollectionOptions/AlbumCastDialog.tsx +++ b/web/apps/photos/src/components/Collections/CollectionOptions/AlbumCastDialog.tsx @@ -161,9 +161,7 @@ export default function AlbumCastDialog(props: Props) { {browserCanCast && ( <> - {t( - "AUTO_CAST_PAIR_REQUIRES_CONNECTION_TO_GOOGLE", - )} + {t("AUTO_CAST_PAIR_DESC")} )} - {t("PAIR_WITH_PIN_WORKS_FOR_ANY_LARGE_SCREEN_DEVICE")} + {t("PAIR_WITH_PIN_DESC")} ` diff --git a/web/apps/photos/src/components/PhotoFrame.tsx b/web/apps/photos/src/components/PhotoFrame.tsx index f7db350daa327510c617227b467760b25f227797..89f1ce887e9a696d1518df86cd142da237c68819 100644 --- a/web/apps/photos/src/components/PhotoFrame.tsx +++ b/web/apps/photos/src/components/PhotoFrame.tsx @@ -308,7 +308,7 @@ const PhotoFrame = ({ item: EnteFile, ) => { log.info( - `[${item.id}] getSlideData called for thumbnail: ${!!item.msrc} sourceLoaded: ${item.isSourceLoaded} fetching:${fetching[item.id]}`, + `[${item.id}] getSlideData called for thumbnail: ${!!item.msrc} sourceLoaded: ${!!item.isSourceLoaded} fetching: ${!!fetching[item.id]}`, ); if (!item.msrc) { diff --git a/web/apps/photos/src/components/PhotoList/dedupe.tsx b/web/apps/photos/src/components/PhotoList/dedupe.tsx index 7181f626754fce4e3f1c556adb2ec8dd3c8580f0..61b9958ef08b04fdbe8461154bd0728b48073d8f 100644 --- a/web/apps/photos/src/components/PhotoList/dedupe.tsx +++ b/web/apps/photos/src/components/PhotoList/dedupe.tsx @@ -19,7 +19,7 @@ import { } from "react-window"; import { Duplicate } from "services/deduplicationService"; import { EnteFile } from "types/file"; -import { convertBytesToHumanReadable } from "utils/file"; +import { formattedByteSize } from "utils/units"; export enum ITEM_TYPE { TIME = "TIME", @@ -304,10 +304,13 @@ export function DedupePhotoList({ switch (listItem.itemType) { case ITEM_TYPE.SIZE_AND_COUNT: return ( + /*TODO: Translate the full phrase instead of piecing + together parts like this See: + https://crowdin.com/editor/ente-photos-web/9/enus-de?view=comfortable&filter=basic&value=0#8104 + */ {listItem.fileCount} {t("FILES")},{" "} - {convertBytesToHumanReadable(listItem.fileSize || 0)}{" "} - {t("EACH")} + {formattedByteSize(listItem.fileSize || 0)} {t("EACH")} ); case ITEM_TYPE.FILE: { diff --git a/web/apps/photos/src/components/PhotoList/index.tsx b/web/apps/photos/src/components/PhotoList/index.tsx index 91f712df1b051115ef60ea18f8d36d2062c43c2d..5ac6b263edaa583208828af453d0896aff30b15a 100644 --- a/web/apps/photos/src/components/PhotoList/index.tsx +++ b/web/apps/photos/src/components/PhotoList/index.tsx @@ -22,9 +22,9 @@ import { areEqual, } from "react-window"; import { EnteFile } from "types/file"; -import { convertBytesToHumanReadable } from "utils/file"; import { handleSelectCreator } from "utils/photoFrame"; import { PublicCollectionGalleryContext } from "utils/publicCollectionGallery"; +import { formattedByteSize } from "utils/units"; const A_DAY = 24 * 60 * 60 * 1000; const FOOTER_HEIGHT = 90; @@ -111,14 +111,13 @@ function getShrinkRatio(width: number, columns: number) { ); } -const ListContainer = styled(Box)<{ - columns: number; - shrinkRatio: number; - groups?: number[]; +const ListContainer = styled(Box, { + shouldForwardProp: (propName) => propName != "gridTemplateColumns", +})<{ + gridTemplateColumns: string; }>` display: grid; - grid-template-columns: ${({ columns, shrinkRatio, groups }) => - getTemplateColumns(columns, shrinkRatio, groups)}; + grid-template-columns: ${(props) => props.gridTemplateColumns}; grid-column-gap: ${GAP_BTW_TILES}px; width: 100%; color: #fff; @@ -235,9 +234,11 @@ const PhotoListRow = React.memo( return ( {renderListItem(timeStampList[index], isScrolling)} @@ -828,8 +829,7 @@ export function PhotoList({ return ( {listItem.fileCount} {t("FILES")},{" "} - {convertBytesToHumanReadable(listItem.fileSize || 0)}{" "} - {t("EACH")} + {formattedByteSize(listItem.fileSize || 0)} {t("EACH")} ); case ITEM_TYPE.FILE: { diff --git a/web/apps/photos/src/components/PhotoViewer/FileInfo/RenderFileName.tsx b/web/apps/photos/src/components/PhotoViewer/FileInfo/RenderFileName.tsx index 39905118550f5441322458995a740372e0c2780f..e9e27d55e8e585bc1c0c392b8be52d7d41af28c3 100644 --- a/web/apps/photos/src/components/PhotoViewer/FileInfo/RenderFileName.tsx +++ b/web/apps/photos/src/components/PhotoViewer/FileInfo/RenderFileName.tsx @@ -7,8 +7,8 @@ import VideocamOutlined from "@mui/icons-material/VideocamOutlined"; import Box from "@mui/material/Box"; import { useEffect, useState } from "react"; import { EnteFile } from "types/file"; -import { makeHumanReadableStorage } from "utils/billing"; import { changeFileName, updateExistingFilePubMetadata } from "utils/file"; +import { formattedByteSize } from "utils/units"; import { FileNameEditDialog } from "./FileNameEditDialog"; import InfoItem from "./InfoItem"; @@ -33,7 +33,7 @@ const getCaption = (file: EnteFile, parsedExifData) => { captionParts.push(resolution); } if (fileSize) { - captionParts.push(makeHumanReadableStorage(fileSize)); + captionParts.push(formattedByteSize(fileSize)); } return ( diff --git a/web/apps/photos/src/components/PhotoViewer/index.tsx b/web/apps/photos/src/components/PhotoViewer/index.tsx index 8e6debf68b2ddcf70ec900bf06469b58538666e3..c7383efb13bf89c098df4a631a8885db378ac3ff 100644 --- a/web/apps/photos/src/components/PhotoViewer/index.tsx +++ b/web/apps/photos/src/components/PhotoViewer/index.tsx @@ -11,11 +11,11 @@ import { copyFileToClipboard, downloadSingleFile, getFileFromURL, - isRawFile, isSupportedRawFormat, } from "utils/file"; import { FILE_TYPE } from "@/media/file-type"; +import { isNonWebImageFileExtension } from "@/media/formats"; import { lowercaseExtension } from "@/next/file"; import { FlexWrapper } from "@ente/shared/components/Container"; import EnteSpinner from "@ente/shared/components/EnteSpinner"; @@ -350,7 +350,8 @@ function PhotoViewer(props: Iprops) { function updateShowEditButton(file: EnteFile) { const extension = lowercaseExtension(file.metadata.title); const isSupported = - !isRawFile(extension) || isSupportedRawFormat(extension); + !isNonWebImageFileExtension(extension) || + isSupportedRawFormat(extension); setShowEditButton( file.metadata.fileType === FILE_TYPE.IMAGE && isSupported, ); diff --git a/web/apps/photos/src/components/PhotoViewer/styledComponents/LivePhotoBtn.tsx b/web/apps/photos/src/components/PhotoViewer/styledComponents/LivePhotoBtn.tsx index 40de098f5e81372641ca5ffe6109c0724906f321..00b8979d5ab0f87bdd574e258ec93db364c28655 100644 --- a/web/apps/photos/src/components/PhotoViewer/styledComponents/LivePhotoBtn.tsx +++ b/web/apps/photos/src/components/PhotoViewer/styledComponents/LivePhotoBtn.tsx @@ -1,5 +1,4 @@ -import { Paper } from "@mui/material"; -import { styled } from "@mui/material/styles"; +import { Paper, styled } from "@mui/material"; export const LivePhotoBtnContainer = styled(Paper)` border-radius: 4px; diff --git a/web/apps/photos/src/components/Search/SearchBar/styledComponents.tsx b/web/apps/photos/src/components/Search/SearchBar/styledComponents.tsx index 41d4a0971e1927f78aeb486f2b3c0a6571d7c1f2..d33c7c94909dd74f0959d15c7ac10b6d4878502c 100644 --- a/web/apps/photos/src/components/Search/SearchBar/styledComponents.tsx +++ b/web/apps/photos/src/components/Search/SearchBar/styledComponents.tsx @@ -23,7 +23,9 @@ export const SearchMobileBox = styled(FluidContainer)` } `; -export const SearchInputWrapper = styled(CenteredFlex)<{ isOpen: boolean }>` +export const SearchInputWrapper = styled(CenteredFlex, { + shouldForwardProp: (propName) => propName != "isOpen", +})<{ isOpen: boolean }>` background: ${({ theme }) => theme.colors.background.base}; max-width: 484px; margin: auto; diff --git a/web/apps/photos/src/components/Sidebar/Preferences/LanguageSelector.tsx b/web/apps/photos/src/components/Sidebar/Preferences/LanguageSelector.tsx index a9474a37d97c12ed2150ac770d95e91e59f8fb38..bdc0d5a84fd7ce855d450d65928a592365d63a04 100644 --- a/web/apps/photos/src/components/Sidebar/Preferences/LanguageSelector.tsx +++ b/web/apps/photos/src/components/Sidebar/Preferences/LanguageSelector.tsx @@ -19,6 +19,8 @@ export const localeName = (locale: SupportedLocale) => { return "English"; case "fr-FR": return "Français"; + case "de-DE": + return "Deutsch"; case "zh-CN": return "中文"; case "nl-NL": diff --git a/web/apps/photos/src/components/Sidebar/SubscriptionCard/contentOverlay/individual/usageSection.tsx b/web/apps/photos/src/components/Sidebar/SubscriptionCard/contentOverlay/individual/usageSection.tsx index 4b0ce31b042df5c1e431118c75e884b81d96b4e3..8975941ad50f790ca4d655617555ef0c47311ec6 100644 --- a/web/apps/photos/src/components/Sidebar/SubscriptionCard/contentOverlay/individual/usageSection.tsx +++ b/web/apps/photos/src/components/Sidebar/SubscriptionCard/contentOverlay/individual/usageSection.tsx @@ -1,7 +1,7 @@ import { SpaceBetweenFlex } from "@ente/shared/components/Container"; import { Box, Typography } from "@mui/material"; import { t } from "i18next"; -import { makeHumanReadableStorage } from "utils/billing"; +import { formattedStorageByteSize } from "utils/units"; import { Progressbar } from "../../styledComponents"; @@ -19,7 +19,7 @@ export function IndividualUsageSection({ usage, storage, fileCount }: Iprops) { marginTop: 1.5, }} > - {`${makeHumanReadableStorage( + {`${formattedStorageByteSize( storage - usage, )} ${t("FREE")}`} diff --git a/web/apps/photos/src/components/Sidebar/SubscriptionCard/contentOverlay/storageSection.tsx b/web/apps/photos/src/components/Sidebar/SubscriptionCard/contentOverlay/storageSection.tsx index 6143044f0d8f3bb8584d1ee31fd5117a4d9974b3..7f2712f7386c0df77453c612d3d9d7c9c4f2f45f 100644 --- a/web/apps/photos/src/components/Sidebar/SubscriptionCard/contentOverlay/storageSection.tsx +++ b/web/apps/photos/src/components/Sidebar/SubscriptionCard/contentOverlay/storageSection.tsx @@ -1,6 +1,6 @@ import { Box, styled, Typography } from "@mui/material"; import { t } from "i18next"; -import { convertBytesToGBs, makeHumanReadableStorage } from "utils/billing"; +import { bytesInGB, formattedStorageByteSize } from "utils/units"; const MobileSmallBox = styled(Box)` display: none; @@ -30,9 +30,9 @@ export default function StorageSection({ usage, storage }: Iprops) { fontWeight={"bold"} sx={{ fontSize: "24px", lineHeight: "30px" }} > - {`${makeHumanReadableStorage(usage, { roundUp: true })} ${t( + {`${formattedStorageByteSize(usage, { round: true })} ${t( "OF", - )} ${makeHumanReadableStorage(storage)} ${t("USED")}`} + )} ${formattedStorageByteSize(storage)} ${t("USED")}`} @@ -40,9 +40,7 @@ export default function StorageSection({ usage, storage }: Iprops) { fontWeight={"bold"} sx={{ fontSize: "24px", lineHeight: "30px" }} > - {`${convertBytesToGBs(usage)} / ${convertBytesToGBs( - storage, - )} ${t("GB")} ${t("USED")}`} + {`${bytesInGB(usage)} / ${bytesInGB(storage)} ${t("storage_unit.gb")} ${t("USED")}`} diff --git a/web/apps/photos/src/components/Upload/Uploader.tsx b/web/apps/photos/src/components/Upload/Uploader.tsx index fdc6ee93290f17f77610c1a0c41975041b150ea5..bea54c645b2fffd577987b54228d7595c2b79121 100644 --- a/web/apps/photos/src/components/Upload/Uploader.tsx +++ b/web/apps/photos/src/components/Upload/Uploader.tsx @@ -1,7 +1,8 @@ import { basename } from "@/next/file"; import log from "@/next/log"; -import { type FileAndPath } from "@/next/types/file"; import type { CollectionMapping, Electron, ZipItem } from "@/next/types/ipc"; +import { firstNonEmpty } from "@/utils/array"; +import { ensure } from "@/utils/ensure"; import { CustomError } from "@ente/shared/error"; import { isPromise } from "@ente/shared/utils"; import DiscFullIcon from "@mui/icons-material/DiscFull"; @@ -20,7 +21,7 @@ import { getPublicCollectionUploaderName, savePublicCollectionUploaderName, } from "services/publicCollectionService"; -import type { UploadItem } from "services/upload/types"; +import type { FileAndPath, UploadItem } from "services/upload/types"; import type { InProgressUpload, SegregatedFinishedUploads, @@ -261,7 +262,9 @@ export default function Uploader({ const { collectionName, filePaths, zipItems } = pending; - log.info("Resuming pending upload", pending); + log.info( + `Resuming pending of upload of ${filePaths.length + zipItems.length} items${collectionName ? " to collection " + collectionName : ""}`, + ); isPendingDesktopUpload.current = true; pendingDesktopUploadCollectionName.current = collectionName; setDesktopFilePaths(filePaths); @@ -323,11 +326,26 @@ export default function Uploader({ // Trigger an upload when any of the dependencies change. useEffect(() => { + // About the paths: + // + // - These are not necessarily the full paths. In particular, when + // running on the browser they'll be the relative paths (at best) or + // just the file-name otherwise. + // + // - All the paths use POSIX separators. See inline comments. + // const allItemAndPaths = [ - /* TODO(MR): ElectronFile | use webkitRelativePath || name here */ - webFiles.map((f) => [f, f["path"] ?? f.name]), + // Relative path (using POSIX separators) or the file's name. + webFiles.map((f) => [f, pathLikeForWebFile(f)]), + // The paths we get from the desktop app all eventually come either + // from electron.selectDirectory or electron.pathForFile, both of + // which return POSIX paths. desktopFiles.map((fp) => [fp, fp.path]), desktopFilePaths.map((p) => [p, p]), + // The first path, that of the zip file itself, is POSIX like the + // other paths we get over the IPC boundary. And the second path, + // ze[1], the entry name, uses POSIX separators because that is what + // the ZIP format uses. desktopZipItems.map((ze) => [ze, ze[1]]), ].flat() as [UploadItem, string][]; @@ -790,10 +808,7 @@ async function waitAndRun( await task(); } -const desktopFilesAndZipItems = async ( - electron: Electron, - files: File[], -): Promise<{ fileAndPaths: FileAndPath[]; zipItems: ZipItem[] }> => { +const desktopFilesAndZipItems = async (electron: Electron, files: File[]) => { const fileAndPaths: FileAndPath[] = []; let zipItems: ZipItem[] = []; @@ -809,6 +824,37 @@ const desktopFilesAndZipItems = async ( return { fileAndPaths, zipItems }; }; +/** + * Return the relative path or name of a File object selected or + * drag-and-dropped on the web. + * + * There are three cases here: + * + * 1. If the user selects individual file(s), then the returned File objects + * will only have a `name`. + * + * 2. If the user selects directory(ies), then the returned File objects will + * have a `webkitRelativePath`. For more details, see [Note: + * webkitRelativePath]. In particular, these will POSIX separators. + * + * 3. If the user drags-and-drops, then the react-dropzone library that we use + * will internally convert `webkitRelativePath` to `path`, but otherwise it + * behaves same as case 2. + * https://github.com/react-dropzone/file-selector/blob/master/src/file.ts#L1214 + */ +const pathLikeForWebFile = (file: File): string => + ensure( + firstNonEmpty([ + // We need to check first, since path is not a property of + // the standard File objects. + "path" in file && typeof file.path == "string" + ? file.path + : undefined, + file.webkitRelativePath, + file.name, + ]), + ); + // This is used to prompt the user the make upload strategy choice interface ImportSuggestion { rootFolderName: string; @@ -930,9 +976,5 @@ export const setPendingUploads = async ( } } - await electron.setPendingUploads({ - collectionName, - filePaths, - zipItems: zipItems, - }); + await electron.setPendingUploads({ collectionName, filePaths, zipItems }); }; diff --git a/web/apps/photos/src/components/UploadSelectorInputs.tsx b/web/apps/photos/src/components/UploadSelectorInputs.tsx index 13e33fc6d33ff576f4006085164469f79bc6a085..e22e2f541a3ec4ad29c5b1be6486e579979cbe29 100644 --- a/web/apps/photos/src/components/UploadSelectorInputs.tsx +++ b/web/apps/photos/src/components/UploadSelectorInputs.tsx @@ -1,9 +1,24 @@ -export default function UploadSelectorInputs({ +type GetInputProps = () => React.HTMLAttributes; + +interface UploadSelectorInputsProps { + getDragAndDropInputProps: GetInputProps; + getFileSelectorInputProps: GetInputProps; + getFolderSelectorInputProps: GetInputProps; + getZipFileSelectorInputProps?: GetInputProps; +} + +/** + * Create a bunch of HTML inputs elements, one each for the given props. + * + * These hidden input element serve as the way for us to show various file / + * folder Selector dialogs and handle drag and drop inputs. + */ +export const UploadSelectorInputs: React.FC = ({ getDragAndDropInputProps, getFileSelectorInputProps, getFolderSelectorInputProps, getZipFileSelectorInputProps, -}) { +}) => { return ( <> @@ -14,4 +29,4 @@ export default function UploadSelectorInputs({ )} ); -} +}; diff --git a/web/apps/photos/src/components/WatchFolder.tsx b/web/apps/photos/src/components/WatchFolder.tsx index 710a54168384e02c8e04388b022a85ecac6ceb56..4d2144e0ceeee543721a94f9ec96d24d3a0ca1e5 100644 --- a/web/apps/photos/src/components/WatchFolder.tsx +++ b/web/apps/photos/src/components/WatchFolder.tsx @@ -25,8 +25,8 @@ import { Stack, Tooltip, Typography, + styled, } from "@mui/material"; -import { styled } from "@mui/material/styles"; import { CollectionMappingChoiceModal } from "components/Upload/CollectionMappingChoiceModal"; import { t } from "i18next"; import { AppContext } from "pages/_app"; diff --git a/web/apps/photos/src/components/ml/MLSearchSettings.tsx b/web/apps/photos/src/components/ml/MLSearchSettings.tsx index 583b79529c00e6ab02d6f6734bb9d179488eec48..409df4fc6f1995e5e2e48f696a4d8796fd52fae0 100644 --- a/web/apps/photos/src/components/ml/MLSearchSettings.tsx +++ b/web/apps/photos/src/components/ml/MLSearchSettings.tsx @@ -22,7 +22,7 @@ import { getFaceSearchEnabledStatus, updateFaceSearchEnabledStatus, } from "services/userService"; -import { openLink } from "utils/common"; +import { isInternalUserForML } from "utils/user"; export const MLSearchSettings = ({ open, onClose, onRootClose }) => { const { @@ -255,8 +255,8 @@ function EnableFaceSearch({ open, onClose, enableFaceSearch, onRootClose }) { } function EnableMLSearch({ onClose, enableMlSearch, onRootClose }) { - const showDetails = () => - openLink("https://ente.io/blog/desktop-ml-beta", true); + // const showDetails = () => + // openLink("https://ente.io/blog/desktop-ml-beta", true); return ( @@ -269,25 +269,37 @@ function EnableMLSearch({ onClose, enableMlSearch, onRootClose }) { {" "} - + {/* */} +

+ We're putting finishing touches, coming back soon! +

+

+ + Existing indexed faces will continue to show. + +

- - - + {/* + - + > + {t("ML_MORE_DETAILS")} + + */} +
+ )} ); diff --git a/web/apps/photos/src/components/ml/PeopleList.tsx b/web/apps/photos/src/components/ml/PeopleList.tsx index 8e6bc968f7f518ad1ed6ec797cdb81b628fe52f8..4691d4b650cc81ebbb1517d35a661b71ca2c667f 100644 --- a/web/apps/photos/src/components/ml/PeopleList.tsx +++ b/web/apps/photos/src/components/ml/PeopleList.tsx @@ -1,11 +1,8 @@ -import { cachedOrNew } from "@/next/blob-cache"; -import { ensureLocalUser } from "@/next/local-user"; import log from "@/next/log"; import { Skeleton, styled } from "@mui/material"; import { Legend } from "components/PhotoViewer/styledComponents/Legend"; import { t } from "i18next"; import React, { useEffect, useState } from "react"; -import machineLearningService from "services/machineLearning/machineLearningService"; import { EnteFile } from "types/file"; import { Face, Person } from "types/machineLearning"; import { getPeopleList, getUnidentifiedFaces } from "utils/machineLearning"; @@ -61,7 +58,7 @@ export const PeopleList = React.memo((props: PeopleListProps) => { } > @@ -140,7 +137,7 @@ export function UnidentifiedFaces(props: { faces.map((face, index) => ( @@ -151,20 +148,24 @@ export function UnidentifiedFaces(props: { } interface FaceCropImageViewProps { - faceId: string; + faceID: string; cacheKey?: string; } const FaceCropImageView: React.FC = ({ - faceId, + faceID, cacheKey, }) => { const [objectURL, setObjectURL] = useState(); useEffect(() => { let didCancel = false; + const electron = globalThis.electron; - if (cacheKey) { + if (faceID && electron) { + electron + .legacyFaceCrop(faceID) + /* cachedOrNew("face-crops", cacheKey, async () => { const user = await ensureLocalUser(); return machineLearningService.regenerateFaceCrop( @@ -172,16 +173,20 @@ const FaceCropImageView: React.FC = ({ user.id, faceId, ); - }).then((blob) => { - if (!didCancel) setObjectURL(URL.createObjectURL(blob)); - }); + })*/ + .then((data) => { + if (data) { + const blob = new Blob([data]); + if (!didCancel) setObjectURL(URL.createObjectURL(blob)); + } + }); } else setObjectURL(undefined); return () => { didCancel = true; if (objectURL) URL.revokeObjectURL(objectURL); }; - }, [faceId, cacheKey]); + }, [faceID, cacheKey]); return objectURL ? ( diff --git a/web/apps/photos/src/components/pages/gallery/PlanSelector/card.tsx b/web/apps/photos/src/components/pages/gallery/PlanSelector/card.tsx new file mode 100644 index 0000000000000000000000000000000000000000..6fe86769e12452044dee786eb7bcb2a96363ee8a --- /dev/null +++ b/web/apps/photos/src/components/pages/gallery/PlanSelector/card.tsx @@ -0,0 +1,356 @@ +import log from "@/next/log"; +import { SpaceBetweenFlex } from "@ente/shared/components/Container"; +import { SUPPORT_EMAIL } from "@ente/shared/constants/urls"; +import Close from "@mui/icons-material/Close"; +import { IconButton, Link, Stack } from "@mui/material"; +import Box from "@mui/material/Box"; +import Typography from "@mui/material/Typography"; +import { PLAN_PERIOD } from "constants/gallery"; +import { t } from "i18next"; +import { AppContext } from "pages/_app"; +import { GalleryContext } from "pages/gallery"; +import { useContext, useEffect, useMemo, useState } from "react"; +import { Trans } from "react-i18next"; +import billingService, { type PlansResponse } from "services/billingService"; +import { Plan } from "types/billing"; +import { SetLoading } from "types/gallery"; +import { + getLocalUserSubscription, + hasAddOnBonus, + hasMobileSubscription, + hasPaidSubscription, + hasStripeSubscription, + isOnFreePlan, + isSubscriptionActive, + isSubscriptionCancelled, + isUserSubscribedPlan, + planForSubscription, + updateSubscription, +} from "utils/billing"; +import { bytesInGB } from "utils/units"; +import { getLocalUserDetails } from "utils/user"; +import { getTotalFamilyUsage, isPartOfFamily } from "utils/user/family"; +import { ManageSubscription } from "./manageSubscription"; +import { PeriodToggler } from "./periodToggler"; +import Plans from "./plans"; +import { BFAddOnRow } from "./plans/BfAddOnRow"; + +interface Props { + closeModal: any; + setLoading: SetLoading; +} + +function PlanSelectorCard(props: Props) { + const subscription = useMemo(() => getLocalUserSubscription(), []); + const [plansResponse, setPlansResponse] = useState< + PlansResponse | undefined + >(); + + const [planPeriod, setPlanPeriod] = useState( + subscription?.period || PLAN_PERIOD.MONTH, + ); + const galleryContext = useContext(GalleryContext); + const appContext = useContext(AppContext); + const bonusData = useMemo(() => { + const userDetails = getLocalUserDetails(); + if (!userDetails) { + return null; + } + return userDetails.bonusData; + }, []); + + const usage = useMemo(() => { + const userDetails = getLocalUserDetails(); + if (!userDetails) { + return 0; + } + return isPartOfFamily(userDetails.familyData) + ? getTotalFamilyUsage(userDetails.familyData) + : userDetails.usage; + }, []); + + const togglePeriod = () => { + setPlanPeriod((prevPeriod) => + prevPeriod === PLAN_PERIOD.MONTH + ? PLAN_PERIOD.YEAR + : PLAN_PERIOD.MONTH, + ); + }; + function onReopenClick() { + appContext.closeMessageDialog(); + galleryContext.showPlanSelectorModal(); + } + useEffect(() => { + const main = async () => { + try { + props.setLoading(true); + const response = await billingService.getPlans(); + const { plans } = response; + if (isSubscriptionActive(subscription)) { + const planNotListed = + plans.filter((plan) => + isUserSubscribedPlan(plan, subscription), + ).length === 0; + if ( + subscription && + !isOnFreePlan(subscription) && + planNotListed + ) { + plans.push(planForSubscription(subscription)); + } + } + setPlansResponse(response); + } catch (e) { + log.error("plan selector modal open failed", e); + props.closeModal(); + appContext.setDialogMessage({ + title: t("OPEN_PLAN_SELECTOR_MODAL_FAILED"), + content: t("UNKNOWN_ERROR"), + close: { text: t("CLOSE"), variant: "secondary" }, + proceed: { + text: t("REOPEN_PLAN_SELECTOR_MODAL"), + variant: "accent", + action: onReopenClick, + }, + }); + } finally { + props.setLoading(false); + } + }; + main(); + }, []); + + async function onPlanSelect(plan: Plan) { + if ( + !hasPaidSubscription(subscription) || + isSubscriptionCancelled(subscription) + ) { + try { + props.setLoading(true); + await billingService.buySubscription(plan.stripeID); + } catch (e) { + props.setLoading(false); + appContext.setDialogMessage({ + title: t("ERROR"), + content: t("SUBSCRIPTION_PURCHASE_FAILED"), + close: { variant: "critical" }, + }); + } + } else if (hasStripeSubscription(subscription)) { + appContext.setDialogMessage({ + title: t("update_subscription_title"), + content: t("UPDATE_SUBSCRIPTION_MESSAGE"), + proceed: { + text: t("UPDATE_SUBSCRIPTION"), + action: updateSubscription.bind( + null, + plan, + appContext.setDialogMessage, + props.setLoading, + props.closeModal, + ), + variant: "accent", + }, + close: { text: t("CANCEL") }, + }); + } else if (hasMobileSubscription(subscription)) { + appContext.setDialogMessage({ + title: t("CANCEL_SUBSCRIPTION_ON_MOBILE"), + content: t("CANCEL_SUBSCRIPTION_ON_MOBILE_MESSAGE"), + close: { variant: "secondary" }, + }); + } else { + appContext.setDialogMessage({ + title: t("MANAGE_PLAN"), + content: ( + , + }} + values={{ emailID: SUPPORT_EMAIL }} + /> + ), + close: { variant: "secondary" }, + }); + } + } + + const { closeModal, setLoading } = props; + + const commonCardData = { + subscription, + bonusData, + closeModal, + planPeriod, + togglePeriod, + setLoading, + }; + + const plansList = ( + + ); + + return ( + <> + + {hasPaidSubscription(subscription) ? ( + + {plansList} + + ) : ( + + {plansList} + + )} + + + ); +} + +export default PlanSelectorCard; + +function FreeSubscriptionPlanSelectorCard({ + children, + subscription, + bonusData, + closeModal, + setLoading, + planPeriod, + togglePeriod, +}) { + return ( + <> + + {t("CHOOSE_PLAN")} + + + + + + + + {t("TWO_MONTHS_FREE")} + + + {children} + {hasAddOnBonus(bonusData) && ( + + )} + {hasAddOnBonus(bonusData) && ( + + )} + + + + ); +} + +function PaidSubscriptionPlanSelectorCard({ + children, + subscription, + bonusData, + closeModal, + usage, + planPeriod, + togglePeriod, + setLoading, +}) { + return ( + <> + + + + + {t("SUBSCRIPTION")} + + + {bytesInGB(subscription.storage, 2)}{" "} + {t("storage_unit.gb")} + + + + + + + + + + + + + + + + `1px solid ${theme.palette.divider}`} + p={1.5} + borderRadius={(theme) => `${theme.shape.borderRadius}px`} + > + + + + {t("TWO_MONTHS_FREE")} + + + {children} + + + + + {!isSubscriptionCancelled(subscription) + ? t("RENEWAL_ACTIVE_SUBSCRIPTION_STATUS", { + date: subscription.expiryTime, + }) + : t("RENEWAL_CANCELLED_SUBSCRIPTION_STATUS", { + date: subscription.expiryTime, + })} + + {hasAddOnBonus(bonusData) && ( + + )} + + + + + + ); +} diff --git a/web/apps/photos/src/components/pages/gallery/PlanSelector/card/free.tsx b/web/apps/photos/src/components/pages/gallery/PlanSelector/card/free.tsx deleted file mode 100644 index a2ac1090b774925a4e4ef4ad104bf049213ef4a6..0000000000000000000000000000000000000000 --- a/web/apps/photos/src/components/pages/gallery/PlanSelector/card/free.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import { Stack } from "@mui/material"; -import Box from "@mui/material/Box"; -import Typography from "@mui/material/Typography"; -import { t } from "i18next"; -import { hasAddOnBonus } from "utils/billing"; -import { ManageSubscription } from "../manageSubscription"; -import { PeriodToggler } from "../periodToggler"; -import Plans from "../plans"; -import { BFAddOnRow } from "../plans/BfAddOnRow"; - -export default function FreeSubscriptionPlanSelectorCard({ - plans, - subscription, - bonusData, - closeModal, - setLoading, - planPeriod, - togglePeriod, - onPlanSelect, -}) { - return ( - <> - - {t("CHOOSE_PLAN")} - - - - - - - - {t("TWO_MONTHS_FREE")} - - - - {hasAddOnBonus(bonusData) && ( - - )} - {hasAddOnBonus(bonusData) && ( - - )} - - - - ); -} diff --git a/web/apps/photos/src/components/pages/gallery/PlanSelector/card/index.tsx b/web/apps/photos/src/components/pages/gallery/PlanSelector/card/index.tsx deleted file mode 100644 index 2ef3c361fdfc6ac85514130e51b5da31f6159212..0000000000000000000000000000000000000000 --- a/web/apps/photos/src/components/pages/gallery/PlanSelector/card/index.tsx +++ /dev/null @@ -1,202 +0,0 @@ -import log from "@/next/log"; -import { SUPPORT_EMAIL } from "@ente/shared/constants/urls"; -import { useLocalState } from "@ente/shared/hooks/useLocalState"; -import { LS_KEYS } from "@ente/shared/storage/localStorage"; -import { Link, Stack } from "@mui/material"; -import { PLAN_PERIOD } from "constants/gallery"; -import { t } from "i18next"; -import { AppContext } from "pages/_app"; -import { GalleryContext } from "pages/gallery"; -import { useContext, useEffect, useMemo, useState } from "react"; -import { Trans } from "react-i18next"; -import billingService from "services/billingService"; -import { Plan } from "types/billing"; -import { SetLoading } from "types/gallery"; -import { - getLocalUserSubscription, - hasMobileSubscription, - hasPaidSubscription, - hasStripeSubscription, - isOnFreePlan, - isSubscriptionActive, - isSubscriptionCancelled, - isUserSubscribedPlan, - planForSubscription, - updateSubscription, -} from "utils/billing"; -import { getLocalUserDetails } from "utils/user"; -import { getTotalFamilyUsage, isPartOfFamily } from "utils/user/family"; -import FreeSubscriptionPlanSelectorCard from "./free"; -import PaidSubscriptionPlanSelectorCard from "./paid"; - -interface Props { - closeModal: any; - setLoading: SetLoading; -} - -function PlanSelectorCard(props: Props) { - const subscription = useMemo(() => getLocalUserSubscription(), []); - const [plans, setPlans] = useLocalState(LS_KEYS.PLANS); - - const [planPeriod, setPlanPeriod] = useState( - subscription?.period || PLAN_PERIOD.MONTH, - ); - const galleryContext = useContext(GalleryContext); - const appContext = useContext(AppContext); - const bonusData = useMemo(() => { - const userDetails = getLocalUserDetails(); - if (!userDetails) { - return null; - } - return userDetails.bonusData; - }, []); - - const usage = useMemo(() => { - const userDetails = getLocalUserDetails(); - if (!userDetails) { - return 0; - } - return isPartOfFamily(userDetails.familyData) - ? getTotalFamilyUsage(userDetails.familyData) - : userDetails.usage; - }, []); - - const togglePeriod = () => { - setPlanPeriod((prevPeriod) => - prevPeriod === PLAN_PERIOD.MONTH - ? PLAN_PERIOD.YEAR - : PLAN_PERIOD.MONTH, - ); - }; - function onReopenClick() { - appContext.closeMessageDialog(); - galleryContext.showPlanSelectorModal(); - } - useEffect(() => { - const main = async () => { - try { - props.setLoading(true); - const plans = await billingService.getPlans(); - if (isSubscriptionActive(subscription)) { - const planNotListed = - plans.filter((plan) => - isUserSubscribedPlan(plan, subscription), - ).length === 0; - if ( - subscription && - !isOnFreePlan(subscription) && - planNotListed - ) { - plans.push(planForSubscription(subscription)); - } - } - setPlans(plans); - } catch (e) { - log.error("plan selector modal open failed", e); - props.closeModal(); - appContext.setDialogMessage({ - title: t("OPEN_PLAN_SELECTOR_MODAL_FAILED"), - content: t("UNKNOWN_ERROR"), - close: { text: t("CLOSE"), variant: "secondary" }, - proceed: { - text: t("REOPEN_PLAN_SELECTOR_MODAL"), - variant: "accent", - action: onReopenClick, - }, - }); - } finally { - props.setLoading(false); - } - }; - main(); - }, []); - - async function onPlanSelect(plan: Plan) { - if ( - !hasPaidSubscription(subscription) || - isSubscriptionCancelled(subscription) - ) { - try { - props.setLoading(true); - await billingService.buySubscription(plan.stripeID); - } catch (e) { - props.setLoading(false); - appContext.setDialogMessage({ - title: t("ERROR"), - content: t("SUBSCRIPTION_PURCHASE_FAILED"), - close: { variant: "critical" }, - }); - } - } else if (hasStripeSubscription(subscription)) { - appContext.setDialogMessage({ - title: t("update_subscription_title"), - content: t("UPDATE_SUBSCRIPTION_MESSAGE"), - proceed: { - text: t("UPDATE_SUBSCRIPTION"), - action: updateSubscription.bind( - null, - plan, - appContext.setDialogMessage, - props.setLoading, - props.closeModal, - ), - variant: "accent", - }, - close: { text: t("CANCEL") }, - }); - } else if (hasMobileSubscription(subscription)) { - appContext.setDialogMessage({ - title: t("CANCEL_SUBSCRIPTION_ON_MOBILE"), - content: t("CANCEL_SUBSCRIPTION_ON_MOBILE_MESSAGE"), - close: { variant: "secondary" }, - }); - } else { - appContext.setDialogMessage({ - title: t("MANAGE_PLAN"), - content: ( - , - }} - values={{ emailID: SUPPORT_EMAIL }} - /> - ), - close: { variant: "secondary" }, - }); - } - } - - return ( - <> - - {hasPaidSubscription(subscription) ? ( - - ) : ( - - )} - - - ); -} - -export default PlanSelectorCard; diff --git a/web/apps/photos/src/components/pages/gallery/PlanSelector/card/paid.tsx b/web/apps/photos/src/components/pages/gallery/PlanSelector/card/paid.tsx deleted file mode 100644 index 4ef76a491ff1888e582ca4446c4c8051e10b67fa..0000000000000000000000000000000000000000 --- a/web/apps/photos/src/components/pages/gallery/PlanSelector/card/paid.tsx +++ /dev/null @@ -1,112 +0,0 @@ -import { SpaceBetweenFlex } from "@ente/shared/components/Container"; -import Close from "@mui/icons-material/Close"; -import { IconButton, Stack } from "@mui/material"; -import Box from "@mui/material/Box"; -import Typography from "@mui/material/Typography"; -import { t } from "i18next"; -import { Trans } from "react-i18next"; -import { - convertBytesToGBs, - hasAddOnBonus, - isSubscriptionCancelled, -} from "utils/billing"; -import { ManageSubscription } from "../manageSubscription"; -import { PeriodToggler } from "../periodToggler"; -import Plans from "../plans"; -import { BFAddOnRow } from "../plans/BfAddOnRow"; - -export default function PaidSubscriptionPlanSelectorCard({ - plans, - subscription, - bonusData, - closeModal, - usage, - planPeriod, - togglePeriod, - onPlanSelect, - setLoading, -}) { - return ( - <> - - - - - {t("SUBSCRIPTION")} - - - {convertBytesToGBs(subscription.storage, 2)}{" "} - {t("GB")} - - - - - - - - - - - - - - - - `1px solid ${theme.palette.divider}`} - p={1.5} - borderRadius={(theme) => `${theme.shape.borderRadius}px`} - > - - - - {t("TWO_MONTHS_FREE")} - - - - - - - - {!isSubscriptionCancelled(subscription) - ? t("RENEWAL_ACTIVE_SUBSCRIPTION_STATUS", { - date: subscription.expiryTime, - }) - : t("RENEWAL_CANCELLED_SUBSCRIPTION_STATUS", { - date: subscription.expiryTime, - })} - - {hasAddOnBonus(bonusData) && ( - - )} - - - - - - ); -} diff --git a/web/apps/photos/src/components/pages/gallery/PlanSelector/plans/BfAddOnRow.tsx b/web/apps/photos/src/components/pages/gallery/PlanSelector/plans/BfAddOnRow.tsx index 8b0ce7bd5f6e42517e045eda617f864df56e1ab6..5f7e13deb8be4081bcdbda67b7ba8b025f0248b1 100644 --- a/web/apps/photos/src/components/pages/gallery/PlanSelector/plans/BfAddOnRow.tsx +++ b/web/apps/photos/src/components/pages/gallery/PlanSelector/plans/BfAddOnRow.tsx @@ -2,7 +2,7 @@ import { SpaceBetweenFlex } from "@ente/shared/components/Container"; import { Box, styled, Typography } from "@mui/material"; import { Trans } from "react-i18next"; -import { makeHumanReadableStorage } from "utils/billing"; +import { formattedStorageByteSize } from "utils/units"; const RowContainer = styled(SpaceBetweenFlex)(({ theme }) => ({ // gap: theme.spacing(1.5), @@ -24,7 +24,7 @@ export function BFAddOnRow({ bonusData, closeModal }) { ({ - gap: theme.spacing(1.5), - padding: theme.spacing(1.5, 1), - cursor: "pointer", - "&:hover .endIcon": { - backgroundColor: "rgba(255,255,255,0.08)", - }, -})); -export function FreePlanRow({ closeModal }) { - return ( - - - {t("FREE_PLAN_OPTION_LABEL")} - - {t("FREE_PLAN_DESCRIPTION")} - - - - - - - ); -} diff --git a/web/apps/photos/src/components/pages/gallery/PlanSelector/plans/index.tsx b/web/apps/photos/src/components/pages/gallery/PlanSelector/plans/index.tsx index ed1a666edb14a89fdc603eb90bbe47f444b8f958..31e97c68e6a90c719ff308889a264b2ebf99f790 100644 --- a/web/apps/photos/src/components/pages/gallery/PlanSelector/plans/index.tsx +++ b/web/apps/photos/src/components/pages/gallery/PlanSelector/plans/index.tsx @@ -1,5 +1,9 @@ -import { Stack } from "@mui/material"; +import { SpaceBetweenFlex } from "@ente/shared/components/Container"; +import ArrowForward from "@mui/icons-material/ArrowForward"; +import { Box, IconButton, Stack, Typography, styled } from "@mui/material"; import { PLAN_PERIOD } from "constants/gallery"; +import { t } from "i18next"; +import type { PlansResponse } from "services/billingService"; import { Plan, Subscription } from "types/billing"; import { BonusData } from "types/user"; import { @@ -8,11 +12,11 @@ import { isPopularPlan, isUserSubscribedPlan, } from "utils/billing"; -import { FreePlanRow } from "./FreePlanRow"; +import { formattedStorageByteSize } from "utils/units"; import { PlanRow } from "./planRow"; interface Iprops { - plans: Plan[]; + plansResponse: PlansResponse | undefined; planPeriod: PLAN_PERIOD; subscription: Subscription; bonusData?: BonusData; @@ -21,30 +25,70 @@ interface Iprops { } const Plans = ({ - plans, + plansResponse, planPeriod, subscription, bonusData, onPlanSelect, closeModal, -}: Iprops) => ( - - {plans - ?.filter((plan) => plan.period === planPeriod) - ?.map((plan) => ( - - ))} - {!hasPaidSubscription(subscription) && !hasAddOnBonus(bonusData) && ( - - )} - -); +}: Iprops) => { + const { freePlan, plans } = plansResponse ?? {}; + return ( + + {plans + ?.filter((plan) => plan.period === planPeriod) + ?.map((plan) => ( + + ))} + {!hasPaidSubscription(subscription) && + !hasAddOnBonus(bonusData) && + freePlan && ( + + )} + + ); +}; export default Plans; + +interface FreePlanRowProps { + storage: number; + closeModal: () => void; +} + +const FreePlanRow: React.FC = ({ closeModal, storage }) => { + return ( + + + {t("FREE_PLAN_OPTION_LABEL")} + + {t("free_plan_description", { + storage: formattedStorageByteSize(storage), + })} + + + + + + + ); +}; + +const FreePlanRow_ = styled(SpaceBetweenFlex)(({ theme }) => ({ + gap: theme.spacing(1.5), + padding: theme.spacing(1.5, 1), + cursor: "pointer", + "&:hover .endIcon": { + backgroundColor: "rgba(255,255,255,0.08)", + }, +})); diff --git a/web/apps/photos/src/components/pages/gallery/PlanSelector/plans/planRow.tsx b/web/apps/photos/src/components/pages/gallery/PlanSelector/plans/planRow.tsx index 6363caee4d2504221cd2126eb96436aa68e81192..9f1351b120f34bd7c34b0a6bb1f0cbbacc21d8a1 100644 --- a/web/apps/photos/src/components/pages/gallery/PlanSelector/plans/planRow.tsx +++ b/web/apps/photos/src/components/pages/gallery/PlanSelector/plans/planRow.tsx @@ -6,11 +6,8 @@ import { Badge } from "components/Badge"; import { PLAN_PERIOD } from "constants/gallery"; import { t } from "i18next"; import { Plan, Subscription } from "types/billing"; -import { - convertBytesToGBs, - hasPaidSubscription, - isUserSubscribedPlan, -} from "utils/billing"; +import { hasPaidSubscription, isUserSubscribedPlan } from "utils/billing"; +import { bytesInGB } from "utils/units"; interface Iprops { plan: Plan; @@ -66,11 +63,11 @@ export function PlanRow({ - {convertBytesToGBs(plan.storage)} + {bytesInGB(plan.storage)} - {t("GB")} + {t("storage_unit.gb")} {popular && !hasPaidSubscription(subscription) && ( {t("POPULAR")} diff --git a/web/apps/photos/src/pages/gallery/index.tsx b/web/apps/photos/src/pages/gallery/index.tsx index 70b48c3cc6f3d0686a09340944751d3231eaf0d9..f90d1b83717de6f342a11ebac649b93c207f7131 100644 --- a/web/apps/photos/src/pages/gallery/index.tsx +++ b/web/apps/photos/src/pages/gallery/index.tsx @@ -1,82 +1,36 @@ -import { - SESSION_KEYS, - clearKeys, - getKey, -} from "@ente/shared/storage/sessionStorage"; -import { Typography, styled } from "@mui/material"; -import { t } from "i18next"; -import { useRouter } from "next/router"; -import { - createContext, - useContext, - useEffect, - useMemo, - useRef, - useState, -} from "react"; -import { - constructEmailList, - createAlbum, - getAllLatestCollections, - getAllLocalCollections, - getCollectionSummaries, - getFavItemIds, - getHiddenItemsSummary, - getSectionSummaries, -} from "services/collectionService"; -import { getLocalFiles, syncFiles } from "services/fileService"; - -import { checkSubscriptionPurchase } from "utils/billing"; - -import EnteSpinner from "@ente/shared/components/EnteSpinner"; -import { - isFirstLogin, - justSignedUp, - setIsFirstLogin, - setJustSignedUp, -} from "@ente/shared/storage/localStorage/helpers"; -import CollectionSelector, { - CollectionSelectorAttributes, -} from "components/Collections/CollectionSelector"; -import FullScreenDropZone from "components/FullScreenDropZone"; -import { LoadingOverlay } from "components/LoadingOverlay"; -import PhotoFrame from "components/PhotoFrame"; -import Sidebar from "components/Sidebar"; -import SelectedFileOptions from "components/pages/gallery/SelectedFileOptions"; -import { useDropzone } from "react-dropzone"; -import { - isTokenValid, - syncMapEnabled, - validateKey, -} from "services/userService"; -import { preloadImage } from "utils/common"; -import { - FILE_OPS_TYPE, - constructFileToCollectionMap, - getSelectedFiles, - getUniqueFiles, - handleFileOps, - mergeMetadata, - sortFiles, -} from "utils/file"; - import log from "@/next/log"; import { APPS } from "@ente/shared/apps/constants"; import { CenteredFlex } from "@ente/shared/components/Container"; +import EnteSpinner from "@ente/shared/components/EnteSpinner"; import { PHOTOS_PAGES as PAGES } from "@ente/shared/constants/pages"; import { CustomError } from "@ente/shared/error"; -import useFileInput from "@ente/shared/hooks/useFileInput"; +import { useFileInput } from "@ente/shared/hooks/useFileInput"; import useMemoSingleThreaded from "@ente/shared/hooks/useMemoSingleThreaded"; import InMemoryStore, { MS_KEYS } from "@ente/shared/storage/InMemoryStore"; import { LS_KEYS, getData } from "@ente/shared/storage/localStorage"; -import { getToken } from "@ente/shared/storage/localStorage/helpers"; +import { + getToken, + isFirstLogin, + justSignedUp, + setIsFirstLogin, + setJustSignedUp, +} from "@ente/shared/storage/localStorage/helpers"; +import { + SESSION_KEYS, + clearKeys, + getKey, +} from "@ente/shared/storage/sessionStorage"; import { User } from "@ente/shared/user/types"; import { isPromise } from "@ente/shared/utils"; +import { Typography, styled } from "@mui/material"; import AuthenticateUserModal from "components/AuthenticateUserModal"; import Collections from "components/Collections"; import CollectionNamer, { CollectionNamerAttributes, } from "components/Collections/CollectionNamer"; +import CollectionSelector, { + CollectionSelectorAttributes, +} from "components/Collections/CollectionSelector"; import ExportModal from "components/ExportModal"; import { FilesDownloadProgress, @@ -85,31 +39,62 @@ import { import FixCreationTime, { FixCreationTimeAttributes, } from "components/FixCreationTime"; +import FullScreenDropZone from "components/FullScreenDropZone"; import GalleryEmptyState from "components/GalleryEmptyState"; +import { LoadingOverlay } from "components/LoadingOverlay"; +import PhotoFrame from "components/PhotoFrame"; import { ITEM_TYPE, TimeStampListItem } from "components/PhotoList"; import SearchResultInfo from "components/Search/SearchResultInfo"; +import Sidebar from "components/Sidebar"; import Uploader from "components/Upload/Uploader"; -import UploadInputs from "components/UploadSelectorInputs"; +import { UploadSelectorInputs } from "components/UploadSelectorInputs"; import { GalleryNavbar } from "components/pages/gallery/Navbar"; import PlanSelector from "components/pages/gallery/PlanSelector"; +import SelectedFileOptions from "components/pages/gallery/SelectedFileOptions"; import { ALL_SECTION, ARCHIVE_SECTION, CollectionSummaryType, - DUMMY_UNCATEGORIZED_COLLECTION, HIDDEN_ITEMS_SECTION, TRASH_SECTION, } from "constants/collection"; import { SYNC_INTERVAL_IN_MICROSECONDS } from "constants/gallery"; +import { t } from "i18next"; +import { useRouter } from "next/router"; import { AppContext } from "pages/_app"; +import { + createContext, + useContext, + useEffect, + useMemo, + useRef, + useState, +} from "react"; +import { useDropzone } from "react-dropzone"; import { clipService } from "services/clip-service"; -import { constructUserIDToEmailMap } from "services/collectionService"; +import { + constructEmailList, + constructUserIDToEmailMap, + createAlbum, + getAllLatestCollections, + getAllLocalCollections, + getCollectionSummaries, + getFavItemIds, + getHiddenItemsSummary, + getSectionSummaries, +} from "services/collectionService"; import downloadManager from "services/download"; import { syncEmbeddings, syncFileEmbeddings } from "services/embeddingService"; import { syncEntities } from "services/entityService"; +import { getLocalFiles, syncFiles } from "services/fileService"; import locationSearchService from "services/locationSearchService"; import { getLocalTrashedFiles, syncTrash } from "services/trashService"; import uploadManager from "services/upload/uploadManager"; +import { + isTokenValid, + syncMapEnabled, + validateKey, +} from "services/userService"; import { Collection, CollectionSummaries } from "types/collection"; import { EnteFile } from "types/file"; import { @@ -121,6 +106,7 @@ import { } from "types/gallery"; import { Search, SearchResultSummary, UpdateSearch } from "types/search"; import { FamilyData } from "types/user"; +import { checkSubscriptionPurchase } from "utils/billing"; import { COLLECTION_OPS_TYPE, constructCollectionNameMap, @@ -132,6 +118,16 @@ import { splitNormalAndHiddenCollections, } from "utils/collection"; import ComlinkSearchWorker from "utils/comlink/ComlinkSearchWorker"; +import { preloadImage } from "utils/common"; +import { + FILE_OPS_TYPE, + constructFileToCollectionMap, + getSelectedFiles, + getUniqueFiles, + handleFileOps, + mergeMetadata, + sortFiles, +} from "utils/file"; import { isArchivedFile } from "utils/magicMetadata"; import { getSessionExpiredMessage } from "utils/ui"; import { getLocalFamilyData } from "utils/user/family"; @@ -202,8 +198,11 @@ export default function Gallery() { const [isPhotoSwipeOpen, setIsPhotoSwipeOpen] = useState(false); const { + // A function to call to get the props we should apply to the container, getRootProps: getDragAndDropRootProps, + // ... the props we should apply to the element, getInputProps: getDragAndDropInputProps, + // ... and the files that we got. acceptedFiles: dragAndDropFiles, } = useDropzone({ noClick: true, @@ -211,23 +210,23 @@ export default function Gallery() { disabled: shouldDisableDropzone, }); const { - selectedFiles: fileSelectorFiles, - open: openFileSelector, getInputProps: getFileSelectorInputProps, + openSelector: openFileSelector, + selectedFiles: fileSelectorFiles, } = useFileInput({ directory: false, }); const { - selectedFiles: folderSelectorFiles, - open: openFolderSelector, getInputProps: getFolderSelectorInputProps, + openSelector: openFolderSelector, + selectedFiles: folderSelectorFiles, } = useFileInput({ directory: true, }); const { - selectedFiles: fileSelectorZipFiles, - open: openZipFileSelector, getInputProps: getZipFileSelectorInputProps, + openSelector: openZipFileSelector, + selectedFiles: fileSelectorZipFiles, } = useFileInput({ directory: false, accept: ".zip", @@ -370,7 +369,7 @@ export default function Gallery() { syncWithRemote(false, true); }, SYNC_INTERVAL_IN_MICROSECONDS); if (electron) { - void clipService.setupOnFileUploadListener(); + // void clipService.setupOnFileUploadListener(); electron.onMainWindowFocus(() => syncWithRemote(false, true)); } }; @@ -446,18 +445,8 @@ export default function Gallery() { } let collectionURL = ""; if (activeCollectionID !== ALL_SECTION) { - collectionURL += "?collection="; - if (activeCollectionID === ARCHIVE_SECTION) { - collectionURL += t("ARCHIVE_SECTION_NAME"); - } else if (activeCollectionID === TRASH_SECTION) { - collectionURL += t("TRASH"); - } else if (activeCollectionID === DUMMY_UNCATEGORIZED_COLLECTION) { - collectionURL += t("UNCATEGORIZED"); - } else if (activeCollectionID === HIDDEN_ITEMS_SECTION) { - collectionURL += t("HIDDEN_ITEMS_SECTION_NAME"); - } else { - collectionURL += activeCollectionID; - } + // TODO: Is this URL param even used? + collectionURL = `?collection=${activeCollectionID}`; } const href = `/gallery${collectionURL}`; router.push(href, undefined, { shallow: true }); @@ -1024,14 +1013,14 @@ export default function Gallery() { setSelectedFiles: setSelected, }} > - - + {blockingLoad && ( diff --git a/web/apps/photos/src/pages/shared-albums/index.tsx b/web/apps/photos/src/pages/shared-albums/index.tsx index ee6284d4a2fd1c4f13f52ba7a3dbe77002e859b8..ab35b23facf1e8ebc58329ceaab7c24a754105e5 100644 --- a/web/apps/photos/src/pages/shared-albums/index.tsx +++ b/web/apps/photos/src/pages/shared-albums/index.tsx @@ -1,40 +1,11 @@ import log from "@/next/log"; +import { logoutUser } from "@ente/accounts/services/user"; +import { APPS } from "@ente/shared/apps/constants"; import { CenteredFlex, SpaceBetweenFlex, VerticallyCentered, } from "@ente/shared/components/Container"; -import { CustomError, parseSharingErrorCodes } from "@ente/shared/error"; -import PhotoFrame from "components/PhotoFrame"; -import { ALL_SECTION } from "constants/collection"; -import { t } from "i18next"; -import { AppContext } from "pages/_app"; -import { useContext, useEffect, useMemo, useRef, useState } from "react"; -import { - getLocalPublicCollection, - getLocalPublicCollectionPassword, - getLocalPublicFiles, - getPublicCollection, - getPublicCollectionUID, - getReferralCode, - removePublicCollectionWithFiles, - removePublicFiles, - savePublicCollectionPassword, - syncPublicFiles, - verifyPublicCollectionPassword, -} from "services/publicCollectionService"; -import { Collection } from "types/collection"; -import { EnteFile } from "types/file"; -import { - downloadSelectedFiles, - getSelectedFiles, - mergeMetadata, - sortFiles, -} from "utils/file"; -import { PublicCollectionGalleryContext } from "utils/publicCollectionGallery"; - -import { logoutUser } from "@ente/accounts/services/user"; -import { APPS } from "@ente/shared/apps/constants"; import EnteSpinner from "@ente/shared/components/EnteSpinner"; import FormPaper from "@ente/shared/components/Form/FormPaper"; import FormPaperTitle from "@ente/shared/components/Form/FormPaper/Title"; @@ -46,7 +17,8 @@ import SingleInputForm, { import { PHOTOS_PAGES as PAGES } from "@ente/shared/constants/pages"; import { ENTE_WEBSITE_LINK } from "@ente/shared/constants/urls"; import ComlinkCryptoWorker from "@ente/shared/crypto"; -import useFileInput from "@ente/shared/hooks/useFileInput"; +import { CustomError, parseSharingErrorCodes } from "@ente/shared/error"; +import { useFileInput } from "@ente/shared/hooks/useFileInput"; import AddPhotoAlternateOutlined from "@mui/icons-material/AddPhotoAlternateOutlined"; import FileDownloadOutlinedIcon from "@mui/icons-material/FileDownloadOutlined"; import MoreHoriz from "@mui/icons-material/MoreHoriz"; @@ -60,15 +32,35 @@ import { } from "components/FilesDownloadProgress"; import FullScreenDropZone from "components/FullScreenDropZone"; import { LoadingOverlay } from "components/LoadingOverlay"; +import PhotoFrame from "components/PhotoFrame"; import { ITEM_TYPE, TimeStampListItem } from "components/PhotoList"; import UploadButton from "components/Upload/UploadButton"; import Uploader from "components/Upload/Uploader"; -import UploadSelectorInputs from "components/UploadSelectorInputs"; +import { UploadSelectorInputs } from "components/UploadSelectorInputs"; import SharedAlbumNavbar from "components/pages/sharedAlbum/Navbar"; import SelectedFileOptions from "components/pages/sharedAlbum/SelectedFileOptions"; +import { ALL_SECTION } from "constants/collection"; +import { t } from "i18next"; import { useRouter } from "next/router"; +import { AppContext } from "pages/_app"; +import { useContext, useEffect, useMemo, useRef, useState } from "react"; import { useDropzone } from "react-dropzone"; import downloadManager from "services/download"; +import { + getLocalPublicCollection, + getLocalPublicCollectionPassword, + getLocalPublicFiles, + getPublicCollection, + getPublicCollectionUID, + getReferralCode, + removePublicCollectionWithFiles, + removePublicFiles, + savePublicCollectionPassword, + syncPublicFiles, + verifyPublicCollectionPassword, +} from "services/publicCollectionService"; +import { Collection } from "types/collection"; +import { EnteFile } from "types/file"; import { SelectedState, SetFilesDownloadProgressAttributes, @@ -76,6 +68,13 @@ import { UploadTypeSelectorIntent, } from "types/gallery"; import { downloadCollectionFiles, isHiddenCollection } from "utils/collection"; +import { + downloadSelectedFiles, + getSelectedFiles, + mergeMetadata, + sortFiles, +} from "utils/file"; +import { PublicCollectionGalleryContext } from "utils/publicCollectionGallery"; export default function PublicCollectionGallery() { const token = useRef(null); @@ -118,16 +117,16 @@ export default function PublicCollectionGallery() { disabled: shouldDisableDropzone, }); const { - selectedFiles: fileSelectorFiles, - open: openFileSelector, getInputProps: getFileSelectorInputProps, + openSelector: openFileSelector, + selectedFiles: fileSelectorFiles, } = useFileInput({ directory: false, }); const { - selectedFiles: folderSelectorFiles, - open: openFolderSelector, getInputProps: getFolderSelectorInputProps, + openSelector: openFolderSelector, + selectedFiles: folderSelectorFiles, } = useFileInput({ directory: true, }); @@ -543,14 +542,13 @@ export default function PublicCollectionGallery() { photoListFooter, }} > - + { + public async getPlans(): Promise { const token = getToken(); try { let response; @@ -37,8 +47,7 @@ class billingService { }, ); } - const { plans } = response.data; - return plans; + return response.data; } catch (e) { log.error("failed to get plans", e); } diff --git a/web/apps/photos/src/services/embeddingService.ts b/web/apps/photos/src/services/embeddingService.ts index a4309e314cbf637cd96653285f4b956247389a90..36af848424a9671e1dce266ce5d32c445fbfefee 100644 --- a/web/apps/photos/src/services/embeddingService.ts +++ b/web/apps/photos/src/services/embeddingService.ts @@ -86,7 +86,11 @@ export const syncEmbeddings = async () => { allLocalFiles.forEach((file) => { fileIdToKeyMap.set(file.id, file.key); }); - await cleanupDeletedEmbeddings(allLocalFiles, allEmbeddings); + await cleanupDeletedEmbeddings( + allLocalFiles, + allEmbeddings, + EMBEDDINGS_TABLE, + ); log.info(`Syncing embeddings localCount: ${allEmbeddings.length}`); for (const model of models) { let modelLastSinceTime = await getModelEmbeddingSyncTime(model); @@ -168,7 +172,11 @@ export const syncFileEmbeddings = async () => { allLocalFiles.forEach((file) => { fileIdToKeyMap.set(file.id, file.key); }); - await cleanupDeletedEmbeddings(allLocalFiles, allEmbeddings); + await cleanupDeletedEmbeddings( + allLocalFiles, + allEmbeddings, + FILE_EMBEDING_TABLE, + ); log.info(`Syncing embeddings localCount: ${allEmbeddings.length}`); for (const model of models) { let modelLastSinceTime = await getModelEmbeddingSyncTime(model); @@ -289,6 +297,7 @@ export const putEmbedding = async ( export const cleanupDeletedEmbeddings = async ( allLocalFiles: EnteFile[], allLocalEmbeddings: Embedding[] | FileML[], + tableName: string, ) => { const activeFileIds = new Set(); allLocalFiles.forEach((file) => { @@ -302,6 +311,6 @@ export const cleanupDeletedEmbeddings = async ( log.info( `cleanupDeletedEmbeddings embeddingsCount: ${allLocalEmbeddings.length} remainingEmbeddingsCount: ${remainingEmbeddings.length}`, ); - await localForage.setItem(EMBEDDINGS_TABLE, remainingEmbeddings); + await localForage.setItem(tableName, remainingEmbeddings); } }; diff --git a/web/apps/photos/src/services/exif.ts b/web/apps/photos/src/services/exif.ts index 584d79f880714c128ec38bbf528095d1c35f892a..073a695f7584ac16fccda82b5393f359ea4ed4a4 100644 --- a/web/apps/photos/src/services/exif.ts +++ b/web/apps/photos/src/services/exif.ts @@ -167,14 +167,7 @@ function parseExifData(exifData: RawEXIFData): ParsedEXIFData { parsedExif.imageWidth = ImageWidth; parsedExif.imageHeight = ImageHeight; } else { - log.error( - `Image dimension parsing failed - ImageWidth or ImageHeight is not a number ${JSON.stringify( - { - ImageWidth, - ImageHeight, - }, - )}`, - ); + log.warn("EXIF: Ignoring non-numeric ImageWidth or ImageHeight"); } } else if (ExifImageWidth && ExifImageHeight) { if ( @@ -184,13 +177,8 @@ function parseExifData(exifData: RawEXIFData): ParsedEXIFData { parsedExif.imageWidth = ExifImageWidth; parsedExif.imageHeight = ExifImageHeight; } else { - log.error( - `Image dimension parsing failed - ExifImageWidth or ExifImageHeight is not a number ${JSON.stringify( - { - ExifImageWidth, - ExifImageHeight, - }, - )}`, + log.warn( + "EXIF: Ignoring non-numeric ExifImageWidth or ExifImageHeight", ); } } else if (PixelXDimension && PixelYDimension) { @@ -201,13 +189,8 @@ function parseExifData(exifData: RawEXIFData): ParsedEXIFData { parsedExif.imageWidth = PixelXDimension; parsedExif.imageHeight = PixelYDimension; } else { - log.error( - `Image dimension parsing failed - PixelXDimension or PixelYDimension is not a number ${JSON.stringify( - { - PixelXDimension, - PixelYDimension, - }, - )}`, + log.warn( + "EXIF: Ignoring non-numeric PixelXDimension or PixelYDimension", ); } } @@ -302,15 +285,13 @@ export function parseEXIFLocation( ); return { latitude, longitude }; } catch (e) { - log.error( - `Failed to parseEXIFLocation ${JSON.stringify({ - gpsLatitude, - gpsLatitudeRef, - gpsLongitude, - gpsLongitudeRef, - })}`, - e, - ); + const p = { + gpsLatitude, + gpsLatitudeRef, + gpsLongitude, + gpsLongitudeRef, + }; + log.error(`Failed to parse EXIF location ${JSON.stringify(p)}`, e); return { ...NULL_LOCATION }; } } diff --git a/web/apps/photos/src/services/export/index.ts b/web/apps/photos/src/services/export/index.ts index 82dfdbf8bf2e4f6e2cd0cdb66329d06b441ba4a4..786932ff8289ccb08ef7bd89bd9ef2e1a5d23419 100644 --- a/web/apps/photos/src/services/export/index.ts +++ b/web/apps/photos/src/services/export/index.ts @@ -547,6 +547,9 @@ class ExportService { isCanceled: CancellationStatus, ) { const fs = ensureElectron().fs; + const rmdirIfExists = async (dirPath: string) => { + if (await fs.exists(dirPath)) await fs.rmdir(dirPath); + }; try { const exportRecord = await this.getExportRecord(exportFolder); const collectionIDPathMap = @@ -581,11 +584,11 @@ class ExportService { ); try { // delete the collection metadata folder - await fs.rmdir( + await rmdirIfExists( getMetadataFolderExportPath(collectionExportPath), ); // delete the collection folder - await fs.rmdir(collectionExportPath); + await rmdirIfExists(collectionExportPath); } catch (e) { await this.addCollectionExportedRecord( exportFolder, @@ -731,38 +734,31 @@ class ExportService { const collectionExportName = collectionIDExportNameMap.get(collectionID); - await this.removeFileExportedRecord(exportDir, fileUID); - try { - if (isLivePhotoExportName(fileExportName)) { - const { image, video } = - parseLivePhotoExportName(fileExportName); + if (isLivePhotoExportName(fileExportName)) { + const { image, video } = + parseLivePhotoExportName(fileExportName); - await moveToTrash( - exportDir, - collectionExportName, - image, - ); + await moveToTrash( + exportDir, + collectionExportName, + image, + ); - await moveToTrash( - exportDir, - collectionExportName, - video, - ); - } else { - await moveToTrash( - exportDir, - collectionExportName, - fileExportName, - ); - } - } catch (e) { - await this.addFileExportedRecord( + await moveToTrash( exportDir, - fileUID, + collectionExportName, + video, + ); + } else { + await moveToTrash( + exportDir, + collectionExportName, fileExportName, ); - throw e; } + + await this.removeFileExportedRecord(exportDir, fileUID); + log.info(`Moved file id ${fileUID} to Trash`); } catch (e) { log.error("trashing failed for a file", e); @@ -982,26 +978,21 @@ class ExportService { file.metadata.title, electron.fs.exists, ); + await this.saveMetadataFile( + collectionExportPath, + fileExportName, + file, + ); + await writeStream( + electron, + `${collectionExportPath}/${fileExportName}`, + updatedFileStream, + ); await this.addFileExportedRecord( exportDir, fileUID, fileExportName, ); - try { - await this.saveMetadataFile( - collectionExportPath, - fileExportName, - file, - ); - await writeStream( - electron, - `${collectionExportPath}/${fileExportName}`, - updatedFileStream, - ); - } catch (e) { - await this.removeFileExportedRecord(exportDir, fileUID); - throw e; - } } } catch (e) { log.error("download and save failed", e); @@ -1029,52 +1020,46 @@ class ExportService { livePhoto.videoFileName, fs.exists, ); + const livePhotoExportName = getLivePhotoExportName( imageExportName, videoExportName, ); - await this.addFileExportedRecord( - exportDir, - fileUID, - livePhotoExportName, + + const imageStream = generateStreamFromArrayBuffer(livePhoto.imageData); + await this.saveMetadataFile( + collectionExportPath, + imageExportName, + file, + ); + await writeStream( + electron, + `${collectionExportPath}/${imageExportName}`, + imageStream, + ); + + const videoStream = generateStreamFromArrayBuffer(livePhoto.videoData); + await this.saveMetadataFile( + collectionExportPath, + videoExportName, + file, ); try { - const imageStream = generateStreamFromArrayBuffer( - livePhoto.imageData, - ); - await this.saveMetadataFile( - collectionExportPath, - imageExportName, - file, - ); await writeStream( electron, - `${collectionExportPath}/${imageExportName}`, - imageStream, + `${collectionExportPath}/${videoExportName}`, + videoStream, ); - - const videoStream = generateStreamFromArrayBuffer( - livePhoto.videoData, - ); - await this.saveMetadataFile( - collectionExportPath, - videoExportName, - file, - ); - try { - await writeStream( - electron, - `${collectionExportPath}/${videoExportName}`, - videoStream, - ); - } catch (e) { - await fs.rm(`${collectionExportPath}/${imageExportName}`); - throw e; - } } catch (e) { - await this.removeFileExportedRecord(exportDir, fileUID); + await fs.rm(`${collectionExportPath}/${imageExportName}`); throw e; } + + await this.addFileExportedRecord( + exportDir, + fileUID, + livePhotoExportName, + ); } private async saveMetadataFile( @@ -1398,17 +1383,19 @@ const moveToTrash = async ( if (await fs.exists(filePath)) { await fs.mkdirIfNeeded(trashDir); - const trashFilePath = await safeFileName(trashDir, fileName, fs.exists); + const trashFileName = await safeFileName(trashDir, fileName, fs.exists); + const trashFilePath = `${trashDir}/${trashFileName}`; await fs.rename(filePath, trashFilePath); } if (await fs.exists(metadataFilePath)) { await fs.mkdirIfNeeded(metadataTrashDir); - const metadataTrashFilePath = await safeFileName( + const metadataTrashFileName = await safeFileName( metadataTrashDir, metadataFileName, fs.exists, ); - await fs.rename(filePath, metadataTrashFilePath); + const metadataTrashFilePath = `${metadataTrashDir}/${metadataTrashFileName}`; + await fs.rename(metadataFilePath, metadataTrashFilePath); } }; diff --git a/web/apps/photos/src/services/upload/types.ts b/web/apps/photos/src/services/upload/types.ts index 05ad332d4a0c7babfc80c3c8d7901bc8ac8ded3f..25e2ab408a0ad252cf64886506c034c6279611e3 100644 --- a/web/apps/photos/src/services/upload/types.ts +++ b/web/apps/photos/src/services/upload/types.ts @@ -1,4 +1,3 @@ -import type { FileAndPath } from "@/next/types/file"; import type { ZipItem } from "@/next/types/ipc"; /** @@ -30,6 +29,17 @@ import type { ZipItem } from "@/next/types/ipc"; */ export type UploadItem = File | FileAndPath | string | ZipItem; +/** + * When we are running in the context of our desktop app, we have access to the + * absolute path of {@link File} objects. This convenience type clubs these two + * bits of information, saving us the need to query the path again and again + * using the {@link getPathForFile} method of {@link Electron}. + */ +export interface FileAndPath { + file: File; + path: string; +} + /** * The of cases of {@link UploadItem} that apply when we're running in the * context of our desktop app. diff --git a/web/apps/photos/src/services/upload/uploadManager.ts b/web/apps/photos/src/services/upload/uploadManager.ts index 99fe6ced39aa5ead20318d5951c6b7d97525beb0..38fd7037bed3075e6270b34a0bda827308925913 100644 --- a/web/apps/photos/src/services/upload/uploadManager.ts +++ b/web/apps/photos/src/services/upload/uploadManager.ts @@ -321,7 +321,6 @@ class UploadManager { >(maxConcurrentUploads); private parsedMetadataJSONMap: Map; private itemsToBeUploaded: ClusteredUploadItem[]; - private remainingItems: ClusteredUploadItem[] = []; private failedItems: ClusteredUploadItem[]; private existingFiles: EnteFile[]; private setFiles: SetFiles; @@ -360,7 +359,6 @@ class UploadManager { private resetState() { this.itemsToBeUploaded = []; - this.remainingItems = []; this.failedItems = []; this.parsedMetadataJSONMap = new Map(); @@ -440,17 +438,13 @@ class UploadManager { await this.uploadMediaItems(clusteredMediaItems); } } catch (e) { - if (e.message === CustomError.UPLOAD_CANCELLED) { - if (isElectron()) { - this.remainingItems = []; - await cancelRemainingUploads(); - } - } else { - log.error("Uploading failed", e); + if (e.message != CustomError.UPLOAD_CANCELLED) { + log.error("Upload failed", e); throw e; } } finally { this.uiService.setUploadStage(UPLOAD_STAGES.FINISH); + void globalThis.electron?.clearPendingUploads(); for (let i = 0; i < maxConcurrentUploads; i++) { this.cryptoWorkers[i]?.terminate(); } @@ -503,15 +497,8 @@ class UploadManager { private async uploadMediaItems(mediaItems: ClusteredUploadItem[]) { this.itemsToBeUploaded = [...this.itemsToBeUploaded, ...mediaItems]; - - if (isElectron()) { - this.remainingItems = [...this.remainingItems, ...mediaItems]; - } - this.uiService.reset(mediaItems.length); - await UploadService.setFileCount(mediaItems.length); - this.uiService.setUploadStage(UPLOAD_STAGES.UPLOADING); const uploadProcesses = []; @@ -584,8 +571,10 @@ class UploadManager { `Uploaded ${uploadableItem.fileName} with result ${uploadResult}`, ); try { + const electron = globalThis.electron; + if (electron) await markUploaded(electron, uploadableItem); + let decryptedFile: EnteFile; - await this.removeFromPendingUploads(uploadableItem); switch (uploadResult) { case UPLOAD_RESULT.FAILED: case UPLOAD_RESULT.BLOCKED: @@ -620,11 +609,25 @@ class UploadManager { ].includes(uploadResult) ) { try { + let file: File | undefined; + const uploadItem = + uploadableItem.uploadItem ?? + uploadableItem.livePhotoAssets.image; + if (uploadItem) { + if (uploadItem instanceof File) { + file = uploadItem; + } else if ( + typeof uploadItem == "string" || + Array.isArray(uploadItem) + ) { + // path from desktop, no file object + } else { + file = uploadItem.file; + } + } eventBus.emit(Events.FILE_UPLOADED, { enteFile: decryptedFile, - localFile: - uploadableItem.uploadItem ?? - uploadableItem.livePhotoAssets.image, + localFile: file, }); } catch (e) { log.warn("Ignoring error in fileUploaded handlers", e); @@ -688,18 +691,6 @@ class UploadManager { this.setFiles((files) => sortFiles([...files, decryptedFile])); } - private async removeFromPendingUploads( - clusteredUploadItem: ClusteredUploadItem, - ) { - const electron = globalThis.electron; - if (electron) { - this.remainingItems = this.remainingItems.filter( - (f) => f.localID != clusteredUploadItem.localID, - ); - await markUploaded(electron, clusteredUploadItem); - } - } - public shouldAllowNewUpload = () => { return !this.uploadInProgress || watcher.isUploadRunning(); }; @@ -847,8 +838,6 @@ const markUploaded = async (electron: Electron, item: ClusteredUploadItem) => { } }; -const cancelRemainingUploads = () => ensureElectron().clearPendingUploads(); - /** * Go through the given files, combining any sibling image + video assets into a * single live photo when appropriate. diff --git a/web/apps/photos/src/services/upload/uploadService.ts b/web/apps/photos/src/services/upload/uploadService.ts index 7d3303884259e50a3eda458e13e08316cb2518a2..52f495785a48579b5fd45033858c31fcfa608bc3 100644 --- a/web/apps/photos/src/services/upload/uploadService.ts +++ b/web/apps/photos/src/services/upload/uploadService.ts @@ -1021,7 +1021,7 @@ const withThumbnail = async ( fileTypeInfo, ); } catch (e) { - if (e.message == CustomErrorMessage.NotAvailable) { + if (e.message.endsWith(CustomErrorMessage.NotAvailable)) { moduleState.isNativeImageThumbnailGenerationNotAvailable = true; } else { log.error("Native thumbnail generation failed", e); diff --git a/web/apps/photos/src/types/billing/index.ts b/web/apps/photos/src/types/billing/index.ts index b2058948bc17b51280e1b2f6aac607da83f9a8ef..ef203d49fe562103a2da3f7b75e670f6dd2abb54 100644 --- a/web/apps/photos/src/types/billing/index.ts +++ b/web/apps/photos/src/types/billing/index.ts @@ -14,6 +14,7 @@ export interface Subscription { price: string; period: PLAN_PERIOD; } + export interface Plan { id: string; androidID: string; diff --git a/web/apps/photos/src/utils/billing/index.ts b/web/apps/photos/src/utils/billing/index.ts index 3dfde5384bb1d0eee95bf948c207d958c6fe2710..d2e593e9e1a26df6abfe632e47740cfe6ef67d5b 100644 --- a/web/apps/photos/src/utils/billing/index.ts +++ b/web/apps/photos/src/utils/billing/index.ts @@ -31,44 +31,6 @@ enum RESPONSE_STATUS { fail = "fail", } -const StorageUnits = ["B", "KB", "MB", "GB", "TB"]; - -const ONE_GB = 1024 * 1024 * 1024; - -export function convertBytesToGBs(bytes: number, precision = 0): string { - return (bytes / (1024 * 1024 * 1024)).toFixed(precision); -} - -export function makeHumanReadableStorage( - bytes: number, - { roundUp } = { roundUp: false }, -): string { - if (bytes <= 0) { - return `0 ${t("STORAGE_UNITS.MB")}`; - } - const i = Math.floor(Math.log(bytes) / Math.log(1024)); - - let quantity = bytes / Math.pow(1024, i); - let unit = StorageUnits[i]; - - if (quantity > 100 && unit !== "GB") { - quantity /= 1024; - unit = StorageUnits[i + 1]; - } - - quantity = Number(quantity.toFixed(1)); - - if (bytes >= 10 * ONE_GB) { - if (roundUp) { - quantity = Math.ceil(quantity); - } else { - quantity = Math.round(quantity); - } - } - - return `${quantity} ${t(`STORAGE_UNITS.${unit}`)}`; -} - export function hasPaidSubscription(subscription: Subscription) { return ( subscription && @@ -160,9 +122,8 @@ export function isSubscriptionPastDue(subscription: Subscription) { ); } -export function isPopularPlan(plan: Plan) { - return plan.storage === 100 * ONE_GB; -} +export const isPopularPlan = (plan: Plan) => + plan.storage === 100 * 1024 * 1024 * 1024; /* 100 GB */ export async function updateSubscription( plan: Plan, diff --git a/web/apps/photos/src/utils/file/index.ts b/web/apps/photos/src/utils/file/index.ts index abbc8b0fa38cc3d0d4180e34dd1ab8a74c2d38d3..98a8dd94813db813801c422041903562e86930e8 100644 --- a/web/apps/photos/src/utils/file/index.ts +++ b/web/apps/photos/src/utils/file/index.ts @@ -1,4 +1,5 @@ import { FILE_TYPE } from "@/media/file-type"; +import { isNonWebImageFileExtension } from "@/media/formats"; import { decodeLivePhoto } from "@/media/live-photo"; import { lowercaseExtension } from "@/next/file"; import log from "@/next/log"; @@ -40,20 +41,6 @@ import { isArchivedFile, updateMagicMetadata } from "utils/magicMetadata"; import { safeFileName } from "utils/native-fs"; import { writeStream } from "utils/native-stream"; -const RAW_FORMATS = [ - "heic", - "rw2", - "tiff", - "arw", - "cr3", - "cr2", - "raf", - "nef", - "psd", - "dng", - "tif", -]; - const SUPPORTED_RAW_FORMATS = [ "heic", "rw2", @@ -116,19 +103,6 @@ export async function getUpdatedEXIFFileForDownload( } } -export function convertBytesToHumanReadable( - bytes: number, - precision = 2, -): string { - if (bytes === 0 || isNaN(bytes)) { - return "0 MB"; - } - - const i = Math.floor(Math.log(bytes) / Math.log(1024)); - const sizes = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]; - return (bytes / Math.pow(1024, i)).toFixed(precision) + " " + sizes[i]; -} - export async function downloadFile(file: EnteFile) { try { const fileReader = new FileReader(); @@ -301,13 +275,14 @@ export const getRenderableImage = async (fileName: string, imageBlob: Blob) => { const tempFile = new File([imageBlob], fileName); const fileTypeInfo = await detectFileTypeInfo(tempFile); log.debug( - () => `Need renderable image for ${JSON.stringify(fileTypeInfo)}`, + () => + `Need renderable image for ${JSON.stringify({ fileName, ...fileTypeInfo })}`, ); const { extension } = fileTypeInfo; - if (!isRawFile(extension)) { - // Either it is not something we know how to handle yet, or - // something that the browser already knows how to render. + if (!isNonWebImageFileExtension(extension)) { + // Either it is something that the browser already knows how to + // render, or something we don't even about yet. return imageBlob; } @@ -318,7 +293,7 @@ export const getRenderableImage = async (fileName: string, imageBlob: Blob) => { try { return await nativeConvertToJPEG(imageBlob); } catch (e) { - if (e.message == CustomErrorMessage.NotAvailable) { + if (e.message.endsWith(CustomErrorMessage.NotAvailable)) { moduleState.isNativeJPEGConversionNotAvailable = true; } else { log.error("Native conversion to JPEG failed", e); @@ -352,10 +327,6 @@ const nativeConvertToJPEG = async (imageBlob: Blob) => { return new Blob([jpegData]); }; -export function isRawFile(exactType: string) { - return RAW_FORMATS.includes(exactType.toLowerCase()); -} - export function isSupportedRawFormat(exactType: string) { return SUPPORTED_RAW_FORMATS.includes(exactType.toLowerCase()); } diff --git a/web/apps/photos/src/utils/machineLearning/config.ts b/web/apps/photos/src/utils/machineLearning/config.ts index 4d2030ca3e89978c60bd7aab6b73b93add53fa66..0c25356abaa982671809de158a609dcbfb1378b7 100644 --- a/web/apps/photos/src/utils/machineLearning/config.ts +++ b/web/apps/photos/src/utils/machineLearning/config.ts @@ -10,6 +10,7 @@ import mlIDbStorage, { ML_SYNC_CONFIG_NAME, ML_SYNC_JOB_CONFIG_NAME, } from "utils/storage/mlIDbStorage"; +import { isInternalUserForML } from "utils/user"; export async function getMLSyncJobConfig() { return mlIDbStorage.getConfig( @@ -23,10 +24,15 @@ export async function getMLSyncConfig() { } export async function getMLSearchConfig() { - return mlIDbStorage.getConfig( - ML_SEARCH_CONFIG_NAME, - DEFAULT_ML_SEARCH_CONFIG, - ); + if (isInternalUserForML()) { + return mlIDbStorage.getConfig( + ML_SEARCH_CONFIG_NAME, + DEFAULT_ML_SEARCH_CONFIG, + ); + } + // Force disabled for everyone else while we finalize it to avoid redundant + // reindexing for users. + return DEFAULT_ML_SEARCH_CONFIG; } export async function updateMLSyncJobConfig(newConfig: JobConfig) { diff --git a/web/apps/photos/src/utils/native-stream.ts b/web/apps/photos/src/utils/native-stream.ts index 8ada6070cd206d8e63fef4d8d0668ec36550666d..4ed9da753a4ba941b6e1f95bcca22c589f70c0ec 100644 --- a/web/apps/photos/src/utils/native-stream.ts +++ b/web/apps/photos/src/utils/native-stream.ts @@ -39,11 +39,12 @@ export const readStream = async ( ): Promise<{ response: Response; size: number; lastModifiedMs: number }> => { let url: URL; if (typeof pathOrZipItem == "string") { - url = new URL(`stream://read${pathOrZipItem}`); + const params = new URLSearchParams({ path: pathOrZipItem }); + url = new URL(`stream://read?${params.toString()}`); } else { const [zipPath, entryName] = pathOrZipItem; - url = new URL(`stream://read${zipPath}`); - url.hash = entryName; + const params = new URLSearchParams({ zipPath, entryName }); + url = new URL(`stream://read-zip?${params.toString()}`); } const req = new Request(url, { method: "GET" }); @@ -89,40 +90,24 @@ export const writeStream = async ( path: string, stream: ReadableStream, ) => { - // TODO(MR): This doesn't currently work. - // - // Not sure what I'm doing wrong here; I've opened an issue upstream - // https://github.com/electron/electron/issues/41872 - // - // A gist with a minimal reproduction - // https://gist.github.com/mnvr/e08d9f4876fb8400b7615347b4d268eb - // - // Meanwhile, write the complete body in one go (this'll eventually run into - // memory failures with large files - just a temporary stopgap to get the - // code to work). + const params = new URLSearchParams({ path }); + const url = new URL(`stream://write?${params.toString()}`); - /* // The duplex parameter needs to be set to 'half' when streaming requests. // // Currently browsers, and specifically in our case, since this code runs // only within our desktop (Electron) app, Chromium, don't support 'full' // duplex mode (i.e. streaming both the request and the response). // https://developer.chrome.com/docs/capabilities/web-apis/fetch-streaming-requests - const req = new Request(`stream://write${path}`, { + const req = new Request(url, { // GET can't have a body method: "POST", body: stream, - // --@ts-expect-error TypeScript's libdom.d.ts does not include the + // @ts-expect-error TypeScript's libdom.d.ts does not include the // "duplex" parameter, e.g. see // https://github.com/node-fetch/node-fetch/issues/1769. duplex: "half", }); - */ - - const req = new Request(`stream://write${path}`, { - method: "POST", - body: await new Response(stream).blob(), - }); const res = await fetch(req); if (!res.ok) diff --git a/web/apps/photos/src/utils/storage/mlIDbStorage.ts b/web/apps/photos/src/utils/storage/mlIDbStorage.ts index 40e6dad66274a0a5489f2c62a58d92785bea8d17..766c3ac9a98ca384653388ff569fed27c260eb07 100644 --- a/web/apps/photos/src/utils/storage/mlIDbStorage.ts +++ b/web/apps/photos/src/utils/storage/mlIDbStorage.ts @@ -144,7 +144,13 @@ class MLIDbStorage { .objectStore("configs") .add(DEFAULT_ML_SEARCH_CONFIG, ML_SEARCH_CONFIG_NAME); } + /* + This'll go in version 5. Note that version 4 was never released, + but it was in main for a while, so we'll just skip it to avoid + breaking the upgrade path for people who ran the mainline. + */ if (oldVersion < 4) { + /* try { await tx .objectStore("configs") @@ -163,8 +169,8 @@ class MLIDbStorage { // the shipped implementation should have a more // deterministic migration. } + */ } - log.info( `ML DB upgraded from version ${oldVersion} to version ${newVersion}`, ); diff --git a/web/apps/photos/src/utils/units.ts b/web/apps/photos/src/utils/units.ts new file mode 100644 index 0000000000000000000000000000000000000000..229ec2ab9d7eff898c7a292a7017832e5d2bc2aa --- /dev/null +++ b/web/apps/photos/src/utils/units.ts @@ -0,0 +1,100 @@ +import { t } from "i18next"; + +/** + * Localized unit keys. + * + * For each of these, there is expected to be a localized key under + * "storage_unit". e.g. "storage_unit.tb". + */ +const units = ["b", "kb", "mb", "gb", "tb"]; + +/** + * Convert the given number of {@link bytes} to their equivalent GB string with + * {@link precision}. + * + * The returned string does not have the GB suffix. + */ +export const bytesInGB = (bytes: number, precision = 0): string => + (bytes / (1024 * 1024 * 1024)).toFixed(precision); + +/** + * Convert the given number of {@link bytes} to a user visible string in an + * appropriately sized unit. + * + * The returned string includes the (localized) unit suffix, e.g. "TB". + * + * @param precision Modify the number of digits after the decimal point. + * Defaults to 2. + */ +export function formattedByteSize(bytes: number, precision = 2): string { + if (bytes <= 0) return `0 ${t("storage_unit.mb")}`; + + const i = Math.min( + Math.floor(Math.log(bytes) / Math.log(1024)), + units.length - 1, + ); + const quantity = bytes / Math.pow(1024, i); + const unit = units[i]; + + return `${quantity.toFixed(precision)} ${t(`storage_unit.${unit}`)}`; +} + +interface FormattedStorageByteSizeOptions { + /** + * If `true` then round up the fractional quantity we obtain when dividing + * the number of bytes by the number of bytes in the unit that got chosen. + * + * The default behaviour is to take the ceiling. + */ + round?: boolean; +} + +/** + * Convert the given number of storage {@link bytes} to a user visible string in + * an appropriately sized unit. + * + * This differs from {@link formattedByteSize} in that while + * {@link formattedByteSize} is meant for arbitrary byte sizes, this function + * has a few additional beautification heuristics that we want to apply when + * displaying the "storage size" (in different contexts) as opposed to, say, a + * generic "file size". + * + * @param options {@link FormattedStorageByteSizeOptions}. + * + * @return A user visible string, including the localized unit suffix. + */ +export const formattedStorageByteSize = ( + bytes: number, + options?: FormattedStorageByteSizeOptions, +): string => { + if (bytes <= 0) return `0 ${t("storage_unit.mb")}`; + + const i = Math.min( + Math.floor(Math.log(bytes) / Math.log(1024)), + units.length - 1, + ); + + let quantity = bytes / Math.pow(1024, i); + let unit = units[i]; + + // Round up bytes, KBs and MBs to the bigger unit whenever they'll come of + // as more than 0.1. + if (quantity > 100 && i < units.length - 2) { + quantity /= 1024; + unit = units[i + 1]; + } + + quantity = Number(quantity.toFixed(1)); + + // Truncate or round storage sizes to trim off unnecessary and potentially + // obscuring precision when they are larger that 10 GB. + if (bytes >= 10 * 1024 * 1024 * 1024 /* 10 GB */) { + if (options?.round) { + quantity = Math.ceil(quantity); + } else { + quantity = Math.round(quantity); + } + } + + return `${quantity} ${t(`storage_unit.${unit}`)}`; +}; diff --git a/web/apps/photos/src/utils/user/index.ts b/web/apps/photos/src/utils/user/index.ts index 17551014d05dbe68db7dc26e83b8c961fbf94124..68ffc9bbd7e9edeea6e20c3f23129ec9a74b5df2 100644 --- a/web/apps/photos/src/utils/user/index.ts +++ b/web/apps/photos/src/utils/user/index.ts @@ -1,4 +1,5 @@ import { getData, LS_KEYS } from "@ente/shared/storage/localStorage"; +import type { User } from "@ente/shared/user/types"; import { UserDetails } from "types/user"; export function getLocalUserDetails(): UserDetails { @@ -9,7 +10,12 @@ export const isInternalUser = () => { const userEmail = getData(LS_KEYS.USER)?.email; if (!userEmail) return false; - return ( - userEmail.endsWith("@ente.io") || userEmail === "kr.anand619@gmail.com" - ); + return userEmail.endsWith("@ente.io"); +}; + +export const isInternalUserForML = () => { + const userId = (getData(LS_KEYS.USER) as User)?.id; + if (userId == 1) return true; + + return isInternalUser(); }; diff --git a/web/apps/staff/src/App.tsx b/web/apps/staff/src/App.tsx index f8984fecbd8b7103d76c8e89f42c9a908760155a..01d79b18cc610ffb97f1e1ffa7895eed2df5e05e 100644 --- a/web/apps/staff/src/App.tsx +++ b/web/apps/staff/src/App.tsx @@ -9,7 +9,7 @@ export const App: React.FC = () => { .then((userDetails) => { console.log("Fetched user details", userDetails); }) - .catch((e) => { + .catch((e: unknown) => { console.error("Failed to fetch user details", e); }); }; diff --git a/web/docs/dependencies.md b/web/docs/dependencies.md index 83c4c16c842f5ef21d9bbd9e56d004191606a657..3e9cb9a2fdee485b47c21b8e6422688397fd63fa 100644 --- a/web/docs/dependencies.md +++ b/web/docs/dependencies.md @@ -133,17 +133,19 @@ some cases. ## Media -- ["jszip"](https://github.com/Stuk/jszip) is used for reading zip files in +- [jszip](https://github.com/Stuk/jszip) is used for reading zip files in JavaScript (Live photos are zip files under the hood). -- ["file-type"](https://github.com/sindresorhus/file-type) is used for MIME - type detection. We are at an old version 16.5.4 because v17 onwards the - package became ESM only - for our limited use case, the custom Webpack - configuration that entails is not worth the upgrade. +- [file-type](https://github.com/sindresorhus/file-type) is used for MIME type + detection. We are at an old version 16.5.4 because v17 onwards the package + became ESM only - for our limited use case, the custom Webpack configuration + that entails is not worth the upgrade. ## Photos app specific -### Misc +- [react-dropzone](https://github.com/react-dropzone/react-dropzone/) is a + React hook to create a drag-and-drop input zone. -- "sanitize-filename" is for converting arbitrary strings into strings that - are suitable for being used as filenames. +- [sanitize-filename](https://github.com/parshap/node-sanitize-filename) is + for converting arbitrary strings into strings that are suitable for being + used as filenames. diff --git a/web/package.json b/web/package.json index 2d5919eb1a935acba500e6212759e24dd04de7c0..647ee3ba3ac18baf6381f3712b8cc203a79a4fd0 100644 --- a/web/package.json +++ b/web/package.json @@ -27,8 +27,8 @@ "dev:payments": "yarn workspace payments dev", "dev:photos": "yarn workspace photos next dev", "dev:staff": "yarn workspace staff dev", - "lint": "yarn prettier --check . && yarn workspaces run eslint --report-unused-disable-directives .", - "lint-fix": "yarn prettier --write . && yarn workspaces run eslint --fix .", + "lint": "yarn prettier --check --log-level warn . && yarn workspaces run eslint --report-unused-disable-directives .", + "lint-fix": "yarn prettier --write --log-level warn . && yarn workspaces run eslint --fix .", "preview": "yarn preview:photos", "preview:accounts": "yarn build:accounts && python3 -m http.server -d apps/accounts/out 3001", "preview:auth": "yarn build:auth && python3 -m http.server -d apps/auth/out 3000", diff --git a/web/packages/accounts/pages/credentials.tsx b/web/packages/accounts/pages/credentials.tsx index 3e8fbabbe69972a34daa48f54603b90e354d2222..36425c142929b6536a228c7e329ad1472c575c00 100644 --- a/web/packages/accounts/pages/credentials.tsx +++ b/web/packages/accounts/pages/credentials.tsx @@ -5,7 +5,6 @@ import { VerticallyCentered } from "@ente/shared/components/Container"; import EnteSpinner from "@ente/shared/components/EnteSpinner"; import FormPaper from "@ente/shared/components/Form/FormPaper"; import FormPaperFooter from "@ente/shared/components/Form/FormPaper/Footer"; -import FormPaperTitle from "@ente/shared/components/Form/FormPaper/Title"; import LinkButton from "@ente/shared/components/LinkButton"; import VerifyMasterPasswordForm, { VerifyMasterPasswordFormProps, @@ -39,6 +38,7 @@ import { setKey, } from "@ente/shared/storage/sessionStorage"; import { KeyAttributes, User } from "@ente/shared/user/types"; +import { Typography, styled } from "@mui/material"; import { t } from "i18next"; import { useRouter } from "next/router"; import { useEffect, useState } from "react"; @@ -259,7 +259,7 @@ export default function Credentials({ appContext, appName }: PageProps) { return ( - {t("PASSWORD")} + {user.email} ); } + +const Title: React.FC = ({ children }) => { + return ( + + {t("PASSWORD")} + {children} + + ); +}; + +const Title_ = styled("div")` + margin-block-end: 4rem; + display: flex; + flex-direction: column; + gap: 8px; +`; diff --git a/web/packages/accounts/services/user.ts b/web/packages/accounts/services/user.ts index fb0e1c929050d56748e5dfff39656d4eb010bbe3..8f6d6609a155bbc6a0aa4429f87a3dc6867fa69b 100644 --- a/web/packages/accounts/services/user.ts +++ b/web/packages/accounts/services/user.ts @@ -40,10 +40,18 @@ export const logoutUser = async () => { } catch (e) { log.error("Ignoring error when clearing files", e); } - try { - globalThis.electron?.clearStores(); - } catch (e) { - log.error("Ignoring error when clearing electron stores", e); + const electron = globalThis.electron; + if (electron) { + try { + await electron.watch.reset(); + } catch (e) { + log.error("Ignoring error when resetting native folder watches", e); + } + try { + await electron.clearStores(); + } catch (e) { + log.error("Ignoring error when clearing native stores", e); + } } try { eventBus.emit(Events.LOGOUT); diff --git a/web/packages/build-config/eslintrc-base.js b/web/packages/build-config/eslintrc-base.js index b302be36d4cba355cfaddaee5a1be7e6c284dbf0..3e65638c1b9dd43f13ed03ef39b8655f531f7964 100644 --- a/web/packages/build-config/eslintrc-base.js +++ b/web/packages/build-config/eslintrc-base.js @@ -10,4 +10,20 @@ module.exports = { parserOptions: { project: true }, parser: "@typescript-eslint/parser", ignorePatterns: [".eslintrc.js"], + 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/web/packages/media/formats.ts b/web/packages/media/formats.ts new file mode 100644 index 0000000000000000000000000000000000000000..24d2c7c877458c4e15d60568b8fde7ae3f50829e --- /dev/null +++ b/web/packages/media/formats.ts @@ -0,0 +1,26 @@ +/** + * Image file extensions that we know the browser is unlikely to have native + * support for. + */ +const nonWebImageFileExtensions = [ + "heic", + "rw2", + "tiff", + "arw", + "cr3", + "cr2", + "raf", + "nef", + "psd", + "dng", + "tif", +]; + +/** + * Return `true` if {@link extension} is from amongst a known set of image file + * extensions that we know that the browser is unlikely to have native support + * for. If we want to display such files in the browser, we'll need to convert + * them to some other format first. + */ +export const isNonWebImageFileExtension = (extension: string) => + nonWebImageFileExtensions.includes(extension.toLowerCase()); diff --git a/web/packages/next/blob-cache.ts b/web/packages/next/blob-cache.ts index 0e092fed61d026b17da23cc8d59212295faa13f3..e6c3734df2448730bedf4245d294834fe1c7cddf 100644 --- a/web/packages/next/blob-cache.ts +++ b/web/packages/next/blob-cache.ts @@ -50,8 +50,6 @@ export type BlobCacheNamespace = (typeof blobCacheNames)[number]; * ([the WebKit bug](https://bugs.webkit.org/show_bug.cgi?id=231706)), so it's * not trivial to use this as a full on replacement of the Web Cache in the * browser. So for now we go with this split implementation. - * - * See also: [Note: Increased disk cache for the desktop app]. */ export interface BlobCache { /** diff --git a/web/packages/next/i18n.ts b/web/packages/next/i18n.ts index 913ecf746ea7fce208b79812d7d84758c851f311..cdc60e27ca480471d0ef8f520f83e2ea165b3384 100644 --- a/web/packages/next/i18n.ts +++ b/web/packages/next/i18n.ts @@ -22,6 +22,7 @@ import { object, string } from "yup"; export const supportedLocales = [ "en-US" /* English */, "fr-FR" /* French */, + "de-DE" /* German */, "zh-CN" /* Simplified Chinese */, "nl-NL" /* Dutch */, "es-ES" /* Spanish */, @@ -209,6 +210,8 @@ const closestSupportedLocale = ( return "en-US"; } else if (ls.startsWith("fr")) { return "fr-FR"; + } else if (ls.startsWith("de")) { + return "de-DE"; } else if (ls.startsWith("zh")) { return "zh-CN"; } else if (ls.startsWith("nl")) { diff --git a/web/packages/next/locales/bg-BG/translation.json b/web/packages/next/locales/bg-BG/translation.json index 28689ba498cad3813976873c41d76977fc3b6115..aa88d9c504012be728da4a7c1cca2934e5dc6e84 100644 --- a/web/packages/next/locales/bg-BG/translation.json +++ b/web/packages/next/locales/bg-BG/translation.json @@ -418,7 +418,6 @@ "ALL_HIDDEN_ALBUMS": "", "HIDDEN_ALBUMS": "", "HIDDEN_ITEMS": "", - "HIDDEN_ITEMS_SECTION_NAME": "", "ENTER_TWO_FACTOR_OTP": "", "CREATE_ACCOUNT": "", "COPIED": "", @@ -448,10 +447,9 @@ "DOWNLOAD_LOGS_MESSAGE": "", "CHANGE_FOLDER": "", "TWO_MONTHS_FREE": "", - "GB": "", "POPULAR": "", "FREE_PLAN_OPTION_LABEL": "", - "FREE_PLAN_DESCRIPTION": "", + "free_plan_description": "", "CURRENT_USAGE": "", "WEAK_DEVICE": "", "DRAG_AND_DROP_HINT": "", @@ -495,12 +493,12 @@ "EXPORT_DIRECTORY_DOES_NOT_EXIST": "", "EXPORT_DIRECTORY_DOES_NOT_EXIST_MESSAGE": "", "SUBSCRIPTION_VERIFICATION_ERROR": "", - "STORAGE_UNITS": { - "B": "", - "KB": "", - "MB": "", - "GB": "", - "TB": "" + "storage_unit": { + "b": "", + "kb": "", + "mb": "", + "gb": "", + "tb": "" }, "AFTER_TIME": { "HOUR": "", @@ -599,10 +597,10 @@ "PAIR_DEVICE_TO_TV": "", "TV_NOT_FOUND": "", "AUTO_CAST_PAIR": "", - "AUTO_CAST_PAIR_REQUIRES_CONNECTION_TO_GOOGLE": "", + "AUTO_CAST_PAIR_DESC": "", "PAIR_WITH_PIN": "", "CHOOSE_DEVICE_FROM_BROWSER": "", - "PAIR_WITH_PIN_WORKS_FOR_ANY_LARGE_SCREEN_DEVICE": "", + "PAIR_WITH_PIN_DESC": "", "VISIT_CAST_ENTE_IO": "", "CAST_AUTO_PAIR_FAILED": "", "FREEHAND": "", diff --git a/web/packages/next/locales/de-DE/translation.json b/web/packages/next/locales/de-DE/translation.json index a0ee15a7c6e1ccb2c1bbbe60ae48a2513c75e429..8bfddc1cdc58517fc23ab67beb31f8795b092ae7 100644 --- a/web/packages/next/locales/de-DE/translation.json +++ b/web/packages/next/locales/de-DE/translation.json @@ -340,11 +340,11 @@ "UPDATE_CREATION_TIME_COMPLETED": "Alle Dateien erfolgreich aktualisiert", "UPDATE_CREATION_TIME_COMPLETED_WITH_ERROR": "Aktualisierung der Dateizeit für einige Dateien fehlgeschlagen, bitte versuche es erneut", "CAPTION_CHARACTER_LIMIT": "Maximal 5000 Zeichen", - "DATE_TIME_ORIGINAL": "", - "DATE_TIME_DIGITIZED": "", - "METADATA_DATE": "", + "DATE_TIME_ORIGINAL": "EXIF:DateTimeOriginal", + "DATE_TIME_DIGITIZED": "EXIF:DateTimeDigitized", + "METADATA_DATE": "EXIF:MetadataDate", "CUSTOM_TIME": "Benutzerdefinierte Zeit", - "REOPEN_PLAN_SELECTOR_MODAL": "", + "REOPEN_PLAN_SELECTOR_MODAL": "Aboauswahl erneut öffnen", "OPEN_PLAN_SELECTOR_MODAL_FAILED": "Fehler beim Öffnen der Pläne", "INSTALL": "Installieren", "SHARING_DETAILS": "Details teilen", @@ -374,7 +374,7 @@ "ADD_MORE": "Mehr hinzufügen", "VIEWERS": "Zuschauer", "OR_ADD_EXISTING": "Oder eine Vorherige auswählen", - "REMOVE_PARTICIPANT_MESSAGE": "", + "REMOVE_PARTICIPANT_MESSAGE": "

{{selectedEmail}} wird vom Album entfernt

Alle Bilder von {{selectedEmail}} werden ebenfalls aus dem Album entfernt

", "NOT_FOUND": "404 - Nicht gefunden", "LINK_EXPIRED": "Link ist abgelaufen", "LINK_EXPIRED_MESSAGE": "Dieser Link ist abgelaufen oder wurde deaktiviert!", @@ -388,9 +388,9 @@ "LINK_EXPIRY": "Ablaufdatum des Links", "NEVER": "Niemals", "DISABLE_FILE_DOWNLOAD": "Download deaktivieren", - "DISABLE_FILE_DOWNLOAD_MESSAGE": "", + "DISABLE_FILE_DOWNLOAD_MESSAGE": "

Bist du sicher, dass du den Downloadbutton für Dateien deaktivieren möchtest?

Betrachter können weiterhin Screenshots machen oder die Bilder mithilfe externer Werkzeuge speichern

", "SHARED_USING": "Freigegeben über ", - "SHARING_REFERRAL_CODE": "", + "SHARING_REFERRAL_CODE": "Benutze den code {{referralCode}} für 10GB extra", "LIVE": "LIVE", "DISABLE_PASSWORD": "Passwort-Sperre deaktivieren", "DISABLE_PASSWORD_MESSAGE": "Sind Sie sicher, dass Sie die Passwort-Sperre deaktivieren möchten?", @@ -400,12 +400,12 @@ "UPLOAD_FILES": "Datei", "UPLOAD_DIRS": "Ordner", "UPLOAD_GOOGLE_TAKEOUT": "Google Takeout", - "DEDUPLICATE_FILES": "", + "DEDUPLICATE_FILES": "Duplikate bereinigen", "NO_DUPLICATES_FOUND": "Du hast keine Duplikate, die gelöscht werden können", "FILES": "dateien", - "EACH": "", - "DEDUPLICATE_BASED_ON_SIZE": "", - "STOP_ALL_UPLOADS_MESSAGE": "", + "EACH": "pro Datei", + "DEDUPLICATE_BASED_ON_SIZE": "Die folgenden Dateien wurden aufgrund ihrer Größe zusammengefasst. Bitte prüfe und lösche Dateien, die du für duplikate hälst", + "STOP_ALL_UPLOADS_MESSAGE": "Bist du sicher, dass du alle laufenden Uploads abbrechen möchtest?", "STOP_UPLOADS_HEADER": "Hochladen stoppen?", "YES_STOP_UPLOADS": "Ja, Hochladen stoppen", "STOP_DOWNLOADS_HEADER": "Downloads anhalten?", @@ -415,14 +415,13 @@ "albums_other": "{{count, number}} Alben", "ALL_ALBUMS": "Alle Alben", "ALBUMS": "Alben", - "ALL_HIDDEN_ALBUMS": "", - "HIDDEN_ALBUMS": "", - "HIDDEN_ITEMS": "", - "HIDDEN_ITEMS_SECTION_NAME": "", + "ALL_HIDDEN_ALBUMS": "Alle versteckten Alben", + "HIDDEN_ALBUMS": "Versteckte Alben", + "HIDDEN_ITEMS": "Versteckte Dateien", "ENTER_TWO_FACTOR_OTP": "Gib den 6-stelligen Code aus\ndeiner Authentifizierungs-App ein.", "CREATE_ACCOUNT": "Account erstellen", "COPIED": "Kopiert", - "WATCH_FOLDERS": "", + "WATCH_FOLDERS": "Überwachte Ordner", "UPGRADE_NOW": "Jetzt upgraden", "RENEW_NOW": "Jetzt erneuern", "STORAGE": "Speicher", @@ -431,34 +430,33 @@ "FAMILY": "Familie", "FREE": "frei", "OF": "von", - "WATCHED_FOLDERS": "", + "WATCHED_FOLDERS": "Überwachte Ordner", "NO_FOLDERS_ADDED": "Noch keine Ordner hinzugefügt!", - "FOLDERS_AUTOMATICALLY_MONITORED": "", - "UPLOAD_NEW_FILES_TO_ENTE": "", + "FOLDERS_AUTOMATICALLY_MONITORED": "Die Ordner, die du hier hinzufügst, werden überwacht, um automatisch", + "UPLOAD_NEW_FILES_TO_ENTE": "Neue Dateien bei Ente zu sichern", "REMOVE_DELETED_FILES_FROM_ENTE": "Gelöschte Dateien aus Ente entfernen", "ADD_FOLDER": "Ordner hinzufügen", - "STOP_WATCHING": "", - "STOP_WATCHING_FOLDER": "", - "STOP_WATCHING_DIALOG_MESSAGE": "", + "STOP_WATCHING": "Nicht mehr überwachen", + "STOP_WATCHING_FOLDER": "Ordner nicht mehr überwachen?", + "STOP_WATCHING_DIALOG_MESSAGE": "Deine bestehenden Dateien werden nicht gelöscht, aber das verknüpfte Ente-Album wird bei Änderungen in diesem Ordner nicht mehr aktualisiert.", "YES_STOP": "Ja, Stopp", - "MONTH_SHORT": "", + "MONTH_SHORT": "M", "YEAR": "Jahr", "FAMILY_PLAN": "Familientarif", "DOWNLOAD_LOGS": "Logs herunterladen", - "DOWNLOAD_LOGS_MESSAGE": "", + "DOWNLOAD_LOGS_MESSAGE": "

Hier kannst du Debug-Logs herunterladen, die du uns zur Fehleranalyse zusenden kannst.

Beachte bitte, dass die Logs Dateinamen enthalten, um Probleme mit bestimmten Dateien nachvollziehen zu können.

", "CHANGE_FOLDER": "Ordner ändern", "TWO_MONTHS_FREE": "Erhalte 2 Monate kostenlos bei Jahresabonnements", - "GB": "GB", "POPULAR": "Beliebt", "FREE_PLAN_OPTION_LABEL": "Mit kostenloser Testversion fortfahren", - "FREE_PLAN_DESCRIPTION": "1 GB für 1 Jahr", + "free_plan_description": "{{storage}} für 1 Jahr", "CURRENT_USAGE": "Aktuelle Nutzung ist {{usage}}", - "WEAK_DEVICE": "", - "DRAG_AND_DROP_HINT": "", + "WEAK_DEVICE": "Dein Browser ist nicht leistungsstark genug, um deine Bilder zu verschlüsseln. Versuche, dich an einem Computer bei Ente anzumelden, oder lade dir die Ente-App für dein Gerät (Handy oder Desktop) herunter.", + "DRAG_AND_DROP_HINT": "Oder ziehe Dateien per Drag-and-Drop in das Ente-Fenster", "CONFIRM_ACCOUNT_DELETION_MESSAGE": "Deine hochgeladenen Daten werden zur Löschung vorgemerkt und dein Konto wird endgültig gelöscht.

Dieser Vorgang kann nicht rückgängig gemacht werden.", "AUTHENTICATE": "Authentifizieren", - "UPLOADED_TO_SINGLE_COLLECTION": "", - "UPLOADED_TO_SEPARATE_COLLECTIONS": "", + "UPLOADED_TO_SINGLE_COLLECTION": "In einzelnes Album hochgeladen", + "UPLOADED_TO_SEPARATE_COLLECTIONS": "In separate Alben hochgeladen", "NEVERMIND": "Egal", "UPDATE_AVAILABLE": "Neue Version verfügbar", "UPDATE_INSTALLABLE_MESSAGE": "Eine neue Version von Ente ist für die Installation bereit.", @@ -471,10 +469,10 @@ "YESTERDAY": "Gestern", "NAME_PLACEHOLDER": "Name...", "ROOT_LEVEL_FILE_WITH_FOLDER_NOT_ALLOWED": "Alben können nicht aus Datei/Ordnermix erstellt werden", - "ROOT_LEVEL_FILE_WITH_FOLDER_NOT_ALLOWED_MESSAGE": "", + "ROOT_LEVEL_FILE_WITH_FOLDER_NOT_ALLOWED_MESSAGE": "

Du hast sowohl Dateien als auch Ordner in das Ente-Fenster gezogen.

Bitte wähle entweder nur Dateien oder nur Ordner aus, wenn separate Alben erstellt werden sollen

", "CHOSE_THEME": "Design auswählen", "ML_SEARCH": "Gesichtserkennung", - "ENABLE_ML_SEARCH_DESCRIPTION": "", + "ENABLE_ML_SEARCH_DESCRIPTION": "

Hiermit wird on-device machine learning aktiviert, und die Gesichtserkennung beginnt damit, die Fotos auf deinem Gerät zu analysieren.

Beim ersten Durchlauf nach der Anmeldung oder Aktivierung der Funktion werden alle Bilder auf dein Gerät heruntergeladen, um analysiert zu werden. Bitte aktiviere diese Funktion nur, wenn du einverstanden bist, dass dein Gerät die dafür benötigte Bandbreite und Rechenleistung aufbringt.

Falls dies das erste Mal ist, dass du diese Funktion aktivierst, werden wir deine Erlaubnis zur Verarbeitung von Gesichtsdaten einholen.

", "ML_MORE_DETAILS": "Weitere Details", "ENABLE_FACE_SEARCH": "Gesichtserkennung aktivieren", "ENABLE_FACE_SEARCH_TITLE": "Gesichtserkennung aktivieren?", @@ -482,25 +480,25 @@ "DISABLE_BETA": "Beta deaktivieren", "DISABLE_FACE_SEARCH": "Gesichtserkennung deaktivieren", "DISABLE_FACE_SEARCH_TITLE": "Gesichtserkennung deaktivieren?", - "DISABLE_FACE_SEARCH_DESCRIPTION": "", + "DISABLE_FACE_SEARCH_DESCRIPTION": "

Ente wird aufhören, Gesichtsdaten zu verarbeiten.

Du kannst die Gesichtserkennung jederzeit wieder aktivieren, wenn du möchtest, daher ist dieser Vorgang risikofrei.

", "ADVANCED": "Erweitert", "FACE_SEARCH_CONFIRMATION": "Ich verstehe und möchte Ente erlauben, Gesichtsgeometrie zu verarbeiten", "LABS": "Experimente", - "YOURS": "", + "YOURS": "von dir", "PASSPHRASE_STRENGTH_WEAK": "Passwortstärke: Schwach", "PASSPHRASE_STRENGTH_MODERATE": "Passwortstärke: Moderat", "PASSPHRASE_STRENGTH_STRONG": "Passwortstärke: Stark", "PREFERENCES": "Einstellungen", "LANGUAGE": "Sprache", "EXPORT_DIRECTORY_DOES_NOT_EXIST": "Ungültiges Exportverzeichnis", - "EXPORT_DIRECTORY_DOES_NOT_EXIST_MESSAGE": "", + "EXPORT_DIRECTORY_DOES_NOT_EXIST_MESSAGE": "

Das von dir gewählte Exportverzeichnis existiert nicht.

Bitte wähle einen gültigen Ordner.

", "SUBSCRIPTION_VERIFICATION_ERROR": "Verifizierung des Abonnements fehlgeschlagen", - "STORAGE_UNITS": { - "B": "B", - "KB": "KB", - "MB": "MB", - "GB": "GB", - "TB": "TB" + "storage_unit": { + "b": "B", + "kb": "KB", + "mb": "MB", + "gb": "GB", + "tb": "TB" }, "AFTER_TIME": { "HOUR": "nach einer Stunde", @@ -516,39 +514,39 @@ "CREATE_PUBLIC_SHARING": "Öffentlichen Link erstellen", "PUBLIC_LINK_CREATED": "Öffentlicher Link erstellt", "PUBLIC_LINK_ENABLED": "Öffentlicher Link aktiviert", - "COLLECT_PHOTOS": "", - "PUBLIC_COLLECT_SUBTEXT": "", + "COLLECT_PHOTOS": "Bilder sammeln", + "PUBLIC_COLLECT_SUBTEXT": "Erlaube Personen mit diesem Link, Fotos zum gemeinsamen Album hinzuzufügen.", "STOP_EXPORT": "Stop", - "EXPORT_PROGRESS": "", + "EXPORT_PROGRESS": "{{progress.success, number}} / {{progress.total, number}} Dateien synchronisiert", "MIGRATING_EXPORT": "Vorbereiten...", "RENAMING_COLLECTION_FOLDERS": "Albumordner umbenennen...", - "TRASHING_DELETED_FILES": "", - "TRASHING_DELETED_COLLECTIONS": "", - "CONTINUOUS_EXPORT": "", - "PENDING_ITEMS": "", - "EXPORT_STARTING": "", - "DELETE_ACCOUNT_REASON_LABEL": "", - "DELETE_ACCOUNT_REASON_PLACEHOLDER": "", + "TRASHING_DELETED_FILES": "Verschiebe gelöschte Dateien in den Trash-Ordner...", + "TRASHING_DELETED_COLLECTIONS": "Verschiebe gelöschte Alben in den Trash-Ordner...", + "CONTINUOUS_EXPORT": "Stets aktuell halten", + "PENDING_ITEMS": "Ausstehende Dateien", + "EXPORT_STARTING": "Starte Export...", + "DELETE_ACCOUNT_REASON_LABEL": "Was ist der Hauptgrund für die Löschung deines Kontos?", + "DELETE_ACCOUNT_REASON_PLACEHOLDER": "Wähle einen Grund aus", "DELETE_REASON": { - "MISSING_FEATURE": "", - "BROKEN_BEHAVIOR": "", - "FOUND_ANOTHER_SERVICE": "", - "NOT_LISTED": "" + "MISSING_FEATURE": "Es fehlt eine wichtige Funktion die ich benötige", + "BROKEN_BEHAVIOR": "Die App oder eine bestimmte Funktion verhält sich nicht so wie gedacht", + "FOUND_ANOTHER_SERVICE": "Ich habe einen anderen Dienst gefunden, der mir mehr zusagt", + "NOT_LISTED": "Mein Grund ist nicht aufgeführt" }, - "DELETE_ACCOUNT_FEEDBACK_LABEL": "", + "DELETE_ACCOUNT_FEEDBACK_LABEL": "Wir bedauern sehr, dass uns verlässt. Bitte hilf uns besser zu werden, indem du uns sagst warum du gehst.", "DELETE_ACCOUNT_FEEDBACK_PLACEHOLDER": "Feedback", "CONFIRM_DELETE_ACCOUNT_CHECKBOX_LABEL": "Ja, ich möchte dieses Konto und alle enthaltenen Daten endgültig und unwiderruflich löschen", "CONFIRM_DELETE_ACCOUNT": "Kontolöschung bestätigen", - "FEEDBACK_REQUIRED": "", + "FEEDBACK_REQUIRED": "Bitte hilf uns durch das Angeben dieser Daten", "FEEDBACK_REQUIRED_FOUND_ANOTHER_SERVICE": "Was macht der andere Dienst besser?", "RECOVER_TWO_FACTOR": "Zwei-Faktor wiederherstellen", - "at": "", + "at": "um", "AUTH_NEXT": "Weiter", - "AUTH_DOWNLOAD_MOBILE_APP": "", + "AUTH_DOWNLOAD_MOBILE_APP": "Lade unsere smartphone App herunter, um deine Schlüssel zu verwalten", "HIDDEN": "Versteckt", "HIDE": "Ausblenden", "UNHIDE": "Einblenden", - "UNHIDE_TO_COLLECTION": "", + "UNHIDE_TO_COLLECTION": "In Album wieder sichtbar machen", "SORT_BY": "Sortieren nach", "NEWEST_FIRST": "Neueste zuerst", "OLDEST_FIRST": "Älteste zuerst", @@ -562,14 +560,14 @@ "DOWNLOAD_PROGRESS": "{{progress.current}} / {{progress.total}} Dateien", "CHRISTMAS": "Weihnachten", "CHRISTMAS_EVE": "Heiligabend", - "NEW_YEAR": "", - "NEW_YEAR_EVE": "", + "NEW_YEAR": "Neujahr", + "NEW_YEAR_EVE": "Silvester", "IMAGE": "Bild", "VIDEO": "Video", "LIVE_PHOTO": "Live-Foto", "CONVERT": "Konvertieren", - "CONFIRM_EDITOR_CLOSE_MESSAGE": "", - "CONFIRM_EDITOR_CLOSE_DESCRIPTION": "", + "CONFIRM_EDITOR_CLOSE_MESSAGE": "Editor wirklich schließen?", + "CONFIRM_EDITOR_CLOSE_DESCRIPTION": "Lade dein bearbeitetes Bild herunter oder speichere es in Ente, um die Änderungen nicht zu verlieren.", "BRIGHTNESS": "Helligkeit", "CONTRAST": "Kontrast", "SATURATION": "Sättigung", @@ -581,7 +579,7 @@ "ROTATE_RIGHT": "Nach rechts drehen", "FLIP_VERTICALLY": "Vertikal spiegeln", "FLIP_HORIZONTALLY": "Horizontal spiegeln", - "DOWNLOAD_EDITED": "", + "DOWNLOAD_EDITED": "Bearbeitetes Bild herunterladen", "SAVE_A_COPY_TO_ENTE": "Kopie in Ente speichern", "RESTORE_ORIGINAL": "Original wiederherstellen", "TRANSFORM": "Transformieren", @@ -590,24 +588,24 @@ "ROTATION": "Drehen", "RESET": "Zurücksetzen", "PHOTO_EDITOR": "Foto-Editor", - "FASTER_UPLOAD": "", - "FASTER_UPLOAD_DESCRIPTION": "", - "MAGIC_SEARCH_STATUS": "", + "FASTER_UPLOAD": "Schnelleres hochladen", + "FASTER_UPLOAD_DESCRIPTION": "Uploads über nahegelegene Server leiten", + "MAGIC_SEARCH_STATUS": "Status der magischen Suche", "INDEXED_ITEMS": "Indizierte Elemente", "CAST_ALBUM_TO_TV": "Album auf Fernseher wiedergeben", "ENTER_CAST_PIN_CODE": "Gib den Code auf dem Fernseher unten ein, um dieses Gerät zu koppeln.", "PAIR_DEVICE_TO_TV": "Geräte koppeln", "TV_NOT_FOUND": "Fernseher nicht gefunden. Hast du die PIN korrekt eingegeben?", - "AUTO_CAST_PAIR": "", - "AUTO_CAST_PAIR_REQUIRES_CONNECTION_TO_GOOGLE": "", - "PAIR_WITH_PIN": "", - "CHOOSE_DEVICE_FROM_BROWSER": "", - "PAIR_WITH_PIN_WORKS_FOR_ANY_LARGE_SCREEN_DEVICE": "", - "VISIT_CAST_ENTE_IO": "", - "CAST_AUTO_PAIR_FAILED": "", + "AUTO_CAST_PAIR": "Automatisch verbinden", + "AUTO_CAST_PAIR_DESC": "Automatisches Verbinden funktioniert nur mit Geräten, die Chromecast unterstützen.", + "PAIR_WITH_PIN": "Mit PIN verbinden", + "CHOOSE_DEVICE_FROM_BROWSER": "Wähle ein Cast-Gerät aus dem Browser-Popup aus.", + "PAIR_WITH_PIN_DESC": "\"Mit PIN verbinden\" funktioniert mit jedem Bildschirm, auf dem du dein Album sehen möchtest.", + "VISIT_CAST_ENTE_IO": "Besuche {{url}} auf dem Gerät, das du verbinden möchtest.", + "CAST_AUTO_PAIR_FAILED": "Das automatische Verbinden über Chromecast ist fehlgeschlagen. Bitte versuche es erneut.", "FREEHAND": "Freihand", "APPLY_CROP": "Zuschnitt anwenden", - "PHOTO_EDIT_REQUIRED_TO_SAVE": "", + "PHOTO_EDIT_REQUIRED_TO_SAVE": "Es muss mindestens eine Transformation oder Farbanpassung vorgenommen werden, bevor gespeichert werden kann.", "PASSKEYS": "Passkeys", "DELETE_PASSKEY": "Passkey löschen", "DELETE_PASSKEY_CONFIRMATION": "Bist du sicher, dass du diesen Passkey löschen willst? Dieser Vorgang ist nicht umkehrbar.", @@ -622,6 +620,6 @@ "TRY_AGAIN": "Erneut versuchen", "PASSKEY_FOLLOW_THE_STEPS_FROM_YOUR_BROWSER": "Folge den Schritten in deinem Browser, um mit dem Anmelden fortzufahren.", "LOGIN_WITH_PASSKEY": "Mit Passkey anmelden", - "autogenerated_first_album_name": "", - "autogenerated_default_album_name": "" + "autogenerated_first_album_name": "Mein erstes Album", + "autogenerated_default_album_name": "Neues Album" } diff --git a/web/packages/next/locales/en-US/translation.json b/web/packages/next/locales/en-US/translation.json index b3debe5aa0730b264fe58208d6e3f62444887b10..f4d6f610018a0305a24baa3420c47dc8210925a7 100644 --- a/web/packages/next/locales/en-US/translation.json +++ b/web/packages/next/locales/en-US/translation.json @@ -418,7 +418,6 @@ "ALL_HIDDEN_ALBUMS": "All hidden albums", "HIDDEN_ALBUMS": "Hidden albums", "HIDDEN_ITEMS": "Hidden items", - "HIDDEN_ITEMS_SECTION_NAME": "Hidden_items", "ENTER_TWO_FACTOR_OTP": "Enter the 6-digit code from your authenticator app.", "CREATE_ACCOUNT": "Create account", "COPIED": "Copied", @@ -448,10 +447,9 @@ "DOWNLOAD_LOGS_MESSAGE": "

This will download debug logs, which you can email to us to help debug your issue.

Please note that file names will be included to help track issues with specific files.

", "CHANGE_FOLDER": "Change Folder", "TWO_MONTHS_FREE": "Get 2 months free on yearly plans", - "GB": "GB", "POPULAR": "Popular", "FREE_PLAN_OPTION_LABEL": "Continue with free trial", - "FREE_PLAN_DESCRIPTION": "1 GB for 1 year", + "free_plan_description": "{{storage}} for 1 year", "CURRENT_USAGE": "Current usage is {{usage}}", "WEAK_DEVICE": "The web browser you're using is not powerful enough to encrypt your photos. Please try to log in to Ente on your computer, or download the Ente mobile/desktop app.", "DRAG_AND_DROP_HINT": "Or drag and drop into the Ente window", @@ -495,12 +493,12 @@ "EXPORT_DIRECTORY_DOES_NOT_EXIST": "Invalid export directory", "EXPORT_DIRECTORY_DOES_NOT_EXIST_MESSAGE": "

The export directory you have selected does not exist.

Please select a valid directory.

", "SUBSCRIPTION_VERIFICATION_ERROR": "Subscription verification failed", - "STORAGE_UNITS": { - "B": "B", - "KB": "KB", - "MB": "MB", - "GB": "GB", - "TB": "TB" + "storage_unit": { + "b": "B", + "kb": "KB", + "mb": "MB", + "gb": "GB", + "tb": "TB" }, "AFTER_TIME": { "HOUR": "after an hour", @@ -598,13 +596,13 @@ "ENTER_CAST_PIN_CODE": "Enter the code you see on the TV below to pair this device.", "PAIR_DEVICE_TO_TV": "Pair devices", "TV_NOT_FOUND": "TV not found. Did you enter the PIN correctly?", - "AUTO_CAST_PAIR": "Auto Pair", - "AUTO_CAST_PAIR_REQUIRES_CONNECTION_TO_GOOGLE": "Auto Pair requires connecting to Google servers and only works with Chromecast supported devices. Google will not receive sensitive data, such as your photos.", + "AUTO_CAST_PAIR": "Auto pair", + "AUTO_CAST_PAIR_DESC": "Auto pair works only with devices that support Chromecast.", "PAIR_WITH_PIN": "Pair with PIN", "CHOOSE_DEVICE_FROM_BROWSER": "Choose a cast-compatible device from the browser popup.", - "PAIR_WITH_PIN_WORKS_FOR_ANY_LARGE_SCREEN_DEVICE": "Pair with PIN works for any large screen device you want to play your album on.", + "PAIR_WITH_PIN_DESC": "Pair with PIN works with any screen you wish to view your album on.", "VISIT_CAST_ENTE_IO": "Visit {{url}} on the device you want to pair.", - "CAST_AUTO_PAIR_FAILED": "Chromecast Auto Pair failed. Please try again.", + "CAST_AUTO_PAIR_FAILED": "Chromecast auto pair failed. Please try again.", "FREEHAND": "Freehand", "APPLY_CROP": "Apply Crop", "PHOTO_EDIT_REQUIRED_TO_SAVE": "At least one transformation or color adjustment must be performed before saving.", diff --git a/web/packages/next/locales/es-ES/translation.json b/web/packages/next/locales/es-ES/translation.json index a01d322b74d82fcb7b1a24164671a4579ccb17fd..ffc06ffa3f39c98d60c0913409329ee596fa9c9f 100644 --- a/web/packages/next/locales/es-ES/translation.json +++ b/web/packages/next/locales/es-ES/translation.json @@ -418,7 +418,6 @@ "ALL_HIDDEN_ALBUMS": "", "HIDDEN_ALBUMS": "", "HIDDEN_ITEMS": "", - "HIDDEN_ITEMS_SECTION_NAME": "", "ENTER_TWO_FACTOR_OTP": "Ingrese el código de seis dígitos de su aplicación de autenticación a continuación.", "CREATE_ACCOUNT": "Crear cuenta", "COPIED": "Copiado", @@ -448,10 +447,9 @@ "DOWNLOAD_LOGS_MESSAGE": "

Esto descargará los registros de depuración, que puede enviarnos por correo electrónico para ayudarnos a depurar su problema.

Tenga en cuenta que los nombres de los archivos se incluirán para ayudar al seguimiento de problemas con archivos específicos.

", "CHANGE_FOLDER": "Cambiar carpeta", "TWO_MONTHS_FREE": "Obtén 2 meses gratis en planes anuales", - "GB": "GB", "POPULAR": "Popular", "FREE_PLAN_OPTION_LABEL": "Continuar con el plan gratuito", - "FREE_PLAN_DESCRIPTION": "1 GB por 1 año", + "free_plan_description": "{{storage}} por 1 año", "CURRENT_USAGE": "El uso actual es {{usage}}", "WEAK_DEVICE": "El navegador web que está utilizando no es lo suficientemente poderoso para cifrar sus fotos. Por favor, intente iniciar sesión en ente en su computadora, o descargue la aplicación ente para móvil/escritorio.", "DRAG_AND_DROP_HINT": "O arrastre y suelte en la ventana ente", @@ -495,12 +493,12 @@ "EXPORT_DIRECTORY_DOES_NOT_EXIST": "Archivo de exportación inválido", "EXPORT_DIRECTORY_DOES_NOT_EXIST_MESSAGE": "

El directorio de exportación seleccionado no existe.

Por favor, seleccione un directorio válido.

", "SUBSCRIPTION_VERIFICATION_ERROR": "Falló la verificación de la suscripción", - "STORAGE_UNITS": { - "B": "B", - "KB": "KB", - "MB": "MB", - "GB": "GB", - "TB": "TB" + "storage_unit": { + "b": "B", + "kb": "KB", + "mb": "MB", + "gb": "GB", + "tb": "TB" }, "AFTER_TIME": { "HOUR": "después de una hora", @@ -599,10 +597,10 @@ "PAIR_DEVICE_TO_TV": "", "TV_NOT_FOUND": "", "AUTO_CAST_PAIR": "", - "AUTO_CAST_PAIR_REQUIRES_CONNECTION_TO_GOOGLE": "", + "AUTO_CAST_PAIR_DESC": "", "PAIR_WITH_PIN": "", "CHOOSE_DEVICE_FROM_BROWSER": "", - "PAIR_WITH_PIN_WORKS_FOR_ANY_LARGE_SCREEN_DEVICE": "", + "PAIR_WITH_PIN_DESC": "", "VISIT_CAST_ENTE_IO": "", "CAST_AUTO_PAIR_FAILED": "", "FREEHAND": "", diff --git a/web/packages/next/locales/fa-IR/translation.json b/web/packages/next/locales/fa-IR/translation.json index 0c3749d135e8446fe070c225a8d6a62b6d5ca597..2f960501994fecd3741fab46bc744e75f6e3c36f 100644 --- a/web/packages/next/locales/fa-IR/translation.json +++ b/web/packages/next/locales/fa-IR/translation.json @@ -418,7 +418,6 @@ "ALL_HIDDEN_ALBUMS": "", "HIDDEN_ALBUMS": "", "HIDDEN_ITEMS": "", - "HIDDEN_ITEMS_SECTION_NAME": "", "ENTER_TWO_FACTOR_OTP": "", "CREATE_ACCOUNT": "", "COPIED": "", @@ -448,10 +447,9 @@ "DOWNLOAD_LOGS_MESSAGE": "", "CHANGE_FOLDER": "", "TWO_MONTHS_FREE": "", - "GB": "", "POPULAR": "", "FREE_PLAN_OPTION_LABEL": "", - "FREE_PLAN_DESCRIPTION": "", + "free_plan_description": "", "CURRENT_USAGE": "", "WEAK_DEVICE": "", "DRAG_AND_DROP_HINT": "", @@ -495,12 +493,12 @@ "EXPORT_DIRECTORY_DOES_NOT_EXIST": "", "EXPORT_DIRECTORY_DOES_NOT_EXIST_MESSAGE": "", "SUBSCRIPTION_VERIFICATION_ERROR": "", - "STORAGE_UNITS": { - "B": "", - "KB": "", - "MB": "", - "GB": "", - "TB": "" + "storage_unit": { + "b": "", + "kb": "", + "mb": "", + "gb": "", + "tb": "" }, "AFTER_TIME": { "HOUR": "", @@ -599,10 +597,10 @@ "PAIR_DEVICE_TO_TV": "", "TV_NOT_FOUND": "", "AUTO_CAST_PAIR": "", - "AUTO_CAST_PAIR_REQUIRES_CONNECTION_TO_GOOGLE": "", + "AUTO_CAST_PAIR_DESC": "", "PAIR_WITH_PIN": "", "CHOOSE_DEVICE_FROM_BROWSER": "", - "PAIR_WITH_PIN_WORKS_FOR_ANY_LARGE_SCREEN_DEVICE": "", + "PAIR_WITH_PIN_DESC": "", "VISIT_CAST_ENTE_IO": "", "CAST_AUTO_PAIR_FAILED": "", "FREEHAND": "", diff --git a/web/packages/next/locales/fi-FI/translation.json b/web/packages/next/locales/fi-FI/translation.json index d945fcde320d6fdf4f9f00288963f3a504786f1c..33306389c9ba2916a1ffce90b0dd1d79da867018 100644 --- a/web/packages/next/locales/fi-FI/translation.json +++ b/web/packages/next/locales/fi-FI/translation.json @@ -418,7 +418,6 @@ "ALL_HIDDEN_ALBUMS": "", "HIDDEN_ALBUMS": "", "HIDDEN_ITEMS": "", - "HIDDEN_ITEMS_SECTION_NAME": "", "ENTER_TWO_FACTOR_OTP": "", "CREATE_ACCOUNT": "", "COPIED": "", @@ -448,10 +447,9 @@ "DOWNLOAD_LOGS_MESSAGE": "", "CHANGE_FOLDER": "", "TWO_MONTHS_FREE": "", - "GB": "", "POPULAR": "", "FREE_PLAN_OPTION_LABEL": "", - "FREE_PLAN_DESCRIPTION": "", + "free_plan_description": "", "CURRENT_USAGE": "", "WEAK_DEVICE": "", "DRAG_AND_DROP_HINT": "", @@ -495,12 +493,12 @@ "EXPORT_DIRECTORY_DOES_NOT_EXIST": "", "EXPORT_DIRECTORY_DOES_NOT_EXIST_MESSAGE": "", "SUBSCRIPTION_VERIFICATION_ERROR": "", - "STORAGE_UNITS": { - "B": "", - "KB": "", - "MB": "", - "GB": "", - "TB": "" + "storage_unit": { + "b": "", + "kb": "", + "mb": "", + "gb": "", + "tb": "" }, "AFTER_TIME": { "HOUR": "", @@ -599,10 +597,10 @@ "PAIR_DEVICE_TO_TV": "", "TV_NOT_FOUND": "", "AUTO_CAST_PAIR": "", - "AUTO_CAST_PAIR_REQUIRES_CONNECTION_TO_GOOGLE": "", + "AUTO_CAST_PAIR_DESC": "", "PAIR_WITH_PIN": "", "CHOOSE_DEVICE_FROM_BROWSER": "", - "PAIR_WITH_PIN_WORKS_FOR_ANY_LARGE_SCREEN_DEVICE": "", + "PAIR_WITH_PIN_DESC": "", "VISIT_CAST_ENTE_IO": "", "CAST_AUTO_PAIR_FAILED": "", "FREEHAND": "", diff --git a/web/packages/next/locales/fr-FR/translation.json b/web/packages/next/locales/fr-FR/translation.json index f3113202fd9340558a2311136d5cce7c9b41feb3..dd17e54ab0d82eb53303eaeb44aeb9321b9260d8 100644 --- a/web/packages/next/locales/fr-FR/translation.json +++ b/web/packages/next/locales/fr-FR/translation.json @@ -418,7 +418,6 @@ "ALL_HIDDEN_ALBUMS": "Tous les albums masqués", "HIDDEN_ALBUMS": "Albums masqués", "HIDDEN_ITEMS": "Éléments masqués", - "HIDDEN_ITEMS_SECTION_NAME": "Éléments masqués", "ENTER_TWO_FACTOR_OTP": "Saisir le code à 6 caractères de votre appli d'authentification.", "CREATE_ACCOUNT": "Créer un compte", "COPIED": "Copié", @@ -448,10 +447,9 @@ "DOWNLOAD_LOGS_MESSAGE": "

Cela va télécharger les journaux de débug, que vous pourrez nosu envoyer par e-mail pour nous aider à résoudre votre problàme .

Veuillez noter que les noms de fichiers seront inclus .

", "CHANGE_FOLDER": "Modifier le dossier", "TWO_MONTHS_FREE": "Obtenir 2 mois gratuits sur les plans annuels", - "GB": "Go", "POPULAR": "Populaire", "FREE_PLAN_OPTION_LABEL": "Poursuivre avec la version d'essai gratuite", - "FREE_PLAN_DESCRIPTION": "1 Go pour 1 an", + "free_plan_description": "{{storage}} pour 1 an", "CURRENT_USAGE": "L'utilisation actuelle est de {{usage}}", "WEAK_DEVICE": "Le navigateur que vous utilisez n'est pas assez puissant pour chiffrer vos photos. Veuillez essayer de vous connecter à Ente sur votre ordinateur, ou télécharger l'appli Ente mobile/ordinateur.", "DRAG_AND_DROP_HINT": "Sinon glissez déposez dans la fenêtre Ente", @@ -495,12 +493,12 @@ "EXPORT_DIRECTORY_DOES_NOT_EXIST": "Dossier d'export invalide", "EXPORT_DIRECTORY_DOES_NOT_EXIST_MESSAGE": "

Le dossier d'export que vous avez sélectionné n'existe pas

Veuillez sélectionner un dossier valide

", "SUBSCRIPTION_VERIFICATION_ERROR": "Échec de la vérification de l'abonnement", - "STORAGE_UNITS": { - "B": "o", - "KB": "Ko", - "MB": "Mo", - "GB": "Go", - "TB": "To" + "storage_unit": { + "b": "o", + "kb": "Ko", + "mb": "Mo", + "gb": "Go", + "tb": "To" }, "AFTER_TIME": { "HOUR": "dans une heure", @@ -598,13 +596,13 @@ "ENTER_CAST_PIN_CODE": "Entrez le code que vous voyez sur la TV ci-dessous pour appairer cet appareil.", "PAIR_DEVICE_TO_TV": "Associer les appareils", "TV_NOT_FOUND": "TV introuvable. Avez-vous entré le code PIN correctement ?", - "AUTO_CAST_PAIR": "Paire automatique", - "AUTO_CAST_PAIR_REQUIRES_CONNECTION_TO_GOOGLE": "La paire automatique nécessite la connexion aux serveurs Google et ne fonctionne qu'avec les appareils pris en charge par Chromecast. Google ne recevra pas de données sensibles, telles que vos photos.", + "AUTO_CAST_PAIR": "", + "AUTO_CAST_PAIR_DESC": "", "PAIR_WITH_PIN": "Associer avec le code PIN", "CHOOSE_DEVICE_FROM_BROWSER": "Choisissez un périphérique compatible avec la caste à partir de la fenêtre pop-up du navigateur.", - "PAIR_WITH_PIN_WORKS_FOR_ANY_LARGE_SCREEN_DEVICE": "L'association avec le code PIN fonctionne pour tout appareil grand écran sur lequel vous voulez lire votre album.", + "PAIR_WITH_PIN_DESC": "", "VISIT_CAST_ENTE_IO": "Visitez {{url}} sur l'appareil que vous voulez associer.", - "CAST_AUTO_PAIR_FAILED": "La paire automatique de Chromecast a échoué. Veuillez réessayer.", + "CAST_AUTO_PAIR_FAILED": "", "FREEHAND": "Main levée", "APPLY_CROP": "Appliquer le recadrage", "PHOTO_EDIT_REQUIRED_TO_SAVE": "Au moins une transformation ou un ajustement de couleur doit être effectué avant de sauvegarder.", diff --git a/web/packages/next/locales/it-IT/translation.json b/web/packages/next/locales/it-IT/translation.json index bf555911c31979a1d1229416f98f45de5f661294..8c767c05449dbb80d36f20abeccd0928aa23973c 100644 --- a/web/packages/next/locales/it-IT/translation.json +++ b/web/packages/next/locales/it-IT/translation.json @@ -418,7 +418,6 @@ "ALL_HIDDEN_ALBUMS": "", "HIDDEN_ALBUMS": "", "HIDDEN_ITEMS": "", - "HIDDEN_ITEMS_SECTION_NAME": "", "ENTER_TWO_FACTOR_OTP": "", "CREATE_ACCOUNT": "Crea account", "COPIED": "", @@ -448,10 +447,9 @@ "DOWNLOAD_LOGS_MESSAGE": "", "CHANGE_FOLDER": "Cambia Cartella", "TWO_MONTHS_FREE": "Ottieni 2 mesi gratis sui piani annuali", - "GB": "GB", "POPULAR": "", "FREE_PLAN_OPTION_LABEL": "", - "FREE_PLAN_DESCRIPTION": "1 GB per 1 anno", + "free_plan_description": "{{storage}} per 1 anno", "CURRENT_USAGE": "", "WEAK_DEVICE": "", "DRAG_AND_DROP_HINT": "", @@ -495,12 +493,12 @@ "EXPORT_DIRECTORY_DOES_NOT_EXIST": "", "EXPORT_DIRECTORY_DOES_NOT_EXIST_MESSAGE": "", "SUBSCRIPTION_VERIFICATION_ERROR": "", - "STORAGE_UNITS": { - "B": "B", - "KB": "KB", - "MB": "MB", - "GB": "GB", - "TB": "TB" + "storage_unit": { + "b": "B", + "kb": "KB", + "mb": "MB", + "gb": "GB", + "tb": "TB" }, "AFTER_TIME": { "HOUR": "dopo un'ora", @@ -599,10 +597,10 @@ "PAIR_DEVICE_TO_TV": "", "TV_NOT_FOUND": "", "AUTO_CAST_PAIR": "", - "AUTO_CAST_PAIR_REQUIRES_CONNECTION_TO_GOOGLE": "", + "AUTO_CAST_PAIR_DESC": "", "PAIR_WITH_PIN": "", "CHOOSE_DEVICE_FROM_BROWSER": "", - "PAIR_WITH_PIN_WORKS_FOR_ANY_LARGE_SCREEN_DEVICE": "", + "PAIR_WITH_PIN_DESC": "", "VISIT_CAST_ENTE_IO": "", "CAST_AUTO_PAIR_FAILED": "", "FREEHAND": "", diff --git a/web/packages/next/locales/ko-KR/translation.json b/web/packages/next/locales/ko-KR/translation.json index aee2c6cd5baebfefdd7bffc52c83a0228f805c47..17fc4058819091a4a56c691f3461509e384d5b78 100644 --- a/web/packages/next/locales/ko-KR/translation.json +++ b/web/packages/next/locales/ko-KR/translation.json @@ -418,7 +418,6 @@ "ALL_HIDDEN_ALBUMS": "", "HIDDEN_ALBUMS": "", "HIDDEN_ITEMS": "", - "HIDDEN_ITEMS_SECTION_NAME": "", "ENTER_TWO_FACTOR_OTP": "", "CREATE_ACCOUNT": "", "COPIED": "", @@ -448,10 +447,9 @@ "DOWNLOAD_LOGS_MESSAGE": "", "CHANGE_FOLDER": "", "TWO_MONTHS_FREE": "", - "GB": "", "POPULAR": "", "FREE_PLAN_OPTION_LABEL": "", - "FREE_PLAN_DESCRIPTION": "", + "free_plan_description": "", "CURRENT_USAGE": "", "WEAK_DEVICE": "", "DRAG_AND_DROP_HINT": "", @@ -495,12 +493,12 @@ "EXPORT_DIRECTORY_DOES_NOT_EXIST": "", "EXPORT_DIRECTORY_DOES_NOT_EXIST_MESSAGE": "", "SUBSCRIPTION_VERIFICATION_ERROR": "", - "STORAGE_UNITS": { - "B": "", - "KB": "", - "MB": "", - "GB": "", - "TB": "" + "storage_unit": { + "b": "", + "kb": "", + "mb": "", + "gb": "", + "tb": "" }, "AFTER_TIME": { "HOUR": "", @@ -599,10 +597,10 @@ "PAIR_DEVICE_TO_TV": "", "TV_NOT_FOUND": "", "AUTO_CAST_PAIR": "", - "AUTO_CAST_PAIR_REQUIRES_CONNECTION_TO_GOOGLE": "", + "AUTO_CAST_PAIR_DESC": "", "PAIR_WITH_PIN": "", "CHOOSE_DEVICE_FROM_BROWSER": "", - "PAIR_WITH_PIN_WORKS_FOR_ANY_LARGE_SCREEN_DEVICE": "", + "PAIR_WITH_PIN_DESC": "", "VISIT_CAST_ENTE_IO": "", "CAST_AUTO_PAIR_FAILED": "", "FREEHAND": "", diff --git a/web/packages/next/locales/nl-NL/translation.json b/web/packages/next/locales/nl-NL/translation.json index 62b846b14ed4095b7c8e09fcd449237497afdad0..23850582dbbea77d42810d7a4722e7551eb14a91 100644 --- a/web/packages/next/locales/nl-NL/translation.json +++ b/web/packages/next/locales/nl-NL/translation.json @@ -418,7 +418,6 @@ "ALL_HIDDEN_ALBUMS": "Alle verborgen albums", "HIDDEN_ALBUMS": "Verborgen albums", "HIDDEN_ITEMS": "Verborgen bestanden", - "HIDDEN_ITEMS_SECTION_NAME": "Verborgen_items", "ENTER_TWO_FACTOR_OTP": "Voer de 6-cijferige code van uw verificatie app in.", "CREATE_ACCOUNT": "Account aanmaken", "COPIED": "Gekopieerd", @@ -448,10 +447,9 @@ "DOWNLOAD_LOGS_MESSAGE": "

Dit zal logboeken downloaden, die u ons kunt e-mailen om te helpen bij het debuggen van uw probleem.

Houd er rekening mee dat bestandsnamen worden opgenomen om problemen met specifieke bestanden bij te houden.

", "CHANGE_FOLDER": "Map wijzigen", "TWO_MONTHS_FREE": "Krijg 2 maanden gratis op jaarlijkse abonnementen", - "GB": "GB", "POPULAR": "Populair", "FREE_PLAN_OPTION_LABEL": "Doorgaan met gratis account", - "FREE_PLAN_DESCRIPTION": "1 GB voor 1 jaar", + "free_plan_description": "{{storage}} voor 1 jaar", "CURRENT_USAGE": "Huidig gebruik is {{usage}}", "WEAK_DEVICE": "De webbrowser die u gebruikt is niet krachtig genoeg om uw foto's te versleutelen. Probeer in te loggen op uw computer, of download de Ente mobiel/desktop app.", "DRAG_AND_DROP_HINT": "Of sleep en plaats in het Ente venster", @@ -495,12 +493,12 @@ "EXPORT_DIRECTORY_DOES_NOT_EXIST": "Ongeldige export map", "EXPORT_DIRECTORY_DOES_NOT_EXIST_MESSAGE": "

De export map die u heeft geselecteerd bestaat niet.

Selecteer een geldige map.

", "SUBSCRIPTION_VERIFICATION_ERROR": "Abonnementsverificatie mislukt", - "STORAGE_UNITS": { - "B": "B", - "KB": "KB", - "MB": "MB", - "GB": "GB", - "TB": "TB" + "storage_unit": { + "b": "B", + "kb": "KB", + "mb": "MB", + "gb": "GB", + "tb": "TB" }, "AFTER_TIME": { "HOUR": "na één uur", @@ -599,12 +597,12 @@ "PAIR_DEVICE_TO_TV": "Koppel apparaten", "TV_NOT_FOUND": "TV niet gevonden. Heeft u de pincode correct ingevoerd?", "AUTO_CAST_PAIR": "Automatisch koppelen", - "AUTO_CAST_PAIR_REQUIRES_CONNECTION_TO_GOOGLE": "Automatisch koppelen vereist verbinding met Google-servers en werkt alleen met apparaten die door Chromecast worden ondersteund. Google zal geen gevoelige gegevens ontvangen, zoals uw foto's.", + "AUTO_CAST_PAIR_DESC": "Automatisch koppelen werkt alleen met apparaten die Chromecast ondersteunen.", "PAIR_WITH_PIN": "Koppelen met PIN", "CHOOSE_DEVICE_FROM_BROWSER": "Kies een compatibel apparaat uit de browser popup.", - "PAIR_WITH_PIN_WORKS_FOR_ANY_LARGE_SCREEN_DEVICE": "Koppelen met PIN werkt op elk groot schermapparaat waarop u uw album wilt afspelen.", + "PAIR_WITH_PIN_DESC": "Koppelen met de PIN werkt met elk scherm waarop je jouw album wilt zien.", "VISIT_CAST_ENTE_IO": "Bezoek {{url}} op het apparaat dat je wilt koppelen.", - "CAST_AUTO_PAIR_FAILED": "Auto koppelen van Chromecast is mislukt. Probeer het opnieuw.", + "CAST_AUTO_PAIR_FAILED": "Automatisch koppelen van Chromecast mislukt. Probeer het opnieuw.", "FREEHAND": "Losse hand", "APPLY_CROP": "Bijsnijden toepassen", "PHOTO_EDIT_REQUIRED_TO_SAVE": "Tenminste één transformatie of kleuraanpassing moet worden uitgevoerd voordat u opslaat.", @@ -622,6 +620,6 @@ "TRY_AGAIN": "Probeer opnieuw", "PASSKEY_FOLLOW_THE_STEPS_FROM_YOUR_BROWSER": "Volg de stappen van je browser om door te gaan met inloggen.", "LOGIN_WITH_PASSKEY": "Inloggen met passkey", - "autogenerated_first_album_name": "", - "autogenerated_default_album_name": "" + "autogenerated_first_album_name": "Mijn eerste album", + "autogenerated_default_album_name": "Nieuw album" } diff --git a/web/packages/next/locales/pt-BR/translation.json b/web/packages/next/locales/pt-BR/translation.json index dfe0030c5661fe78daa82dd0ca0ba2cce6a75289..6d36812ced7bc658ac3edda10f4b1ecd1a266889 100644 --- a/web/packages/next/locales/pt-BR/translation.json +++ b/web/packages/next/locales/pt-BR/translation.json @@ -239,7 +239,7 @@ "ENABLE_MAPS": "Habilitar mapa?", "ENABLE_MAP": "Habilitar mapa", "DISABLE_MAPS": "Desativar Mapas?", - "ENABLE_MAP_DESCRIPTION": "Isto mostrará suas fotos em um mapa do mundo.

Este mapa é hospedado pelo OpenStreetMap , e os exatos locais de suas fotos nunca são compartilhados.

Você pode desativar esse recurso a qualquer momento nas Configurações.

", + "ENABLE_MAP_DESCRIPTION": "

Isto mostrará suas fotos em um mapa do mundo.

Este mapa é hospedado pelo OpenStreetMap, e os exatos locais de suas fotos nunca são compartilhados.

Você pode desativar esse recurso a qualquer momento nas Configurações.

", "DISABLE_MAP_DESCRIPTION": "

Isto irá desativar a exibição de suas fotos em um mapa mundial.

Você pode ativar este recurso a qualquer momento nas Configurações.

", "DISABLE_MAP": "Desabilitar mapa", "DETAILS": "Detalhes", @@ -380,14 +380,14 @@ "LINK_EXPIRED_MESSAGE": "Este link expirou ou foi desativado!", "MANAGE_LINK": "Gerenciar link", "LINK_TOO_MANY_REQUESTS": "Desculpe, este álbum foi visualizado em muitos dispositivos!", - "FILE_DOWNLOAD": "Permitir transferências", + "FILE_DOWNLOAD": "Permitir downloads", "LINK_PASSWORD_LOCK": "Bloqueio de senha", "PUBLIC_COLLECT": "Permitir adicionar fotos", "LINK_DEVICE_LIMIT": "Limite de dispositivos", "NO_DEVICE_LIMIT": "Nenhum", "LINK_EXPIRY": "Expiração do link", "NEVER": "Nunca", - "DISABLE_FILE_DOWNLOAD": "Desabilitar transferência", + "DISABLE_FILE_DOWNLOAD": "Desabilitar download", "DISABLE_FILE_DOWNLOAD_MESSAGE": "

Tem certeza de que deseja desativar o botão de download para arquivos?

Os visualizadores ainda podem capturar imagens da tela ou salvar uma cópia de suas fotos usando ferramentas externas.

", "SHARED_USING": "Compartilhar usando ", "SHARING_REFERRAL_CODE": "Use o código {{referralCode}} para obter 10 GB de graça", @@ -408,8 +408,8 @@ "STOP_ALL_UPLOADS_MESSAGE": "Tem certeza que deseja parar todos os envios em andamento?", "STOP_UPLOADS_HEADER": "Parar envios?", "YES_STOP_UPLOADS": "Sim, parar envios", - "STOP_DOWNLOADS_HEADER": "Parar transferências?", - "YES_STOP_DOWNLOADS": "Sim, parar transferências", + "STOP_DOWNLOADS_HEADER": "Parar downloads?", + "YES_STOP_DOWNLOADS": "Sim, parar downloads", "STOP_ALL_DOWNLOADS_MESSAGE": "Tem certeza que deseja parar todos as transferências em andamento?", "albums_one": "1 Álbum", "albums_other": "{{count, number}} Álbuns", @@ -418,7 +418,6 @@ "ALL_HIDDEN_ALBUMS": "Todos os álbuns ocultos", "HIDDEN_ALBUMS": "Álbuns ocultos", "HIDDEN_ITEMS": "Itens ocultos", - "HIDDEN_ITEMS_SECTION_NAME": "Itens_ocultos", "ENTER_TWO_FACTOR_OTP": "Digite o código de 6 dígitos de\nseu aplicativo autenticador.", "CREATE_ACCOUNT": "Criar uma conta", "COPIED": "Copiado", @@ -448,10 +447,9 @@ "DOWNLOAD_LOGS_MESSAGE": "

Isto irá baixar os logs de depuração, que você pode enviar para nós para ajudar a depurar seu problema.

Por favor, note que os nomes de arquivos serão incluídos para ajudar a rastrear problemas com arquivos específicos.

", "CHANGE_FOLDER": "Alterar pasta", "TWO_MONTHS_FREE": "Obtenha 2 meses gratuitos em planos anuais", - "GB": "GB", "POPULAR": "Popular", "FREE_PLAN_OPTION_LABEL": "Continuar com teste gratuito", - "FREE_PLAN_DESCRIPTION": "1 GB por 1 ano", + "free_plan_description": "{{storage}} por 1 ano", "CURRENT_USAGE": "O uso atual é {{usage}}", "WEAK_DEVICE": "O navegador da web que você está usando não é poderoso o suficiente para criptografar suas fotos. Por favor, tente entrar para o ente no computador ou baixe o aplicativo móvel.", "DRAG_AND_DROP_HINT": "Ou arraste e solte na janela ente", @@ -495,12 +493,12 @@ "EXPORT_DIRECTORY_DOES_NOT_EXIST": "Diretório de exportação inválido", "EXPORT_DIRECTORY_DOES_NOT_EXIST_MESSAGE": "

O diretório de exportação que você selecionou não existe.

Por favor, selecione um diretório válido.

", "SUBSCRIPTION_VERIFICATION_ERROR": "Falha na verificação de assinatura", - "STORAGE_UNITS": { - "B": "B", - "KB": "KB", - "MB": "MB", - "GB": "GB", - "TB": "TB" + "storage_unit": { + "b": "B", + "kb": "KB", + "mb": "MB", + "gb": "GB", + "tb": "TB" }, "AFTER_TIME": { "HOUR": "após uma hora", @@ -556,8 +554,8 @@ "SELECT_COLLECTION": "Selecionar álbum", "PIN_ALBUM": "Fixar álbum", "UNPIN_ALBUM": "Desafixar álbum", - "DOWNLOAD_COMPLETE": "Transferência concluída", - "DOWNLOADING_COLLECTION": "Transferindo {{name}}", + "DOWNLOAD_COMPLETE": "Download concluído", + "DOWNLOADING_COLLECTION": "Fazendo download de {{name}}", "DOWNLOAD_FAILED": "Falha ao baixar", "DOWNLOAD_PROGRESS": "{{progress.current}} / {{progress.total}} arquivos", "CHRISTMAS": "Natal", @@ -599,12 +597,12 @@ "PAIR_DEVICE_TO_TV": "Parear dispositivos", "TV_NOT_FOUND": "TV não encontrada. Você inseriu o PIN correto?", "AUTO_CAST_PAIR": "Pareamento automático", - "AUTO_CAST_PAIR_REQUIRES_CONNECTION_TO_GOOGLE": "O Auto Pair requer a conexão com servidores do Google e só funciona com dispositivos Chromecast. O Google não receberá dados confidenciais, como suas fotos.", + "AUTO_CAST_PAIR_DESC": "O pareamento automático funciona apenas com dispositivos que suportam o Chromecast.", "PAIR_WITH_PIN": "Parear com PIN", "CHOOSE_DEVICE_FROM_BROWSER": "Escolha um dispositivo compatível com casts no navegador popup.", - "PAIR_WITH_PIN_WORKS_FOR_ANY_LARGE_SCREEN_DEVICE": "Parear com o PIN funciona para qualquer dispositivo de tela grande onde você deseja reproduzir seu álbum.", + "PAIR_WITH_PIN_DESC": "Parear com o PIN funciona com qualquer tela que você deseja ver o seu álbum ativado.", "VISIT_CAST_ENTE_IO": "Acesse
{{url}} no dispositivo que você deseja parear.", - "CAST_AUTO_PAIR_FAILED": "Chromecast Auto Pair falhou. Por favor, tente novamente.", + "CAST_AUTO_PAIR_FAILED": "Falha no pareamento automático do Chromecast. Por favor, tente novamente.", "FREEHAND": "Mão livre", "APPLY_CROP": "Aplicar Recorte", "PHOTO_EDIT_REQUIRED_TO_SAVE": "Pelo menos uma transformação ou ajuste de cor deve ser feito antes de salvar.", @@ -622,6 +620,6 @@ "TRY_AGAIN": "Tente novamente", "PASSKEY_FOLLOW_THE_STEPS_FROM_YOUR_BROWSER": "Siga os passos do seu navegador para continuar acessando.", "LOGIN_WITH_PASSKEY": "Entrar com a chave de acesso", - "autogenerated_first_album_name": "", - "autogenerated_default_album_name": "" + "autogenerated_first_album_name": "Meu Primeiro Álbum", + "autogenerated_default_album_name": "Novo Álbum" } diff --git a/web/packages/next/locales/pt-PT/translation.json b/web/packages/next/locales/pt-PT/translation.json index f6980b56e3a7ab3f0e269dbd9fc529e86c7c306a..c89049ec2d70f6751e86c24497e852249ce35010 100644 --- a/web/packages/next/locales/pt-PT/translation.json +++ b/web/packages/next/locales/pt-PT/translation.json @@ -418,7 +418,6 @@ "ALL_HIDDEN_ALBUMS": "", "HIDDEN_ALBUMS": "", "HIDDEN_ITEMS": "", - "HIDDEN_ITEMS_SECTION_NAME": "", "ENTER_TWO_FACTOR_OTP": "", "CREATE_ACCOUNT": "", "COPIED": "", @@ -448,10 +447,9 @@ "DOWNLOAD_LOGS_MESSAGE": "", "CHANGE_FOLDER": "", "TWO_MONTHS_FREE": "", - "GB": "", "POPULAR": "", "FREE_PLAN_OPTION_LABEL": "", - "FREE_PLAN_DESCRIPTION": "", + "free_plan_description": "", "CURRENT_USAGE": "", "WEAK_DEVICE": "", "DRAG_AND_DROP_HINT": "", @@ -495,12 +493,12 @@ "EXPORT_DIRECTORY_DOES_NOT_EXIST": "", "EXPORT_DIRECTORY_DOES_NOT_EXIST_MESSAGE": "", "SUBSCRIPTION_VERIFICATION_ERROR": "", - "STORAGE_UNITS": { - "B": "", - "KB": "", - "MB": "", - "GB": "", - "TB": "" + "storage_unit": { + "b": "", + "kb": "", + "mb": "", + "gb": "", + "tb": "" }, "AFTER_TIME": { "HOUR": "", @@ -599,10 +597,10 @@ "PAIR_DEVICE_TO_TV": "", "TV_NOT_FOUND": "", "AUTO_CAST_PAIR": "", - "AUTO_CAST_PAIR_REQUIRES_CONNECTION_TO_GOOGLE": "", + "AUTO_CAST_PAIR_DESC": "", "PAIR_WITH_PIN": "", "CHOOSE_DEVICE_FROM_BROWSER": "", - "PAIR_WITH_PIN_WORKS_FOR_ANY_LARGE_SCREEN_DEVICE": "", + "PAIR_WITH_PIN_DESC": "", "VISIT_CAST_ENTE_IO": "", "CAST_AUTO_PAIR_FAILED": "", "FREEHAND": "", diff --git a/web/packages/next/locales/ru-RU/translation.json b/web/packages/next/locales/ru-RU/translation.json index 5d036c6c8a0cdaf8ce333e4d90676886ac44ad71..d8c90af17e5a68e92ea16cdb200f5cb7641e247f 100644 --- a/web/packages/next/locales/ru-RU/translation.json +++ b/web/packages/next/locales/ru-RU/translation.json @@ -418,7 +418,6 @@ "ALL_HIDDEN_ALBUMS": "Все скрытые альбомы", "HIDDEN_ALBUMS": "Скрытые альбомы", "HIDDEN_ITEMS": "Скрытые предметы", - "HIDDEN_ITEMS_SECTION_NAME": "Скрытые_элементы", "ENTER_TWO_FACTOR_OTP": "Введите 6-значный код из вашего приложения для проверки подлинности.", "CREATE_ACCOUNT": "Создать аккаунт", "COPIED": "Скопированный", @@ -448,10 +447,9 @@ "DOWNLOAD_LOGS_MESSAGE": "

При этом будут загружены журналы отладки, которые вы можете отправить нам по электронной почте, чтобы помочь в устранении вашей проблемы.

Пожалуйста, обратите внимание, что будут указаны имена файлов, которые помогут отслеживать проблемы с конкретными файлами.

", "CHANGE_FOLDER": "Изменить папку", "TWO_MONTHS_FREE": "Получите 2 месяца бесплатно по годовым планам", - "GB": "Гб", "POPULAR": "Популярный", "FREE_PLAN_OPTION_LABEL": "Продолжайте пользоваться бесплатной пробной версией", - "FREE_PLAN_DESCRIPTION": "1 ГБ на 1 год", + "free_plan_description": "{{storage}} на 1 год", "CURRENT_USAGE": "Текущее использование составляет {{usage}}", "WEAK_DEVICE": "Используемый вами веб-браузер недостаточно мощный, чтобы зашифровать ваши фотографии. Пожалуйста, попробуйте войти в Ente на своем компьютере или загрузить мобильное/настольное приложение Ente.", "DRAG_AND_DROP_HINT": "Или перетащите в основное окно", @@ -495,12 +493,12 @@ "EXPORT_DIRECTORY_DOES_NOT_EXIST": "Недопустимый каталог экспорта", "EXPORT_DIRECTORY_DOES_NOT_EXIST_MESSAGE": "

Выбранный вами каталог экспорта не существует.

Пожалуйста, выберите правильный каталог.

", "SUBSCRIPTION_VERIFICATION_ERROR": "Не удалось подтвердить подписку", - "STORAGE_UNITS": { - "B": "B", - "KB": "БЗ", - "MB": "Мегабайт", - "GB": "Гб", - "TB": "Терабайт" + "storage_unit": { + "b": "B", + "kb": "БЗ", + "mb": "Мегабайт", + "gb": "Гб", + "tb": "Терабайт" }, "AFTER_TIME": { "HOUR": "через час", @@ -598,13 +596,13 @@ "ENTER_CAST_PIN_CODE": "Введите код, который вы видите на экране телевизора ниже, чтобы выполнить сопряжение с этим устройством.", "PAIR_DEVICE_TO_TV": "Сопряжение устройств", "TV_NOT_FOUND": "Телевизор не найден. Вы правильно ввели PIN-код?", - "AUTO_CAST_PAIR": "Автоматическое сопряжение", - "AUTO_CAST_PAIR_REQUIRES_CONNECTION_TO_GOOGLE": "Автоматическое сопряжение требует подключения к серверам Google и работает только с устройствами, поддерживающими Chromecast. Google не будет получать конфиденциальные данные, такие как ваши фотографии.", + "AUTO_CAST_PAIR": "", + "AUTO_CAST_PAIR_DESC": "", "PAIR_WITH_PIN": "Соединение с помощью булавки", "CHOOSE_DEVICE_FROM_BROWSER": "Выберите устройство, совместимое с cast, во всплывающем окне браузера.", - "PAIR_WITH_PIN_WORKS_FOR_ANY_LARGE_SCREEN_DEVICE": "Сопряжение с помощью PIN-кода работает на любом устройстве с большим экраном, на котором вы хотите воспроизвести свой альбом.", + "PAIR_WITH_PIN_DESC": "", "VISIT_CAST_ENTE_IO": "Перейдите на страницу {{url}} на устройстве, которое вы хотите подключить.", - "CAST_AUTO_PAIR_FAILED": "Не удалось выполнить автоматическое сопряжение Chromecast. Пожалуйста, попробуйте снова.", + "CAST_AUTO_PAIR_FAILED": "", "FREEHAND": "От руки", "APPLY_CROP": "Применить обрезку", "PHOTO_EDIT_REQUIRED_TO_SAVE": "Перед сохранением необходимо выполнить по крайней мере одно преобразование или корректировку цвета.", diff --git a/web/packages/next/locales/sv-SE/translation.json b/web/packages/next/locales/sv-SE/translation.json index ba6ecee09711ec59a9ec6bfb94d03aa99177b8f6..1ceb6370ca7495938972cc22fb2eed42fd29c42e 100644 --- a/web/packages/next/locales/sv-SE/translation.json +++ b/web/packages/next/locales/sv-SE/translation.json @@ -418,7 +418,6 @@ "ALL_HIDDEN_ALBUMS": "", "HIDDEN_ALBUMS": "", "HIDDEN_ITEMS": "", - "HIDDEN_ITEMS_SECTION_NAME": "", "ENTER_TWO_FACTOR_OTP": "", "CREATE_ACCOUNT": "", "COPIED": "", @@ -448,10 +447,9 @@ "DOWNLOAD_LOGS_MESSAGE": "", "CHANGE_FOLDER": "", "TWO_MONTHS_FREE": "", - "GB": "GB", "POPULAR": "", "FREE_PLAN_OPTION_LABEL": "", - "FREE_PLAN_DESCRIPTION": "", + "free_plan_description": "", "CURRENT_USAGE": "", "WEAK_DEVICE": "", "DRAG_AND_DROP_HINT": "", @@ -495,12 +493,12 @@ "EXPORT_DIRECTORY_DOES_NOT_EXIST": "", "EXPORT_DIRECTORY_DOES_NOT_EXIST_MESSAGE": "", "SUBSCRIPTION_VERIFICATION_ERROR": "", - "STORAGE_UNITS": { - "B": "", - "KB": "", - "MB": "", - "GB": "", - "TB": "" + "storage_unit": { + "b": "", + "kb": "", + "mb": "", + "gb": "", + "tb": "" }, "AFTER_TIME": { "HOUR": "", @@ -599,10 +597,10 @@ "PAIR_DEVICE_TO_TV": "", "TV_NOT_FOUND": "", "AUTO_CAST_PAIR": "", - "AUTO_CAST_PAIR_REQUIRES_CONNECTION_TO_GOOGLE": "", + "AUTO_CAST_PAIR_DESC": "", "PAIR_WITH_PIN": "", "CHOOSE_DEVICE_FROM_BROWSER": "", - "PAIR_WITH_PIN_WORKS_FOR_ANY_LARGE_SCREEN_DEVICE": "", + "PAIR_WITH_PIN_DESC": "", "VISIT_CAST_ENTE_IO": "", "CAST_AUTO_PAIR_FAILED": "", "FREEHAND": "", diff --git a/web/packages/next/locales/th-TH/translation.json b/web/packages/next/locales/th-TH/translation.json index d945fcde320d6fdf4f9f00288963f3a504786f1c..33306389c9ba2916a1ffce90b0dd1d79da867018 100644 --- a/web/packages/next/locales/th-TH/translation.json +++ b/web/packages/next/locales/th-TH/translation.json @@ -418,7 +418,6 @@ "ALL_HIDDEN_ALBUMS": "", "HIDDEN_ALBUMS": "", "HIDDEN_ITEMS": "", - "HIDDEN_ITEMS_SECTION_NAME": "", "ENTER_TWO_FACTOR_OTP": "", "CREATE_ACCOUNT": "", "COPIED": "", @@ -448,10 +447,9 @@ "DOWNLOAD_LOGS_MESSAGE": "", "CHANGE_FOLDER": "", "TWO_MONTHS_FREE": "", - "GB": "", "POPULAR": "", "FREE_PLAN_OPTION_LABEL": "", - "FREE_PLAN_DESCRIPTION": "", + "free_plan_description": "", "CURRENT_USAGE": "", "WEAK_DEVICE": "", "DRAG_AND_DROP_HINT": "", @@ -495,12 +493,12 @@ "EXPORT_DIRECTORY_DOES_NOT_EXIST": "", "EXPORT_DIRECTORY_DOES_NOT_EXIST_MESSAGE": "", "SUBSCRIPTION_VERIFICATION_ERROR": "", - "STORAGE_UNITS": { - "B": "", - "KB": "", - "MB": "", - "GB": "", - "TB": "" + "storage_unit": { + "b": "", + "kb": "", + "mb": "", + "gb": "", + "tb": "" }, "AFTER_TIME": { "HOUR": "", @@ -599,10 +597,10 @@ "PAIR_DEVICE_TO_TV": "", "TV_NOT_FOUND": "", "AUTO_CAST_PAIR": "", - "AUTO_CAST_PAIR_REQUIRES_CONNECTION_TO_GOOGLE": "", + "AUTO_CAST_PAIR_DESC": "", "PAIR_WITH_PIN": "", "CHOOSE_DEVICE_FROM_BROWSER": "", - "PAIR_WITH_PIN_WORKS_FOR_ANY_LARGE_SCREEN_DEVICE": "", + "PAIR_WITH_PIN_DESC": "", "VISIT_CAST_ENTE_IO": "", "CAST_AUTO_PAIR_FAILED": "", "FREEHAND": "", diff --git a/web/packages/next/locales/tr-TR/translation.json b/web/packages/next/locales/tr-TR/translation.json index d945fcde320d6fdf4f9f00288963f3a504786f1c..33306389c9ba2916a1ffce90b0dd1d79da867018 100644 --- a/web/packages/next/locales/tr-TR/translation.json +++ b/web/packages/next/locales/tr-TR/translation.json @@ -418,7 +418,6 @@ "ALL_HIDDEN_ALBUMS": "", "HIDDEN_ALBUMS": "", "HIDDEN_ITEMS": "", - "HIDDEN_ITEMS_SECTION_NAME": "", "ENTER_TWO_FACTOR_OTP": "", "CREATE_ACCOUNT": "", "COPIED": "", @@ -448,10 +447,9 @@ "DOWNLOAD_LOGS_MESSAGE": "", "CHANGE_FOLDER": "", "TWO_MONTHS_FREE": "", - "GB": "", "POPULAR": "", "FREE_PLAN_OPTION_LABEL": "", - "FREE_PLAN_DESCRIPTION": "", + "free_plan_description": "", "CURRENT_USAGE": "", "WEAK_DEVICE": "", "DRAG_AND_DROP_HINT": "", @@ -495,12 +493,12 @@ "EXPORT_DIRECTORY_DOES_NOT_EXIST": "", "EXPORT_DIRECTORY_DOES_NOT_EXIST_MESSAGE": "", "SUBSCRIPTION_VERIFICATION_ERROR": "", - "STORAGE_UNITS": { - "B": "", - "KB": "", - "MB": "", - "GB": "", - "TB": "" + "storage_unit": { + "b": "", + "kb": "", + "mb": "", + "gb": "", + "tb": "" }, "AFTER_TIME": { "HOUR": "", @@ -599,10 +597,10 @@ "PAIR_DEVICE_TO_TV": "", "TV_NOT_FOUND": "", "AUTO_CAST_PAIR": "", - "AUTO_CAST_PAIR_REQUIRES_CONNECTION_TO_GOOGLE": "", + "AUTO_CAST_PAIR_DESC": "", "PAIR_WITH_PIN": "", "CHOOSE_DEVICE_FROM_BROWSER": "", - "PAIR_WITH_PIN_WORKS_FOR_ANY_LARGE_SCREEN_DEVICE": "", + "PAIR_WITH_PIN_DESC": "", "VISIT_CAST_ENTE_IO": "", "CAST_AUTO_PAIR_FAILED": "", "FREEHAND": "", diff --git a/web/packages/next/locales/zh-CN/translation.json b/web/packages/next/locales/zh-CN/translation.json index d2345f1ae7cf3942db71386f9c01ed7e49e6b5b1..2489bdd43eb2de74a9e84507fd7f593de82ce5b3 100644 --- a/web/packages/next/locales/zh-CN/translation.json +++ b/web/packages/next/locales/zh-CN/translation.json @@ -418,7 +418,6 @@ "ALL_HIDDEN_ALBUMS": "所有隐藏的相册", "HIDDEN_ALBUMS": "隐藏的相册", "HIDDEN_ITEMS": "隐藏的项目", - "HIDDEN_ITEMS_SECTION_NAME": "隐藏的项目", "ENTER_TWO_FACTOR_OTP": "请输入您从身份验证应用上获得的6位数代码", "CREATE_ACCOUNT": "创建账户", "COPIED": "已复制", @@ -448,10 +447,9 @@ "DOWNLOAD_LOGS_MESSAGE": "

这将下载调试日志,您可以发送电子邮件给我们来帮助调试您的问题。

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

", "CHANGE_FOLDER": "更改文件夹", "TWO_MONTHS_FREE": "在年度计划上免费获得 2 个月", - "GB": "GB", "POPULAR": "流行的", "FREE_PLAN_OPTION_LABEL": "继续免费试用", - "FREE_PLAN_DESCRIPTION": "1 GB 1年", + "free_plan_description": "{{storage}} 1年", "CURRENT_USAGE": "当前使用量是 {{usage}}", "WEAK_DEVICE": "您使用的网络浏览器功能不够强大,无法加密您的照片。 请尝试在电脑上登录Ente,或下载Ente移动/桌面应用程序。", "DRAG_AND_DROP_HINT": "或者拖动并拖动到 Ente 窗口", @@ -495,12 +493,12 @@ "EXPORT_DIRECTORY_DOES_NOT_EXIST": "无效的导出目录", "EXPORT_DIRECTORY_DOES_NOT_EXIST_MESSAGE": "

您选择的导出目录不存在。

请选择一个有效的目录。

", "SUBSCRIPTION_VERIFICATION_ERROR": "订阅验证失败", - "STORAGE_UNITS": { - "B": "B", - "KB": "KB", - "MB": "MB", - "GB": "GB", - "TB": "TB" + "storage_unit": { + "b": "B", + "kb": "KB", + "mb": "MB", + "gb": "GB", + "tb": "TB" }, "AFTER_TIME": { "HOUR": "1小时后", @@ -599,10 +597,10 @@ "PAIR_DEVICE_TO_TV": "配对设备", "TV_NOT_FOUND": "未找到电视。您输入的 PIN 码正确吗?", "AUTO_CAST_PAIR": "自动配对", - "AUTO_CAST_PAIR_REQUIRES_CONNECTION_TO_GOOGLE": "自动配对需要连接到 Google 服务器,且仅适用于支持 Chromecast 的设备。Google 不会接收敏感数据,例如您的照片。", + "AUTO_CAST_PAIR_DESC": "自动配对仅适用于支持 Chromecast 的设备。", "PAIR_WITH_PIN": "用 PIN 配对", "CHOOSE_DEVICE_FROM_BROWSER": "从浏览器弹出窗口中选择兼容 Cast 的设备。", - "PAIR_WITH_PIN_WORKS_FOR_ANY_LARGE_SCREEN_DEVICE": "用 PIN 配对适用于任何大屏幕设备,您可以在这些设备上播放您的相册。", + "PAIR_WITH_PIN_DESC": "用 PIN 码配对适用于您希望在其上查看相册的任何屏幕。", "VISIT_CAST_ENTE_IO": "在您要配对的设备上访问 {{url}} 。", "CAST_AUTO_PAIR_FAILED": "Chromecast 自动配对失败。请再试一次。", "FREEHAND": "手画", @@ -622,6 +620,6 @@ "TRY_AGAIN": "重试", "PASSKEY_FOLLOW_THE_STEPS_FROM_YOUR_BROWSER": "按照浏览器中提示的步骤继续登录。", "LOGIN_WITH_PASSKEY": "使用通行密钥来登录", - "autogenerated_first_album_name": "", - "autogenerated_default_album_name": "" + "autogenerated_first_album_name": "我的第一个相册", + "autogenerated_default_album_name": "新建相册" } diff --git a/web/packages/next/log.ts b/web/packages/next/log.ts index a04520ed3db16669b8c5198e567834ab33f60e1c..f9ef7e5493e3ba09b96ab262d68a6779acb2978c 100644 --- a/web/packages/next/log.ts +++ b/web/packages/next/log.ts @@ -17,7 +17,7 @@ export const logToDisk = (message: string) => { }; const workerLogToDisk = (message: string) => { - workerBridge.logToDisk(message).catch((e) => { + workerBridge.logToDisk(message).catch((e: unknown) => { console.error( "Failed to log a message from worker", e, diff --git a/web/packages/next/types/file.ts b/web/packages/next/types/file.ts deleted file mode 100644 index 6dd1032cdb167e631a4e27d1b6fcf8ab8cc01b74..0000000000000000000000000000000000000000 --- a/web/packages/next/types/file.ts +++ /dev/null @@ -1,36 +0,0 @@ -/* - * ElectronFile is a custom interface that is used to represent - * any file on disk as a File-like object in the Electron desktop app. - * - * This was added to support the auto-resuming of failed uploads - * which needed absolute paths to the files which the - * normal File interface does not provide. - */ -export interface ElectronFile { - name: string; - path: string; - size: number; - lastModified: number; - stream: () => Promise>; - blob: () => Promise; - arrayBuffer: () => Promise; -} - -/** - * When we are running in the context of our desktop app, we have access to the - * absolute path of {@link File} objects. This convenience type clubs these two - * bits of information, saving us the need to query the path again and again - * using the {@link getPathForFile} method of {@link Electron}. - */ -export interface FileAndPath { - file: File; - path: string; -} - -export interface EventQueueItem { - type: "upload" | "trash"; - folderPath: string; - collectionName?: string; - paths?: string[]; - files?: ElectronFile[]; -} diff --git a/web/packages/next/types/ipc.ts b/web/packages/next/types/ipc.ts index 173b12b17c8c968235844ec38f3a973038accb7d..b4ef2b6b24804c51bb00251e3270fa261f0ac74e 100644 --- a/web/packages/next/types/ipc.ts +++ b/web/packages/next/types/ipc.ts @@ -3,8 +3,6 @@ // // See [Note: types.ts <-> preload.ts <-> ipc.ts] -import type { ElectronFile } from "./file"; - /** * Extra APIs provided by our Node.js layer when our code is running inside our * desktop (Electron) app. @@ -51,6 +49,20 @@ export interface Electron { */ openLogDirectory: () => Promise; + /** + * Ask the user to select a directory on their local file system, and return + * it path. + * + * The returned path is guaranteed to use POSIX separators ('/'). + * + * We don't strictly need IPC for this, we can use a hidden element + * and trigger its click for the same behaviour (as we do for the + * `useFileInput` hook that we use for uploads). However, it's a bit + * cumbersome, and we anyways will need to IPC to get back its full path, so + * it is just convenient to expose this direct method. + */ + selectDirectory: () => Promise; + /** * Clear any stored data. * @@ -122,6 +134,8 @@ export interface Electron { */ skipAppUpdate: (version: string) => void; + // - FS + /** * A subset of file system access APIs. * @@ -332,19 +346,27 @@ export interface Electron { */ faceEmbedding: (input: Float32Array) => Promise; - // - File selection - // TODO: Deprecated - use dialogs on the renderer process itself - - selectDirectory: () => Promise; - - showUploadFilesDialog: () => Promise; - - showUploadDirsDialog: () => Promise; - - showUploadZipDialog: () => Promise<{ - zipPaths: string[]; - files: ElectronFile[]; - }>; + /** + * Return a face crop stored by a previous version of ML. + * + * [Note: Legacy face crops] + * + * Older versions of ML generated and stored face crops in a "face-crops" + * cache directory on the Electron side. For the time being, we have + * disabled the face search whilst we put finishing touches to it. However, + * it'll be nice to still show the existing faces that have been clustered + * for people who opted in to the older beta. + * + * So we retain the older "face-crops" disk cache, and use this method to + * serve faces from it when needed. + * + * @param faceID An identifier corresponding to which the face crop had been + * stored by the older version of our app. + * + * @returns the JPEG data of the face crop if a file is found for the given + * {@link faceID}, otherwise undefined. + */ + legacyFaceCrop: (faceID: string) => Promise; // - Watch @@ -462,6 +484,17 @@ export interface Electron { * The returned paths are guaranteed to use POSIX separators ('/'). */ findFiles: (folderPath: string) => Promise; + + /** + * Stop watching all existing folder watches and remove any callbacks. + * + * This function is meant to be called when the user logs out. It stops + * all existing folder watches and forgets about any "on*" callback + * functions that have been registered. + * + * The persisted state itself gets cleared via {@link clearStores}. + */ + reset: () => Promise; }; // - Upload @@ -634,6 +667,19 @@ export interface FolderWatchSyncedFile { * The name of the entry is not just the file name, but rather is the full path * of the file within the zip. That is, each entry name uniquely identifies a * particular file within the given zip. + * + * When `entryName` is a path within a nested directory, it is guaranteed to use + * the POSIX path separator ("/") since that is the path separator required by + * the ZIP format itself + * + * > 4.4.17.1 The name of the file, with optional relative path. + * > + * > The path stored MUST NOT contain a drive or device letter, or a leading + * > slash. All slashes MUST be forward slashes '/' as opposed to backwards + * > slashes '\' for compatibility with Amiga and UNIX file systems etc. If + * > input came from standard input, there is no file name field. + * > + * > https://pkware.cachefly.net/webdocs/casestudies/APPNOTE.TXT */ export type ZipItem = [zipPath: string, entryName: string]; @@ -652,8 +698,10 @@ export interface PendingUploads { * This is name of the collection (when uploading to a singular collection) * or the root collection (when uploading to separate * albums) to which we * these uploads are meant to go to. See {@link CollectionMapping}. + * + * It will not be set if we're just uploading standalone files. */ - collectionName: string; + collectionName?: string; /** * Paths of regular files that need to be uploaded. */ diff --git a/web/packages/shared/components/Navbar/base.tsx b/web/packages/shared/components/Navbar/base.tsx index 101506cfd089a268cc1ec0dd8c1d41c23bd0b274..403dc808ca3d7ff7900f06009e25c06ea1296f47 100644 --- a/web/packages/shared/components/Navbar/base.tsx +++ b/web/packages/shared/components/Navbar/base.tsx @@ -1,6 +1,9 @@ import { styled } from "@mui/material"; import { FlexWrapper } from "../../components/Container"; -const NavbarBase = styled(FlexWrapper)<{ isMobile: boolean }>` + +const NavbarBase = styled(FlexWrapper, { + shouldForwardProp: (propName) => propName != "isMobile", +})<{ isMobile: boolean }>` min-height: 64px; position: sticky; top: 0; diff --git a/web/packages/shared/hooks/useFileInput.tsx b/web/packages/shared/hooks/useFileInput.tsx index 4eb346d39ce96f316b7be5543d8fedafed7e78ee..88c247ecc1ae109f33561091b1bcda198103a727 100644 --- a/web/packages/shared/hooks/useFileInput.tsx +++ b/web/packages/shared/hooks/useFileInput.tsx @@ -1,123 +1,100 @@ import { useCallback, useRef, useState } from "react"; -/** - * [Note: File paths when running under Electron] - * - * We have access to the absolute path of the web {@link File} object when we - * are running in the context of our desktop app. - * - * https://www.electronjs.org/docs/latest/api/file-object - * - * This is in contrast to the `webkitRelativePath` that we get when we're - * running in the browser, which is the relative path to the directory that the - * user selected (or just the name of the file if the user selected or - * drag/dropped a single one). - * - * Note that this is a deprecated approach. From Electron docs: - * - * > Warning: The path property that Electron adds to the File interface is - * > deprecated and will be removed in a future Electron release. We recommend - * > you use `webUtils.getPathForFile` instead. - */ -export interface FileWithPath extends File { - readonly path?: string; -} - interface UseFileInputParams { + /** + * If `true`, the file open dialog will ask the user to select directories. + * Otherwise it'll ask the user to select files (default). + */ directory?: boolean; + /** + * If specified, it'll restrict the type of files that the user can select + * by setting the "accept" attribute of the underlying HTML input element we + * use to surface the file selector dialog. For value of accept can be an + * extension or a MIME type (See + * https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/accept). + */ accept?: string; } +interface UseFileInputResult { + /** + * A function to call to get the properties that should be passed to a dummy + * `input` element that needs to be created to anchor the select file + * dialog. This input HTML element is not going to be visible, but it needs + * to be part of the DOM for {@link openSelector} to have effect. + */ + getInputProps: () => React.HTMLAttributes; + /** + * A function that can be called to open the select file / directory dialog. + */ + openSelector: () => void; + /** + * The list of {@link File}s that the user selected. + * + * This will be a list even if the user selected directories - in that case, + * it will be the recursive list of files within this directory. + */ + selectedFiles: File[]; +} + /** - * Return three things: + * Wrap a open file selector into an easy to use package. * - * - A function that can be called to trigger the showing of the select file / - * directory dialog. + * Returns a {@link UseFileInputResult} which contains a function to get the + * props for an input element, a function to open the file selector, and the + * list of selected files. * - * - The list of properties that should be passed to a dummy `input` element - * that needs to be created to anchor the select file dialog. This input HTML - * element is not going to be visible, but it needs to be part of the DOM fro - * the open trigger to have effect. - * - * - The list of files that the user selected. This will be a list even if the - * user selected directories - in that case, it will be the recursive list of - * files within this directory. - * - * @param param0 - * - * - If {@link directory} is true, the file open dialog will ask the user to - * select directories. Otherwise it'll ask the user to select files. - * - * - If {@link accept} is specified, it'll restrict the type of files that the - * user can select by setting the "accept" attribute of the underlying HTML - * input element we use to surface the file selector dialog. For value of - * accept can be an extension or a MIME type (See - * https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/accept). + * See the documentation of {@link UseFileInputParams} and + * {@link UseFileInputResult} for more details. */ -export default function useFileInput({ +export const useFileInput = ({ directory, accept, -}: UseFileInputParams) { +}: UseFileInputParams): UseFileInputResult => { const [selectedFiles, setSelectedFiles] = useState([]); const inputRef = useRef(); - const openSelectorDialog = useCallback(() => { + const openSelector = useCallback(() => { if (inputRef.current) { inputRef.current.value = null; inputRef.current.click(); } }, []); - const handleChange: React.ChangeEventHandler = async ( + const handleChange: React.ChangeEventHandler = ( event, ) => { - if (!!event.target && !!event.target.files) { - const files = [...event.target.files].map((file) => - toFileWithPath(file), - ); - setSelectedFiles(files); - } + const files = event.target?.files; + if (files) setSelectedFiles([...files]); }; + // [Note: webkitRelativePath] + // + // If the webkitdirectory attribute of an HTML element is set then + // the File objects that we get will have `webkitRelativePath` property + // containing the relative path to the selected directory. + // + // https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/webkitdirectory + // + // These paths use the POSIX path separator ("/"). + // https://stackoverflow.com/questions/62806233/when-using-webkitrelativepath-is-the-path-separator-operating-system-specific + // + const directoryOpts = directory + ? { directory: "", webkitdirectory: "" } + : {}; + const getInputProps = useCallback( () => ({ type: "file", multiple: true, style: { display: "none" }, - ...(directory ? { directory: "", webkitdirectory: "" } : {}), + ...directoryOpts, ref: inputRef, onChange: handleChange, ...(accept ? { accept } : {}), }), - [], + [directoryOpts, accept, handleChange], ); - return { - getInputProps, - open: openSelectorDialog, - selectedFiles: selectedFiles, - }; -} - -// https://github.com/react-dropzone/file-selector/blob/master/src/file.ts#L88 -export function toFileWithPath(file: File, path?: string): FileWithPath { - if (typeof (file as any).path !== "string") { - // on electron, path is already set to the absolute path - const { webkitRelativePath } = file; - Object.defineProperty(file, "path", { - value: - typeof path === "string" - ? path - : typeof webkitRelativePath === "string" && // If is set, - // the File will have a {webkitRelativePath} property - // https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement/webkitdirectory - webkitRelativePath.length > 0 - ? webkitRelativePath - : file.name, - writable: false, - configurable: false, - enumerable: true, - }); - } - return file; -} + return { getInputProps, openSelector, selectedFiles }; +}; diff --git a/web/packages/shared/network/cast.ts b/web/packages/shared/network/cast.ts index b240eab32db650889cb4a44612feeda036471ed0..a18767baa2ada6a03650849d38a7777410d888c8 100644 --- a/web/packages/shared/network/cast.ts +++ b/web/packages/shared/network/cast.ts @@ -58,11 +58,14 @@ class CastGateway { return resp.data.publicKey; } - public async registerDevice(code: string, publicKey: string) { - await HTTPService.post(getEndpoint() + "/cast/device-info/", { - deviceCode: `${code}`, - publicKey: publicKey, - }); + public async registerDevice(publicKey: string): Promise { + const resp = await HTTPService.post( + getEndpoint() + "/cast/device-info/", + { + publicKey: publicKey, + }, + ); + return resp.data.deviceCode; } public async publishCastPayload( diff --git a/web/packages/shared/storage/localStorage/index.ts b/web/packages/shared/storage/localStorage/index.ts index 70b9687cdc5ecbdab91ff6428943cb7d21392bbe..c6ec3f57f4978cf74e6906c87da1fd1cd8ee1dd3 100644 --- a/web/packages/shared/storage/localStorage/index.ts +++ b/web/packages/shared/storage/localStorage/index.ts @@ -7,7 +7,6 @@ export enum LS_KEYS { ORIGINAL_KEY_ATTRIBUTES = "originalKeyAttributes", SUBSCRIPTION = "subscription", FAMILY_DATA = "familyData", - PLANS = "plans", IS_FIRST_LOGIN = "isFirstLogin", JUST_SIGNED_UP = "justSignedUp", SHOW_BACK_BUTTON = "showBackButton", diff --git a/web/packages/utils/array.ts b/web/packages/utils/array.ts new file mode 100644 index 0000000000000000000000000000000000000000..10030b189e9b97911fb496e7868b3463cb3309c7 --- /dev/null +++ b/web/packages/utils/array.ts @@ -0,0 +1,30 @@ +/** + * Shuffle. + * + * Return a new array containing the shuffled elements of the given array. + * + * The algorithm used is not the most efficient, but is effectively a one-liner + * whilst being reasonably efficient. To each element we assign a random key, + * then we sort by this key. Since the key is random, the sorted array will have + * the original elements in a random order. + */ +export const shuffled = (xs: T[]) => + xs + .map((x) => [Math.random(), x]) + .sort() + .map(([, x]) => x) as T[]; + +/** + * Return the first non-empty string from the given list of strings. + * + * This function is needed because the `a ?? b` idiom doesn't do what you'd + * expect when a is "". Perhaps the behaviour is wrong, perhaps the expecation + * is wrong; this function papers over the differences. + * + * If none of the strings are non-empty, or if there are no strings in the given + * array, return undefined. + */ +export const firstNonEmpty = (ss: (string | undefined)[]) => { + for (const s of ss) if (s && s.length > 0) return s; + return undefined; +}; diff --git a/web/packages/utils/ensure.ts b/web/packages/utils/ensure.ts index 761cedc992d0b5712c50a40098aa6585e1daadf7..93706bfb61c15aea0a497ecfb23ada494eef6daf 100644 --- a/web/packages/utils/ensure.ts +++ b/web/packages/utils/ensure.ts @@ -1,7 +1,8 @@ /** - * Throw an exception if the given value is undefined. + * Throw an exception if the given value is `null` or `undefined`. */ -export const ensure = (v: T | undefined): T => { +export const ensure = (v: T | null | undefined): T => { + if (v === null) throw new Error("Required value was null"); if (v === undefined) throw new Error("Required value was not found"); return v; }; diff --git a/web/yarn.lock b/web/yarn.lock index 6886647d7a453768e626e61ede6c71836c525433..2a50e3f95e8a1ad7ae210647d9bacbadb31472b3 100644 --- a/web/yarn.lock +++ b/web/yarn.lock @@ -528,7 +528,7 @@ dependencies: eslint-visitor-keys "^3.3.0" -"@eslint-community/regexpp@^4.5.1", "@eslint-community/regexpp@^4.6.1": +"@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== @@ -1000,6 +1000,11 @@ "@types/node" "*" base-x "^3.0.6" +"@types/chromecast-caf-receiver@^6.0.14": + version "6.0.14" + resolved "https://registry.yarnpkg.com/@types/chromecast-caf-receiver/-/chromecast-caf-receiver-6.0.14.tgz#e1e781c62c84ee85899fd20d658e258f8f45f5be" + integrity sha512-qvN4uE4MlYCEtniTtjxG4D+KeEXfs/Sgqex9sSZdPVh5rffdifINYzKH3z3QRl+0mk41vD6vYZ8s8ZfW/8iFoQ== + "@types/estree@1.0.5": version "1.0.5" resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4" @@ -1018,7 +1023,7 @@ "@types/react" "*" hoist-non-react-statics "^3.3.0" -"@types/json-schema@^7.0.12": +"@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== @@ -1134,10 +1139,10 @@ resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.8.tgz#ce5ace04cfeabe7ef87c0091e50752e36707deff" integrity sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A== -"@types/semver@^7.5.0": - version "7.5.7" - resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.7.tgz#326f5fdda70d13580777bcaa1bc6fa772a5aef0e" - integrity sha512-/wdoPq1QqkSj9/QOeKkFquEuPzQbHTWAMPH/PaUMB+JuR31lXhlWXRZ52IpfDYVlDOUBvX09uBrPwxGT1hjNBg== +"@types/semver@^7.5.8": + version "7.5.8" + resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.8.tgz#8268a8c57a3e4abd25c165ecd36237db7948a55e" + integrity sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ== "@types/uuid@^9.0.2": version "9.0.8" @@ -1150,21 +1155,21 @@ integrity sha512-Tuk4q7q0DnpzyJDI4aMeghGuFu2iS1QAdKpabn8JfbtfGmVDUgvZv1I7mEjP61Bvnp3ljKCC8BE6YYSTNxmvRQ== "@typescript-eslint/eslint-plugin@^7": - version "7.0.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.0.2.tgz#c13a34057be425167cc4a765158c46fdf2fd981d" - integrity sha512-/XtVZJtbaphtdrWjr+CJclaCVGPtOdBpFEnvtNf/jRV0IiEemRrL0qABex/nEt8isYcnFacm3nPHYQwL+Wb7qg== - dependencies: - "@eslint-community/regexpp" "^4.5.1" - "@typescript-eslint/scope-manager" "7.0.2" - "@typescript-eslint/type-utils" "7.0.2" - "@typescript-eslint/utils" "7.0.2" - "@typescript-eslint/visitor-keys" "7.0.2" + 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== + 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" debug "^4.3.4" graphemer "^1.4.0" - ignore "^5.2.4" + ignore "^5.3.1" natural-compare "^1.4.0" - semver "^7.5.4" - ts-api-utils "^1.0.1" + semver "^7.6.0" + ts-api-utils "^1.3.0" "@typescript-eslint/parser@^5.4.2 || ^6.0.0": version "6.21.0" @@ -1178,14 +1183,14 @@ debug "^4.3.4" "@typescript-eslint/parser@^7": - version "7.0.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-7.0.2.tgz#95c31233d343db1ca1df8df7811b5b87ca7b1a68" - integrity sha512-GdwfDglCxSmU+QTS9vhz2Sop46ebNCXpPPvsByK7hu0rFGRHL+AusKQJ7SoN+LbLh6APFpQwHKmDSwN35Z700Q== - dependencies: - "@typescript-eslint/scope-manager" "7.0.2" - "@typescript-eslint/types" "7.0.2" - "@typescript-eslint/typescript-estree" "7.0.2" - "@typescript-eslint/visitor-keys" "7.0.2" + version "7.8.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-7.8.0.tgz#1e1db30c8ab832caffee5f37e677dbcb9357ddc8" + integrity sha512-KgKQly1pv0l4ltcftP59uQZCi4HUYswCLbTqVZEJu7uLX8CTLyswqMLqLN+2QFz4jCptqWVV4SB7vdxcH2+0kQ== + 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" debug "^4.3.4" "@typescript-eslint/scope-manager@6.21.0": @@ -1196,33 +1201,33 @@ "@typescript-eslint/types" "6.21.0" "@typescript-eslint/visitor-keys" "6.21.0" -"@typescript-eslint/scope-manager@7.0.2": - version "7.0.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-7.0.2.tgz#6ec4cc03752758ddd1fdaae6fbd0ed9a2ca4fe63" - integrity sha512-l6sa2jF3h+qgN2qUMjVR3uCNGjWw4ahGfzIYsCtFrQJCjhbrDPdiihYT8FnnqFwsWX+20hK592yX9I2rxKTP4g== +"@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== dependencies: - "@typescript-eslint/types" "7.0.2" - "@typescript-eslint/visitor-keys" "7.0.2" + "@typescript-eslint/types" "7.8.0" + "@typescript-eslint/visitor-keys" "7.8.0" -"@typescript-eslint/type-utils@7.0.2": - version "7.0.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-7.0.2.tgz#a7fc0adff0c202562721357e7478207d380a757b" - integrity sha512-IKKDcFsKAYlk8Rs4wiFfEwJTQlHcdn8CLwLaxwd6zb8HNiMcQIFX9sWax2k4Cjj7l7mGS5N1zl7RCHOVwHq2VQ== +"@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== dependencies: - "@typescript-eslint/typescript-estree" "7.0.2" - "@typescript-eslint/utils" "7.0.2" + "@typescript-eslint/typescript-estree" "7.8.0" + "@typescript-eslint/utils" "7.8.0" debug "^4.3.4" - ts-api-utils "^1.0.1" + ts-api-utils "^1.3.0" "@typescript-eslint/types@6.21.0": version "6.21.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-6.21.0.tgz#205724c5123a8fef7ecd195075fa6e85bac3436d" integrity sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg== -"@typescript-eslint/types@7.0.2": - version "7.0.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-7.0.2.tgz#b6edd108648028194eb213887d8d43ab5750351c" - integrity sha512-ZzcCQHj4JaXFjdOql6adYV4B/oFOFjPOC9XYwCaZFRvqN8Llfvv4gSxrkQkd2u4Ci62i2c6W6gkDwQJDaRc4nA== +"@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/typescript-estree@6.21.0": version "6.21.0" @@ -1238,32 +1243,32 @@ semver "^7.5.4" ts-api-utils "^1.0.1" -"@typescript-eslint/typescript-estree@7.0.2": - version "7.0.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-7.0.2.tgz#3c6dc8a3b9799f4ef7eca0d224ded01974e4cb39" - integrity sha512-3AMc8khTcELFWcKcPc0xiLviEvvfzATpdPj/DXuOGIdQIIFybf4DMT1vKRbuAEOFMwhWt7NFLXRkbjsvKZQyvw== +"@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== dependencies: - "@typescript-eslint/types" "7.0.2" - "@typescript-eslint/visitor-keys" "7.0.2" + "@typescript-eslint/types" "7.8.0" + "@typescript-eslint/visitor-keys" "7.8.0" debug "^4.3.4" globby "^11.1.0" is-glob "^4.0.3" - minimatch "9.0.3" - semver "^7.5.4" - ts-api-utils "^1.0.1" + minimatch "^9.0.4" + semver "^7.6.0" + ts-api-utils "^1.3.0" -"@typescript-eslint/utils@7.0.2": - version "7.0.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-7.0.2.tgz#8756123054cd934c8ba7db6a6cffbc654b10b5c4" - integrity sha512-PZPIONBIB/X684bhT1XlrkjNZJIEevwkKDsdwfiu1WeqBxYEEdIgVDgm8/bbKHVu+6YOpeRqcfImTdImx/4Bsw== +"@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== dependencies: "@eslint-community/eslint-utils" "^4.4.0" - "@types/json-schema" "^7.0.12" - "@types/semver" "^7.5.0" - "@typescript-eslint/scope-manager" "7.0.2" - "@typescript-eslint/types" "7.0.2" - "@typescript-eslint/typescript-estree" "7.0.2" - semver "^7.5.4" + "@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" + semver "^7.6.0" "@typescript-eslint/visitor-keys@6.21.0": version "6.21.0" @@ -1273,13 +1278,13 @@ "@typescript-eslint/types" "6.21.0" eslint-visitor-keys "^3.4.1" -"@typescript-eslint/visitor-keys@7.0.2": - version "7.0.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-7.0.2.tgz#2899b716053ad7094962beb895d11396fc12afc7" - integrity sha512-8Y+YiBmqPighbm5xA2k4wKTxRzx9EkBu7Rlw+WHqMvRJ3RPz/BMBO9b2ru0LUNmXg120PHUXD5+SWFy2R8DqlQ== +"@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== dependencies: - "@typescript-eslint/types" "7.0.2" - eslint-visitor-keys "^3.4.1" + "@typescript-eslint/types" "7.8.0" + eslint-visitor-keys "^3.4.3" "@ungap/structured-clone@^1.2.0": version "1.2.0" @@ -2498,12 +2503,12 @@ file-entry-cache@^6.0.1: dependencies: flat-cache "^3.0.4" -file-selector@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/file-selector/-/file-selector-0.4.0.tgz#59ec4f27aa5baf0841e9c6385c8386bef4d18b17" - integrity sha512-iACCiXeMYOvZqlF1kTiYINzgepRBymz1wwjiuup9u9nayhb6g4fSwiyJ/6adli+EPwrWtpgQAh2PoS7HukEGEg== +file-selector@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/file-selector/-/file-selector-0.6.0.tgz#fa0a8d9007b829504db4d07dd4de0310b65287dc" + integrity sha512-QlZ5yJC0VxHxQQsQhXvBaC7VRJ2uaxTf+Tfpu4Z/OcVQJVpZO+DGU0rkoVW5ce2SccxugvpBJoMvUs59iILYdw== dependencies: - tslib "^2.0.3" + tslib "^2.4.0" file-type@16.5.4: version "16.5.4" @@ -2893,7 +2898,7 @@ ieee754@^1.2.1: resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== -ignore@^5.2.0, ignore@^5.2.4: +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== @@ -3449,6 +3454,13 @@ minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: 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" + minimist@^1.2.0, minimist@^1.2.6: version "1.2.8" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" @@ -3875,13 +3887,13 @@ react-dom@^18: loose-envify "^1.1.0" scheduler "^0.23.0" -react-dropzone@^11.2.4: - version "11.7.1" - resolved "https://registry.yarnpkg.com/react-dropzone/-/react-dropzone-11.7.1.tgz#3851bb75b26af0bf1b17ce1449fd980e643b9356" - integrity sha512-zxCMwhfPy1olUEbw3FLNPLhAm/HnaYH5aELIEglRbqabizKAdHs0h+WuyOpmA+v1JXn0++fpQDdNfUagWt5hJQ== +react-dropzone@^14.2: + version "14.2.3" + resolved "https://registry.yarnpkg.com/react-dropzone/-/react-dropzone-14.2.3.tgz#0acab68308fda2d54d1273a1e626264e13d4e84b" + integrity sha512-O3om8I+PkFKbxCukfIR3QAGftYXDZfOE2N1mr/7qebQJHs7U+/RSL/9xomJNpRg9kM5h9soQSdf0Gc7OHF5Fug== dependencies: attr-accept "^2.2.2" - file-selector "^0.4.0" + file-selector "^0.6.0" prop-types "^15.8.1" react-fast-compare@^2.0.1: @@ -4173,7 +4185,7 @@ semver@^6.3.1: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== -semver@^7.5.4: +semver@^7.5.4, semver@^7.6.0: version "7.6.0" resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.0.tgz#1a46a4db4bffcccd97b743b5005c8325f23d4e2d" integrity sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg== @@ -4565,10 +4577,10 @@ truncate-utf8-bytes@^1.0.0: dependencies: utf8-byte-length "^1.0.1" -ts-api-utils@^1.0.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.2.1.tgz#f716c7e027494629485b21c0df6180f4d08f5e8b" - integrity sha512-RIYA36cJn2WiH9Hy77hdF9r7oEwxAtB/TS9/S4Qd90Ap4z5FSiin5zEiTL44OII1Y3IIlEvxwxFUVgrHSZ/UpA== +ts-api-utils@^1.0.1, 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== tsconfig-paths@^3.15.0: version "3.15.0" @@ -4580,7 +4592,7 @@ tsconfig-paths@^3.15.0: minimist "^1.2.6" strip-bom "^3.0.0" -tslib@^2.0.0, tslib@^2.0.3, tslib@^2.4.0, tslib@^2.6.2: +tslib@^2.0.0, tslib@^2.4.0, 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== @@ -4659,9 +4671,9 @@ typed-array-length@^1.0.6: possible-typed-array-names "^1.0.0" typescript@^5: - version "5.3.3" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.3.3.tgz#b3ce6ba258e72e6305ba66f5c9b452aaee3ffe37" - integrity sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw== + 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"