diff --git a/.github/assets/github-badge.png b/.github/assets/github-badge.png deleted file mode 100644 index ef3207c95d..0000000000 Binary files a/.github/assets/github-badge.png and /dev/null differ diff --git a/.github/workflows/auth-crowdin.yml b/.github/workflows/auth-crowdin.yml index f5b6744817..811def9396 100644 --- a/.github/workflows/auth-crowdin.yml +++ b/.github/workflows/auth-crowdin.yml @@ -28,7 +28,7 @@ jobs: base_path: "auth/" config: "auth/crowdin.yml" upload_sources: true - upload_translations: true + upload_translations: false download_translations: true localization_branch_name: crowdin-translations-auth create_pull_request: true diff --git a/.github/workflows/auth-lint.yml b/.github/workflows/auth-lint.yml index 72d7b39856..6504e0646a 100644 --- a/.github/workflows/auth-lint.yml +++ b/.github/workflows/auth-lint.yml @@ -9,7 +9,7 @@ on: - ".github/workflows/auth-lint.yml" env: - FLUTTER_VERSION: "3.16.9" + FLUTTER_VERSION: "3.19.3" jobs: lint: diff --git a/.github/workflows/auth-release.yml b/.github/workflows/auth-release.yml index bade58d258..da888901de 100644 --- a/.github/workflows/auth-release.yml +++ b/.github/workflows/auth-release.yml @@ -29,11 +29,11 @@ on: - "auth-v*" env: - FLUTTER_VERSION: "3.13.4" + FLUTTER_VERSION: "3.19.3" jobs: build-ubuntu: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 defaults: run: @@ -72,6 +72,8 @@ jobs: SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD }} - name: Build PlayStore AAB + # disable this step if release tag contains nightly or beta + if: startsWith(github.ref, 'refs/tags/auth-v') && !contains(github.ref, 'nightly') && !contains(github.ref, 'beta') run: | flutter build appbundle --release --flavor playstore --dart-define=app.flavor=playstore env: @@ -83,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 + 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 libffi7 - name: Install appimagetool run: | @@ -92,8 +94,6 @@ jobs: mv appimagetool /usr/local/bin/ - name: Build desktop app - # Temporarily disable desktop builds - if: false run: | flutter config --enable-linux-desktop dart pub global activate flutter_distributor @@ -118,6 +118,8 @@ jobs: updateOnlyUnreleased: true - name: Upload AAB to PlayStore + # disable this step if release tag contains nightly or beta + if: startsWith(github.ref, 'refs/tags/auth-v') && !contains(github.ref, 'nightly') && !contains(github.ref, 'beta') uses: r0adkll/upload-google-play@v1 with: serviceAccountJsonPlainText: ${{ secrets.SERVICE_ACCOUNT_JSON }} @@ -149,8 +151,6 @@ jobs: run: mkdir artifacts - name: Build Windows installer - # Temporarily disable desktop builds - if: false run: | flutter config --enable-windows-desktop dart pub global activate flutter_distributor @@ -159,13 +159,9 @@ jobs: mv dist/**/ente_auth-*-windows-setup.exe artifacts/ente-${{ github.ref_name }}-installer.exe - name: Retain Windows EXE and DLLs - # Temporarily disable desktop builds - if: false run: cp -r build/windows/x64/runner/Release ente-${{ github.ref_name }}-windows - name: Code sign Windows installer and EXE - # Temporarily disable desktop builds - if: false uses: dlemstra/code-sign-action@v1 with: certificate: "${{ secrets.WINDOWS_CERTIFICATE }}" @@ -175,9 +171,10 @@ jobs: auth/ente-${{ github.ref_name }}-windows/auth.exe - name: Zip Windows EXE and DLLs - # Temporarily disable desktop builds - if: false - run: tar.exe -a -c -f auth/artifacts/ente-${{ github.ref_name }}-windows.zip auth/ente-${{ github.ref_name }}-windows + run: tar.exe -a -c -f artifacts/ente-${{ github.ref_name }}-windows.zip ente-${{ github.ref_name }}-windows + + - name: Generate checksums + run: sha256sum artifacts/ente-* > artifacts/sha256sum-windows - name: Create a draft GitHub release uses: ncipollo/release-action@v1 @@ -248,8 +245,6 @@ jobs: run: mkdir artifacts - name: Build macOS DMG - # Temporarily disable desktop builds - if: false run: | flutter config --enable-macos-desktop dart pub global activate flutter_distributor @@ -257,16 +252,12 @@ jobs: mv dist/**/ente_auth-*-macos.dmg artifacts/ente-${{ github.ref_name }}.dmg - name: Code sign DMG - # Temporarily disable desktop builds - if: false run: | CERT_NAME=$(security find-identity -v -p codesigning | grep "Developer ID Application" | awk -F'"' '{print $2}' | grep -m1 "") codesign --force --timestamp --sign "$CERT_NAME" --options runtime artifacts/ente-${{ github.ref_name }}.dmg codesign --verify --verbose=4 artifacts/ente-${{ github.ref_name }}.dmg - name: Notarize and staple DMG - # Temporarily disable desktop builds - if: false run: | xcrun notarytool submit artifacts/ente-${{ github.ref_name }}.dmg \ --wait \ @@ -279,6 +270,9 @@ jobs: APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }} APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + - name: Generate checksums + run: shasum -a 256 artifacts/ente-* > artifacts/sha256sum-macos + - name: Create a draft GitHub release uses: ncipollo/release-action@v1 with: diff --git a/.github/workflows/mobile-crowdin.yml b/.github/workflows/mobile-crowdin.yml index 3770098968..5c52b59ad1 100644 --- a/.github/workflows/mobile-crowdin.yml +++ b/.github/workflows/mobile-crowdin.yml @@ -28,7 +28,7 @@ jobs: base_path: "mobile/" config: "mobile/crowdin.yml" upload_sources: true - upload_translations: true + upload_translations: false download_translations: true localization_branch_name: crowdin-translations-mobile create_pull_request: true diff --git a/.github/workflows/server-publish.yml b/.github/workflows/server-publish.yml new file mode 100644 index 0000000000..393fa899dd --- /dev/null +++ b/.github/workflows/server-publish.yml @@ -0,0 +1,38 @@ +name: "Publish (server)" + +on: + # Run manually, providing it the commit. + # + # To obtain the commit from the currently deployed museum, do: + # curl -s https://api.ente.io/ping | jq -r '.id' + # + # See server/docs/publish.md for more details. + workflow_dispatch: + inputs: + commit: + description: "Commit to publish the image from" + type: string + required: true + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + ref: ${{ inputs.commit }} + + - name: Build and push + uses: mr-smithers-excellent/docker-build-push@v6 + with: + dockerfile: server/Dockerfile + directory: server + # Resultant package name will be ghcr.io/ente-io/server + image: server + registry: ghcr.io + enableBuildKit: true + buildArgs: GIT_COMMIT=${{ inputs.commit }} + tags: ${{ inputs.commit }}, latest + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/server-release.yml b/.github/workflows/server-release.yml index 8f0281951a..fa02155300 100644 --- a/.github/workflows/server-release.yml +++ b/.github/workflows/server-release.yml @@ -7,11 +7,11 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - name: Check out code + - name: Checkout code + uses: actions/checkout@v4 - - uses: mr-smithers-excellent/docker-build-push@v6 - name: Build & Push + - name: Build and push + uses: mr-smithers-excellent/docker-build-push@v6 with: dockerfile: server/Dockerfile directory: server diff --git a/.github/workflows/web-crowdin.yml b/.github/workflows/web-crowdin.yml index 45a3fa4a59..f834e62f39 100644 --- a/.github/workflows/web-crowdin.yml +++ b/.github/workflows/web-crowdin.yml @@ -34,7 +34,7 @@ jobs: base_path: "web/" config: "web/crowdin.yml" upload_sources: true - upload_translations: true + upload_translations: false download_translations: true localization_branch_name: crowdin-translations-web create_pull_request: true diff --git a/.github/workflows/web-deploy-payments.yml b/.github/workflows/web-deploy-payments.yml new file mode 100644 index 0000000000..c428d88bc2 --- /dev/null +++ b/.github/workflows/web-deploy-payments.yml @@ -0,0 +1,43 @@ +name: "Deploy (payments)" + +on: + push: + # Run workflow on pushes to the deploy/payments + branches: [deploy/payments] + +jobs: + deploy: + runs-on: ubuntu-latest + + defaults: + run: + working-directory: web + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Setup node and enable yarn caching + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: "yarn" + cache-dependency-path: "docs/yarn.lock" + + - name: Install dependencies + run: yarn install + + - name: Build payments + run: yarn build:payments + + - name: Publish payments + uses: cloudflare/pages-action@1 + with: + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + projectName: ente + branch: deploy/payments + directory: web/apps/payments/out + wranglerVersion: "3" diff --git a/.github/workflows/web-nightly.yml b/.github/workflows/web-nightly.yml index a800a4b736..89d5ecaa5c 100644 --- a/.github/workflows/web-nightly.yml +++ b/.github/workflows/web-nightly.yml @@ -78,6 +78,19 @@ jobs: directory: web/apps/cast/out wranglerVersion: "3" + - name: Build payments + run: yarn build:payments + + - name: Publish payments + uses: cloudflare/pages-action@1 + with: + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + projectName: ente + branch: n-payments + directory: web/apps/payments/out + wranglerVersion: "3" + - name: Build photos run: yarn build:photos env: diff --git a/.github/workflows/web-preview.yml b/.github/workflows/web-preview.yml index 4e86d9a816..2ad73b7a1f 100644 --- a/.github/workflows/web-preview.yml +++ b/.github/workflows/web-preview.yml @@ -12,6 +12,7 @@ on: - "accounts" - "auth" - "cast" + - "payments" - "photos" jobs: diff --git a/README.md b/README.md index 68632ea3b4..88488d11ca 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,7 @@ existing users will be grandfathered in. [](https://apps.apple.com/app/id6444121398) [](https://play.google.com/store/apps/details?id=io.ente.auth) [](https://f-droid.org/packages/io.ente.auth/) -[](https://github.com/ente-io/ente/releases?q=tag%3Aauth-v2) +[](https://github.com/ente-io/ente/releases?q=tag%3Aauth-v2) [](https://auth.ente.io) diff --git a/auth/.gitignore b/auth/.gitignore index 3d6f77b840..87abd6a1c1 100644 --- a/auth/.gitignore +++ b/auth/.gitignore @@ -18,6 +18,11 @@ *.iws .idea/ +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + # Flutter/Dart/Pub related **/doc/api/ .dart_tool/ @@ -35,3 +40,4 @@ lib/generated_plugin_registrant.dart !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages android/key.properties +dist/ \ No newline at end of file diff --git a/auth/.metadata b/auth/.metadata index 42069fa1bb..39ee784e40 100644 --- a/auth/.metadata +++ b/auth/.metadata @@ -1,11 +1,11 @@ # 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. +# This file should be version controlled and should not be manually edited. version: - revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 - channel: unknown + revision: "ba393198430278b6595976de84fe170f553cc728" + channel: "[user-branch]" project_type: app @@ -13,17 +13,26 @@ project_type: app migration: platforms: - platform: root - create_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 - base_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 + create_revision: ba393198430278b6595976de84fe170f553cc728 + base_revision: ba393198430278b6595976de84fe170f553cc728 + - platform: android + create_revision: ba393198430278b6595976de84fe170f553cc728 + base_revision: ba393198430278b6595976de84fe170f553cc728 + - platform: ios + create_revision: ba393198430278b6595976de84fe170f553cc728 + base_revision: ba393198430278b6595976de84fe170f553cc728 - platform: linux - create_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 - base_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 + create_revision: ba393198430278b6595976de84fe170f553cc728 + base_revision: ba393198430278b6595976de84fe170f553cc728 - platform: macos - create_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 - base_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 + create_revision: ba393198430278b6595976de84fe170f553cc728 + base_revision: ba393198430278b6595976de84fe170f553cc728 + - platform: web + create_revision: ba393198430278b6595976de84fe170f553cc728 + base_revision: ba393198430278b6595976de84fe170f553cc728 - platform: windows - create_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 - base_revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851 + create_revision: ba393198430278b6595976de84fe170f553cc728 + base_revision: ba393198430278b6595976de84fe170f553cc728 # User provided section diff --git a/auth/README.md b/auth/README.md index 71ae1f93b4..e2e03f0230 100644 --- a/auth/README.md +++ b/auth/README.md @@ -31,14 +31,16 @@ You can alternatively install the build from PlayStore or F-Droid. +### Desktop + +You can [**download**](https://github.com/ente-io/ente/releases?q=tag%3Aauth-v2) +a native desktop app from this repository's GitHub releases. The desktop app +works on Windows, Linux and macOS. + ### Web You can view your 2FA codes at [auth.ente.io](https://auth.ente.io). For adding -or managing your secrets, please use our mobile app. - -### Desktop - -A native desktop app is coming soon! +or managing your secrets, please use our mobile or desktop app. ## 🧑‍💻 Build from source diff --git a/auth/android/app/build.gradle b/auth/android/app/build.gradle index 916e3b3c97..5621b08b6f 100644 --- a/auth/android/app/build.gradle +++ b/auth/android/app/build.gradle @@ -32,7 +32,7 @@ if (keystorePropertiesFile.exists()) { } android { - compileSdkVersion 33 + compileSdkVersion 34 sourceSets { main.java.srcDirs += 'src/main/kotlin' @@ -46,7 +46,7 @@ android { defaultConfig { applicationId "io.ente.auth" - minSdkVersion 20 + minSdkVersion 21 targetSdkVersion 33 versionCode flutterVersionCode.toInteger() versionName flutterVersionName @@ -56,11 +56,11 @@ android { signingConfigs { release { - storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : System.getenv("SIGNING_KEY_PATH") ? file(System.getenv("SIGNING_KEY_PATH")) : null - keyAlias keystoreProperties['keyAlias'] ? keystoreProperties['keyAlias'] : System.getenv("SIGNING_KEY_ALIAS") - keyPassword keystoreProperties['keyPassword'] ? keystoreProperties['keyPassword'] : System.getenv("SIGNING_KEY_PASSWORD") - storePassword keystoreProperties['storePassword'] ? keystoreProperties['storePassword'] : System.getenv("SIGNING_STORE_PASSWORD") - } + storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : System.getenv("SIGNING_KEY_PATH") ? file(System.getenv("SIGNING_KEY_PATH")) : null + keyAlias keystoreProperties['keyAlias'] ? keystoreProperties['keyAlias'] : System.getenv("SIGNING_KEY_ALIAS") + keyPassword keystoreProperties['keyPassword'] ? keystoreProperties['keyPassword'] : System.getenv("SIGNING_KEY_PASSWORD") + storePassword keystoreProperties['storePassword'] ? keystoreProperties['storePassword'] : System.getenv("SIGNING_STORE_PASSWORD") + } } flavorDimensions "default" @@ -109,6 +109,7 @@ dependencies { implementation 'io.sentry:sentry-android:2.0.0' implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation 'com.android.support:multidex:1.0.3' + implementation 'com.google.guava:guava:28.2-android' implementation 'com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava' testImplementation 'junit:junit:4.12' androidTestImplementation 'androidx.test:runner:1.1.1' diff --git a/auth/assets/custom-icons/_data/custom-icons.json b/auth/assets/custom-icons/_data/custom-icons.json index 489b2a7ec3..3e8f8ce679 100644 --- a/auth/assets/custom-icons/_data/custom-icons.json +++ b/auth/assets/custom-icons/_data/custom-icons.json @@ -36,7 +36,9 @@ }, { "title": "BorgBase", - "altNames": ["borg"], + "altNames": [ + "borg" + ], "slug": "BorgBase" }, { @@ -46,11 +48,17 @@ { "title": "Bybit" }, + { + "title": "CERN" + }, { "title": "Channel Island Hosting", "slug": "cih", "hex": "D14633" }, + { + "title": "ConfigCat" + }, { "title": "Cloudflare" }, @@ -62,6 +70,13 @@ { "title": "Crowdpear" }, + { + "title": "DCS", + "altNames": [ + "Digital Combat Simulator" + ], + "slug": "dcs" + }, { "title": "DEGIRO" }, @@ -107,9 +122,14 @@ }, { "title": "Gosuslugi", - "altNames": ["Госуслуги"], + "altNames": [ + "Госуслуги" + ], "slug": "Gosuslugi" }, + { + "title": "Habbo" + }, { "title": "Healthchecks.io", "slug": "healthchecks" @@ -172,13 +192,24 @@ }, { "title": "Mastodon", - "altNames": ["mstdn", "fediscience", "mathstodon", "fosstodon"], + "altNames": [ + "mstdn", + "fediscience", + "mathstodon", + "fosstodon" + ], "slug": "mastodon", "hex": "6364FF" }, + { + "title": "Mercado Livre", + "slug": "mercado_livre" + }, { "title": "Murena", - "altNames": ["eCloud"], + "altNames": [ + "eCloud" + ], "slug": "ecloud" }, { @@ -276,6 +307,9 @@ "slug": "rust_language_forum", "hex": "000000" }, + { + "title": "Sendgrid" + }, { "title": "service-bw" }, @@ -360,15 +394,24 @@ { "title": "Wise" }, + { + "title": "WYZE", + "slug": "wyze" + }, { "title": "X", - "altNames": ["twitter"], + "altNames": [ + "twitter" + ], "slug": "x" }, { "title": "Yandex", - "altNames": ["Ya", "Яндекс"], + "altNames": [ + "Ya", + "Яндекс" + ], "slug": "Yandex" } ] -} +} \ No newline at end of file diff --git a/auth/assets/custom-icons/icons/CERN.svg b/auth/assets/custom-icons/icons/CERN.svg new file mode 100644 index 0000000000..0e4b82e1f5 --- /dev/null +++ b/auth/assets/custom-icons/icons/CERN.svg @@ -0,0 +1,47 @@ + + + + + + diff --git a/auth/assets/custom-icons/icons/configcat.svg b/auth/assets/custom-icons/icons/configcat.svg new file mode 100644 index 0000000000..cfecd22b02 --- /dev/null +++ b/auth/assets/custom-icons/icons/configcat.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/auth/assets/custom-icons/icons/dcs.svg b/auth/assets/custom-icons/icons/dcs.svg new file mode 100644 index 0000000000..dd7c41ccb7 --- /dev/null +++ b/auth/assets/custom-icons/icons/dcs.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/auth/assets/custom-icons/icons/habbo.svg b/auth/assets/custom-icons/icons/habbo.svg new file mode 100644 index 0000000000..746bcdb229 --- /dev/null +++ b/auth/assets/custom-icons/icons/habbo.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/auth/assets/custom-icons/icons/mercado_livre.svg b/auth/assets/custom-icons/icons/mercado_livre.svg new file mode 100644 index 0000000000..7f4db5fd53 --- /dev/null +++ b/auth/assets/custom-icons/icons/mercado_livre.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/auth/assets/custom-icons/icons/sendgrid.svg b/auth/assets/custom-icons/icons/sendgrid.svg new file mode 100644 index 0000000000..1562adab90 --- /dev/null +++ b/auth/assets/custom-icons/icons/sendgrid.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/auth/assets/custom-icons/icons/wyze.svg b/auth/assets/custom-icons/icons/wyze.svg new file mode 100644 index 0000000000..89d252c154 --- /dev/null +++ b/auth/assets/custom-icons/icons/wyze.svg @@ -0,0 +1 @@ +Wyze \ No newline at end of file diff --git a/auth/assets/icon-light-adaptive-fg.png b/auth/assets/generation-icons/icon-light-adaptive-fg.png similarity index 100% rename from auth/assets/icon-light-adaptive-fg.png rename to auth/assets/generation-icons/icon-light-adaptive-fg.png diff --git a/auth/assets/icon-light.png b/auth/assets/generation-icons/icon-light.png similarity index 100% rename from auth/assets/icon-light.png rename to auth/assets/generation-icons/icon-light.png diff --git a/auth/assets/icons/auth-icon.ico b/auth/assets/icons/auth-icon.ico new file mode 100644 index 0000000000..38bb22bcf8 Binary files /dev/null and b/auth/assets/icons/auth-icon.ico differ diff --git a/auth/assets/icons/auth-icon.png b/auth/assets/icons/auth-icon.png new file mode 100644 index 0000000000..3db75740bb Binary files /dev/null and b/auth/assets/icons/auth-icon.png differ diff --git a/auth/distribute_options.yaml b/auth/distribute_options.yaml new file mode 100644 index 0000000000..9634f4cbda --- /dev/null +++ b/auth/distribute_options.yaml @@ -0,0 +1,25 @@ +output: dist/ + +releases: + - name: dev + jobs: + - name: release-dev-linux-zip + package: + platform: linux + target: zip + - name: release-dev-linux-deb + package: + platform: linux + target: deb + - name: release-dev-linux-appimage + package: + platform: linux + target: appimage + - name: release-dev-windows-exe + package: + platform: windows + target: exe + - name: release-dev-macos-dmg + package: + platform: macos + target: dmg diff --git a/auth/fastlane/metadata/android/en-US/full_description.txt b/auth/fastlane/metadata/android/en-US/full_description.txt index fbab88c75c..0db377761b 100644 --- a/auth/fastlane/metadata/android/en-US/full_description.txt +++ b/auth/fastlane/metadata/android/en-US/full_description.txt @@ -37,4 +37,4 @@ file, that adheres to the above format. SUPPORT If you need help, please reach out to support@ente.io, and a human will get in touch with you. -If you have feature requests, please create an issue @ https://github.com/ente-io/auth +If you have feature requests, please create an issue @ https://github.com/ente-io/ente diff --git a/auth/fastlane/metadata/android/en-US/title.txt b/auth/fastlane/metadata/android/en-US/title.txt index f1c54762a0..257e3ddcc6 100644 --- a/auth/fastlane/metadata/android/en-US/title.txt +++ b/auth/fastlane/metadata/android/en-US/title.txt @@ -1 +1 @@ -ente Authenticator \ No newline at end of file +Ente Authenticator \ No newline at end of file diff --git a/auth/fdroid_flutter_icons.yaml b/auth/fdroid_flutter_icons.yaml index da327160a9..0ef87effdd 100644 --- a/auth/fdroid_flutter_icons.yaml +++ b/auth/fdroid_flutter_icons.yaml @@ -1,6 +1,6 @@ -flutter_icons: - android: "launcher_icon" - image_path: "assets/icon-light.png" - adaptive_icon_foreground: "assets/icon-light-adaptive-fg.png" - adaptive_icon_background: "#ffffff" - +flutter_icons: + android: "launcher_icon" + image_path: "assets/generation-icons/icon-light.png" + adaptive_icon_foreground: "assets/generation-icons/icon-light-adaptive-fg.png" + adaptive_icon_background: "#ffffff" + diff --git a/auth/flutter b/auth/flutter index 367f9ea16b..ba39319843 160000 --- a/auth/flutter +++ b/auth/flutter @@ -1 +1 @@ -Subproject commit 367f9ea16bfae1ca451b9cc27c1366870b187ae2 +Subproject commit ba393198430278b6595976de84fe170f553cc728 diff --git a/auth/ios/Podfile.lock b/auth/ios/Podfile.lock index 9e781fd489..aec7e14f02 100644 --- a/auth/ios/Podfile.lock +++ b/auth/ios/Podfile.lock @@ -1,7 +1,9 @@ PODS: - - connectivity (0.0.1): + - app_links (0.0.1): - Flutter - - Reachability + - connectivity_plus (0.0.1): + - Flutter + - ReachabilitySwift - device_info_plus (0.0.1): - Flutter - DKImagePickerController/Core (4.3.4): @@ -45,27 +47,26 @@ PODS: - Flutter (1.0.0) - flutter_email_sender (0.0.1): - Flutter - - flutter_inappwebview (0.0.1): + - flutter_inappwebview_ios (0.0.1): - Flutter - - flutter_inappwebview/Core (= 0.0.1) + - flutter_inappwebview_ios/Core (= 0.0.1) - OrderedSet (~> 5.0) - - flutter_inappwebview/Core (0.0.1): + - flutter_inappwebview_ios/Core (0.0.1): - Flutter - OrderedSet (~> 5.0) + - flutter_local_authentication (1.2.0): + - Flutter - flutter_local_notifications (0.0.1): - Flutter - flutter_native_splash (0.0.1): - Flutter - flutter_secure_storage (6.0.0): - Flutter - - flutter_sodium (0.0.1): - - Flutter - fluttertoast (0.0.2): - Flutter - Toast - - FMDB (2.7.5): - - FMDB/standard (= 2.7.5) - - FMDB/standard (2.7.5) + - local_auth_darwin (0.0.1): + - Flutter - local_auth_ios (0.0.1): - Flutter - move_to_background (0.0.1): @@ -82,46 +83,65 @@ PODS: - qr_code_scanner (0.2.0): - Flutter - MTBBarcodeScanner - - Reachability (3.2) - - SDWebImage (5.17.0): - - SDWebImage/Core (= 5.17.0) - - SDWebImage/Core (5.17.0) - - Sentry/HybridSDK (8.9.1): - - SentryPrivate (= 8.9.1) + - ReachabilitySwift (5.2.1) + - SDWebImage (5.19.0): + - SDWebImage/Core (= 5.19.0) + - SDWebImage/Core (5.19.0) + - Sentry/HybridSDK (8.21.0): + - SentryPrivate (= 8.21.0) - sentry_flutter (0.0.1): - Flutter - FlutterMacOS - - Sentry/HybridSDK (= 8.9.1) - - SentryPrivate (8.9.1) + - Sentry/HybridSDK (= 8.21.0) + - SentryPrivate (8.21.0) - share_plus (0.0.1): - Flutter - shared_preferences_foundation (0.0.1): - Flutter - FlutterMacOS + - smart_auth (0.0.1): + - Flutter + - sodium_libs (2.2.1): + - Flutter - sqflite (0.0.3): - Flutter - - FMDB (>= 2.7.5) - - SwiftyGif (5.4.4) - - Toast (4.0.0) - - uni_links (0.0.1): + - FlutterMacOS + - sqlite3 (3.45.1): + - sqlite3/common (= 3.45.1) + - sqlite3/common (3.45.1) + - sqlite3/fts5 (3.45.1): + - sqlite3/common + - sqlite3/perf-threadsafe (3.45.1): + - sqlite3/common + - sqlite3/rtree (3.45.1): + - sqlite3/common + - sqlite3_flutter_libs (0.0.1): - Flutter + - sqlite3 (~> 3.45.1) + - sqlite3/fts5 + - sqlite3/perf-threadsafe + - sqlite3/rtree + - SwiftyGif (5.4.4) + - Toast (4.1.0) - url_launcher_ios (0.0.1): - Flutter DEPENDENCIES: - - connectivity (from `.symlinks/plugins/connectivity/ios`) + - app_links (from `.symlinks/plugins/app_links/ios`) + - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`) - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) - file_picker (from `.symlinks/plugins/file_picker/ios`) - file_saver (from `.symlinks/plugins/file_saver/ios`) - fk_user_agent (from `.symlinks/plugins/fk_user_agent/ios`) - Flutter (from `Flutter`) - flutter_email_sender (from `.symlinks/plugins/flutter_email_sender/ios`) - - flutter_inappwebview (from `.symlinks/plugins/flutter_inappwebview/ios`) + - flutter_inappwebview_ios (from `.symlinks/plugins/flutter_inappwebview_ios/ios`) + - flutter_local_authentication (from `.symlinks/plugins/flutter_local_authentication/ios`) - flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`) - flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`) - flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`) - - flutter_sodium (from `.symlinks/plugins/flutter_sodium/ios`) - fluttertoast (from `.symlinks/plugins/fluttertoast/ios`) + - local_auth_darwin (from `.symlinks/plugins/local_auth_darwin/darwin`) - local_auth_ios (from `.symlinks/plugins/local_auth_ios/ios`) - move_to_background (from `.symlinks/plugins/move_to_background/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) @@ -131,27 +151,31 @@ DEPENDENCIES: - sentry_flutter (from `.symlinks/plugins/sentry_flutter/ios`) - share_plus (from `.symlinks/plugins/share_plus/ios`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - - sqflite (from `.symlinks/plugins/sqflite/ios`) - - uni_links (from `.symlinks/plugins/uni_links/ios`) + - smart_auth (from `.symlinks/plugins/smart_auth/ios`) + - sodium_libs (from `.symlinks/plugins/sodium_libs/ios`) + - sqflite (from `.symlinks/plugins/sqflite/darwin`) + - sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/ios`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) SPEC REPOS: trunk: - DKImagePickerController - DKPhotoGallery - - FMDB - MTBBarcodeScanner - OrderedSet - - Reachability + - ReachabilitySwift - SDWebImage - Sentry - SentryPrivate + - sqlite3 - SwiftyGif - Toast EXTERNAL SOURCES: - connectivity: - :path: ".symlinks/plugins/connectivity/ios" + app_links: + :path: ".symlinks/plugins/app_links/ios" + connectivity_plus: + :path: ".symlinks/plugins/connectivity_plus/ios" device_info_plus: :path: ".symlinks/plugins/device_info_plus/ios" file_picker: @@ -164,18 +188,20 @@ EXTERNAL SOURCES: :path: Flutter flutter_email_sender: :path: ".symlinks/plugins/flutter_email_sender/ios" - flutter_inappwebview: - :path: ".symlinks/plugins/flutter_inappwebview/ios" + flutter_inappwebview_ios: + :path: ".symlinks/plugins/flutter_inappwebview_ios/ios" + flutter_local_authentication: + :path: ".symlinks/plugins/flutter_local_authentication/ios" flutter_local_notifications: :path: ".symlinks/plugins/flutter_local_notifications/ios" flutter_native_splash: :path: ".symlinks/plugins/flutter_native_splash/ios" flutter_secure_storage: :path: ".symlinks/plugins/flutter_secure_storage/ios" - flutter_sodium: - :path: ".symlinks/plugins/flutter_sodium/ios" fluttertoast: :path: ".symlinks/plugins/fluttertoast/ios" + local_auth_darwin: + :path: ".symlinks/plugins/local_auth_darwin/darwin" local_auth_ios: :path: ".symlinks/plugins/local_auth_ios/ios" move_to_background: @@ -194,50 +220,58 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/share_plus/ios" shared_preferences_foundation: :path: ".symlinks/plugins/shared_preferences_foundation/darwin" + smart_auth: + :path: ".symlinks/plugins/smart_auth/ios" + sodium_libs: + :path: ".symlinks/plugins/sodium_libs/ios" sqflite: - :path: ".symlinks/plugins/sqflite/ios" - uni_links: - :path: ".symlinks/plugins/uni_links/ios" + :path: ".symlinks/plugins/sqflite/darwin" + sqlite3_flutter_libs: + :path: ".symlinks/plugins/sqlite3_flutter_libs/ios" url_launcher_ios: :path: ".symlinks/plugins/url_launcher_ios/ios" SPEC CHECKSUMS: - connectivity: c4130b2985d4ef6fd26f9702e886bd5260681467 - device_info_plus: e5c5da33f982a436e103237c0c85f9031142abed + app_links: e70ca16b4b0f88253b3b3660200d4a10b4ea9795 + connectivity_plus: bf0076dd84a130856aa636df1c71ccaff908fa1d + device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6 DKImagePickerController: b512c28220a2b8ac7419f21c491fc8534b7601ac DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179 - file_picker: ce3938a0df3cc1ef404671531facef740d03f920 + file_picker: 15fd9539e4eb735dc54bae8c0534a7a9511a03de file_saver: 503e386464dbe118f630e17b4c2e1190fa0cf808 fk_user_agent: 1f47ec39291e8372b1d692b50084b0d54103c545 - Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 + Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 flutter_email_sender: 02d7443217d8c41483223627972bfdc09f74276b - flutter_inappwebview: acd4fc0f012cefd09015000c241137d82f01ba62 - flutter_local_notifications: 0c0b1ae97e741e1521e4c1629a459d04b9aec743 + flutter_inappwebview_ios: 97215cf7d4677db55df76782dbd2930c5e1c1ea0 + flutter_local_authentication: 1172a4dd88f6306dadce067454e2c4caf07977bb + flutter_local_notifications: 4cde75091f6327eb8517fa068a0a5950212d2086 flutter_native_splash: 52501b97d1c0a5f898d687f1646226c1f93c56ef flutter_secure_storage: 23fc622d89d073675f2eaa109381aefbcf5a49be - flutter_sodium: c84426b4de738514b5b66cfdeb8a06634e72fe0b fluttertoast: 31b00dabfa7fb7bacd9e7dbee580d7a2ff4bf265 - FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a - local_auth_ios: c6cf091ded637a88f24f86a8875d8b0f526e2605 + local_auth_darwin: c7e464000a6a89e952235699e32b329457608d98 + local_auth_ios: 5046a18c018dd973247a0564496c8898dbb5adf9 move_to_background: 39a5b79b26d577b0372cbe8a8c55e7aa9fcd3a2d MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c - package_info_plus: fd030dabf36271f146f1f3beacd48f564b0f17f7 - path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943 + package_info_plus: 115f4ad11e0698c8c1c5d8a689390df880f47e85 + path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c privacy_screen: 1a131c052ceb3c3659934b003b0d397c2381a24e qr_code_scanner: bb67d64904c3b9658ada8c402e8b4d406d5d796e - Reachability: 33e18b67625424e47b6cde6d202dce689ad7af96 - SDWebImage: 750adf017a315a280c60fde706ab1e552a3ae4e9 - Sentry: e3203780941722a1fcfee99e351de14244c7f806 - sentry_flutter: 8f0ffd53088e6a4d50c095852c5cad9e4405025c - SentryPrivate: 5e3683390f66611fc7c6215e27645873adb55d13 + ReachabilitySwift: 5ae15e16814b5f9ef568963fb2c87aeb49158c66 + SDWebImage: 981fd7e860af070920f249fd092420006014c3eb + Sentry: ebc12276bd17613a114ab359074096b6b3725203 + sentry_flutter: dff1df05dc39c83d04f9330b36360fc374574c5e + SentryPrivate: d651efb234cf385ec9a1cdd3eff94b5e78a0e0fe share_plus: c3fef564749587fc939ef86ffb283ceac0baf9f5 - shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126 - sqflite: 31f7eba61e3074736dff8807a9b41581e4f7f15a + shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695 + smart_auth: 4bedbc118723912d0e45a07e8ab34039c19e04f2 + sodium_libs: 1faae17af662384acbd13e41867a0008cd2e2318 + sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec + sqlite3: 73b7fc691fdc43277614250e04d183740cb15078 + sqlite3_flutter_libs: af0e8fe9bce48abddd1ffdbbf839db0302d72d80 SwiftyGif: 93a1cc87bf3a51916001cf8f3d63835fb64c819f - Toast: 91b396c56ee72a5790816f40d3a94dd357abc196 - uni_links: d97da20c7701486ba192624d99bffaaffcfc298a - url_launcher_ios: 08a3dfac5fb39e8759aeb0abbd5d9480f30fc8b4 + Toast: ec33c32b8688982cecc6348adeae667c1b9938da + url_launcher_ios: 6116280ddcfe98ab8820085d8d76ae7449447586 PODFILE CHECKSUM: b4e3a7eabb03395b66e81fc061789f61526ee6bb diff --git a/auth/ios/Runner.xcodeproj/project.pbxproj b/auth/ios/Runner.xcodeproj/project.pbxproj index 219a8441c0..4d77182b03 100644 --- a/auth/ios/Runner.xcodeproj/project.pbxproj +++ b/auth/ios/Runner.xcodeproj/project.pbxproj @@ -159,7 +159,7 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1430; + LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { 97C146ED1CF9000F007C117D = { diff --git a/auth/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/auth/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index a6b826db27..5e31d3d342 100644 --- a/auth/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/auth/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ Bool { GeneratedPluginRegistrant.register(with: self) - return super.application(application, didFinishLaunchingWithOptions: launchOptions) + + super.application(application, didFinishLaunchingWithOptions: launchOptions) + + if let url = AppLinks.shared.getLink(launchOptions: launchOptions) { + AppLinks.shared.handleLink(url: url) + } + + return false + + // return super.application(application, didFinishLaunchingWithOptions: launchOptions) } } diff --git a/auth/ios/Runner/Info.plist b/auth/ios/Runner/Info.plist index f3e756539b..35921ba0cc 100644 --- a/auth/ios/Runner/Info.plist +++ b/auth/ios/Runner/Info.plist @@ -78,5 +78,9 @@ UIViewControllerBasedStatusBarAppearance + LSSupportsOpeningDocumentsInPlace + + UIFileSharingEnabled + diff --git a/auth/lib/app/view/app.dart b/auth/lib/app/view/app.dart index b723e54b9b..3bd9d4e734 100644 --- a/auth/lib/app/view/app.dart +++ b/auth/lib/app/view/app.dart @@ -12,15 +12,18 @@ import 'package:ente_auth/locale.dart'; import "package:ente_auth/onboarding/view/onboarding_page.dart"; import 'package:ente_auth/services/update_service.dart'; import 'package:ente_auth/services/user_service.dart'; +import 'package:ente_auth/services/window_listener_service.dart'; import 'package:ente_auth/ui/home_page.dart'; import 'package:ente_auth/ui/settings/app_update_dialog.dart'; import 'package:flutter/foundation.dart'; import "package:flutter/material.dart"; import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:tray_manager/tray_manager.dart'; +import 'package:window_manager/window_manager.dart'; class App extends StatefulWidget { final Locale locale; - const App({Key? key, this.locale = const Locale("en")}) : super(key: key); + const App({super.key, this.locale = const Locale("en")}); static void setLocale(BuildContext context, Locale newLocale) { _AppState state = context.findAncestorStateOfType<_AppState>()!; @@ -31,7 +34,7 @@ class App extends StatefulWidget { State createState() => _AppState(); } -class _AppState extends State { +class _AppState extends State with WindowListener, TrayListener { late StreamSubscription _signedOutEvent; late StreamSubscription _signedInEvent; Locale? locale; @@ -41,8 +44,19 @@ class _AppState extends State { }); } + Future initWindowManager() async { + windowManager.addListener(this); + } + + Future initTrayManager() async { + trayManager.addListener(this); + } + @override void initState() { + initWindowManager(); + initTrayManager(); + _signedOutEvent = Bus.instance.on().listen((event) { if (mounted) { setState(() {}); @@ -76,6 +90,10 @@ class _AppState extends State { @override void dispose() { super.dispose(); + + windowManager.removeListener(this); + trayManager.removeListener(this); + _signedOutEvent.cancel(); _signedInEvent.cancel(); } @@ -134,4 +152,45 @@ class _AppState extends State { : const OnboardingPage(), }; } + + @override + void onWindowResize() { + WindowListenerService.instance.onWindowResize().ignore(); + } + + @override + void onTrayIconMouseDown() { + if (Platform.isWindows) { + windowManager.show(); + } else { + trayManager.popUpContextMenu(); + } + } + + @override + void onTrayIconRightMouseDown() { + if (Platform.isWindows) { + trayManager.popUpContextMenu(); + } else { + windowManager.show(); + } + } + + @override + void onTrayIconRightMouseUp() {} + + @override + void onTrayMenuItemClick(MenuItem menuItem) { + switch (menuItem.key) { + case 'hide_window': + windowManager.hide(); + break; + case 'show_window': + windowManager.show(); + break; + case 'exit_app': + windowManager.close(); + break; + } + } } diff --git a/auth/lib/app/view/app_route.dart b/auth/lib/app/view/app_route.dart deleted file mode 100644 index b3cf7bb425..0000000000 --- a/auth/lib/app/view/app_route.dart +++ /dev/null @@ -1,4 +0,0 @@ -class AppRoute { - static const String enterSecretKeyPage = "enterSecretKeyPage"; - static const String settings = "settings"; -} diff --git a/auth/lib/app/view/app_theme_extension.dart b/auth/lib/app/view/app_theme_extension.dart deleted file mode 100644 index 548be3d4f4..0000000000 --- a/auth/lib/app/view/app_theme_extension.dart +++ /dev/null @@ -1,163 +0,0 @@ -import "package:flutter/material.dart"; - -final lightTheme = ThemeData( - fontFamily: "Inter", - brightness: Brightness.light, - scaffoldBackgroundColor: Colors.white, - appBarTheme: const AppBarTheme().copyWith( - backgroundColor: Colors.white, - foregroundColor: Colors.black, - iconTheme: const IconThemeData(color: Colors.black), - elevation: 0, - ), - colorScheme: const ColorScheme.light( - primary: Colors.black, - ), - textTheme: _buildTextTheme(Colors.black), - outlinedButtonTheme: buildOutlinedButtonThemeData( - bgDisabled: Colors.grey.shade500, - bgEnabled: Colors.black, - fgDisabled: Colors.white, - fgEnabled: Colors.white, - ), - inputDecorationTheme: InputDecorationTheme( - fillColor: null, - filled: true, - contentPadding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 14, - ), - border: UnderlineInputBorder( - borderSide: BorderSide.none, - borderRadius: BorderRadius.circular(6), - ), - ), -); - -final darkTheme = ThemeData( - fontFamily: "Inter", - brightness: Brightness.dark, - scaffoldBackgroundColor: Colors.black, - appBarTheme: const AppBarTheme(color: Colors.orange), - textTheme: _buildTextTheme(Colors.white), - outlinedButtonTheme: buildOutlinedButtonThemeData( - bgDisabled: Colors.grey.shade500, - bgEnabled: Colors.white, - fgDisabled: Colors.white, - fgEnabled: Colors.black, - ), - inputDecorationTheme: InputDecorationTheme( - fillColor: null, - filled: true, - contentPadding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 14, - ), - border: UnderlineInputBorder( - borderSide: BorderSide.none, - borderRadius: BorderRadius.circular(6), - ), - ), colorScheme: const ColorScheme.dark(primary: Colors.white).copyWith(background: Colors.black), -); - -TextTheme _buildTextTheme(Color textColor) { - return const TextTheme().copyWith( - headlineMedium: TextStyle( - color: textColor, - fontSize: 32, - fontWeight: FontWeight.w700, - fontFamily: "Inter", - ), - headlineSmall: TextStyle( - color: textColor, - fontSize: 24, - fontWeight: FontWeight.w600, - fontFamily: "Inter", - ), - // AG: Body - titleLarge: TextStyle( - color: textColor, - fontSize: 18, - fontFamily: "Inter", - fontWeight: FontWeight.w500, - ), - // Use labels for buttons or notifications - labelMedium: TextStyle( - color: textColor, - fontFamily: "Inter", - fontSize: 18, - fontWeight: FontWeight.w700, - height: 28, - ), - - titleMedium: TextStyle( - color: textColor, - fontFamily: "Inter", - fontSize: 16, - fontWeight: FontWeight.w500, - ), - titleSmall: TextStyle( - color: textColor, - fontFamily: "Inter", - fontSize: 14, - fontWeight: FontWeight.w500, - ), - bodyLarge: TextStyle( - fontFamily: "Inter", - color: textColor, - fontSize: 16, - fontWeight: FontWeight.w500, - ), - bodyMedium: TextStyle( - fontFamily: "Inter", - color: textColor, - fontSize: 14, - fontWeight: FontWeight.w500, - ), - bodySmall: TextStyle( - color: textColor.withOpacity(0.6), - fontSize: 14, - fontWeight: FontWeight.w500, - ), - ); -} - -OutlinedButtonThemeData buildOutlinedButtonThemeData({ - required Color bgDisabled, - required Color bgEnabled, - required Color fgDisabled, - required Color fgEnabled, -}) { - return OutlinedButtonThemeData( - style: OutlinedButton.styleFrom( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - alignment: Alignment.center, - padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 50), - textStyle: const TextStyle( - fontWeight: FontWeight.w700, - fontFamily: "Inter", - fontSize: 18, - ), - ).copyWith( - backgroundColor: MaterialStateProperty.resolveWith( - (Set states) { - if (states.contains(MaterialState.disabled)) { - return bgDisabled; - } - return bgEnabled; - }, - ), - foregroundColor: MaterialStateProperty.resolveWith( - (Set states) { - if (states.contains(MaterialState.disabled)) { - return fgDisabled; - } - return fgEnabled; - }, - ), - alignment: Alignment.center, - ), - ); -} diff --git a/auth/lib/bootstrap.dart b/auth/lib/bootstrap.dart index e423ba7c8a..5f774e397c 100644 --- a/auth/lib/bootstrap.dart +++ b/auth/lib/bootstrap.dart @@ -1,3 +1,5 @@ +// ignore_for_file: deprecated_member_use + import "dart:async"; import "dart:developer"; diff --git a/auth/lib/core/configuration.dart b/auth/lib/core/configuration.dart index da3ad9ebf8..096fe91b2f 100644 --- a/auth/lib/core/configuration.dart +++ b/auth/lib/core/configuration.dart @@ -13,12 +13,12 @@ import 'package:ente_auth/models/key_attributes.dart'; import 'package:ente_auth/models/key_gen_result.dart'; import 'package:ente_auth/models/private_key_attributes.dart'; import 'package:ente_auth/store/authenticator_db.dart'; -import 'package:ente_auth/utils/crypto_util.dart'; +import 'package:ente_crypto_dart/ente_crypto_dart.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; -import 'package:flutter_sodium/flutter_sodium.dart'; import 'package:logging/logging.dart'; import 'package:path_provider/path_provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import 'package:sqflite_common_ffi/sqflite_ffi.dart'; import 'package:tuple/tuple.dart'; class Configuration { @@ -72,9 +72,10 @@ class Configuration { Future init() async { _preferences = await SharedPreferences.getInstance(); + sqfliteFfiInit(); _secureStorage = const FlutterSecureStorage(); _documentsDirectory = (await getApplicationDocumentsDirectory()).path; - _tempDirectory = _documentsDirectory + "/temp/"; + _tempDirectory = "$_documentsDirectory/temp/"; final tempDirectory = io.Directory(_tempDirectory); try { final currentTime = DateTime.now().microsecondsSinceEpoch; @@ -162,7 +163,7 @@ class Configuration { // decrypt the master key final kekSalt = CryptoUtil.getSaltToDeriveKey(); final derivedKeyResult = await CryptoUtil.deriveSensitiveKey( - utf8.encode(password) as Uint8List, + utf8.encode(password), kekSalt, ); final loginKey = await CryptoUtil.deriveLoginKey(derivedKeyResult.key); @@ -172,28 +173,28 @@ class Configuration { CryptoUtil.encryptSync(masterKey, derivedKeyResult.key); // Generate a public-private keypair and encrypt the latter - final keyPair = await CryptoUtil.generateKeyPair(); + final keyPair = CryptoUtil.generateKeyPair(); final encryptedSecretKeyData = - CryptoUtil.encryptSync(keyPair.sk, masterKey); + CryptoUtil.encryptSync(keyPair.secretKey.extractBytes(), masterKey); final attributes = KeyAttributes( - Sodium.bin2base64(kekSalt), - Sodium.bin2base64(encryptedKeyData.encryptedData!), - Sodium.bin2base64(encryptedKeyData.nonce!), - Sodium.bin2base64(keyPair.pk), - Sodium.bin2base64(encryptedSecretKeyData.encryptedData!), - Sodium.bin2base64(encryptedSecretKeyData.nonce!), + CryptoUtil.bin2base64(kekSalt), + CryptoUtil.bin2base64(encryptedKeyData.encryptedData!), + CryptoUtil.bin2base64(encryptedKeyData.nonce!), + CryptoUtil.bin2base64(keyPair.publicKey), + CryptoUtil.bin2base64(encryptedSecretKeyData.encryptedData!), + CryptoUtil.bin2base64(encryptedSecretKeyData.nonce!), derivedKeyResult.memLimit, derivedKeyResult.opsLimit, - Sodium.bin2base64(encryptedMasterKey.encryptedData!), - Sodium.bin2base64(encryptedMasterKey.nonce!), - Sodium.bin2base64(encryptedRecoveryKey.encryptedData!), - Sodium.bin2base64(encryptedRecoveryKey.nonce!), + CryptoUtil.bin2base64(encryptedMasterKey.encryptedData!), + CryptoUtil.bin2base64(encryptedMasterKey.nonce!), + CryptoUtil.bin2base64(encryptedRecoveryKey.encryptedData!), + CryptoUtil.bin2base64(encryptedRecoveryKey.nonce!), ); final privateAttributes = PrivateKeyAttributes( - Sodium.bin2base64(masterKey), - Sodium.bin2hex(recoveryKey), - Sodium.bin2base64(keyPair.sk), + CryptoUtil.bin2base64(masterKey), + CryptoUtil.bin2hex(recoveryKey), + CryptoUtil.bin2base64(keyPair.secretKey.extractBytes()), ); return KeyGenResult(attributes, privateAttributes, loginKey); } @@ -208,7 +209,7 @@ class Configuration { // decrypt the master key final kekSalt = CryptoUtil.getSaltToDeriveKey(); final derivedKeyResult = await CryptoUtil.deriveSensitiveKey( - utf8.encode(password) as Uint8List, + utf8.encode(password), kekSalt, ); final loginKey = await CryptoUtil.deriveLoginKey(derivedKeyResult.key); @@ -220,9 +221,9 @@ class Configuration { final existingAttributes = getKeyAttributes(); final updatedAttributes = existingAttributes!.copyWith( - kekSalt: Sodium.bin2base64(kekSalt), - encryptedKey: Sodium.bin2base64(encryptedKeyData.encryptedData!), - keyDecryptionNonce: Sodium.bin2base64(encryptedKeyData.nonce!), + kekSalt: CryptoUtil.bin2base64(kekSalt), + encryptedKey: CryptoUtil.bin2base64(encryptedKeyData.encryptedData!), + keyDecryptionNonce: CryptoUtil.bin2base64(encryptedKeyData.nonce!), memLimit: derivedKeyResult.memLimit, opsLimit: derivedKeyResult.opsLimit, ); @@ -240,8 +241,8 @@ class Configuration { }) async { _logger.info('Start decryptAndSaveSecrets'); keyEncryptionKey ??= await CryptoUtil.deriveKey( - utf8.encode(password) as Uint8List, - Sodium.base642bin(attributes.kekSalt), + utf8.encode(password), + CryptoUtil.base642bin(attributes.kekSalt), attributes.memLimit, attributes.opsLimit, ); @@ -250,31 +251,31 @@ class Configuration { Uint8List key; try { key = CryptoUtil.decryptSync( - Sodium.base642bin(attributes.encryptedKey), + CryptoUtil.base642bin(attributes.encryptedKey), keyEncryptionKey, - Sodium.base642bin(attributes.keyDecryptionNonce), + CryptoUtil.base642bin(attributes.keyDecryptionNonce), ); } catch (e) { _logger.severe('master-key failed, incorrect password?', e); throw Exception("Incorrect password"); } _logger.info("master-key done"); - await setKey(Sodium.bin2base64(key)); + await setKey(CryptoUtil.bin2base64(key)); final secretKey = CryptoUtil.decryptSync( - Sodium.base642bin(attributes.encryptedSecretKey), + CryptoUtil.base642bin(attributes.encryptedSecretKey), key, - Sodium.base642bin(attributes.secretKeyDecryptionNonce), + CryptoUtil.base642bin(attributes.secretKeyDecryptionNonce), ); _logger.info("secret-key done"); - await setSecretKey(Sodium.bin2base64(secretKey)); + await setSecretKey(CryptoUtil.bin2base64(secretKey)); final token = CryptoUtil.openSealSync( - Sodium.base642bin(getEncryptedToken()!), - Sodium.base642bin(attributes.publicKey), + CryptoUtil.base642bin(getEncryptedToken()!), + CryptoUtil.base642bin(attributes.publicKey), secretKey, ); _logger.info('appToken done'); await setToken( - Sodium.bin2base64(token, variant: Sodium.base64VariantUrlsafe), + CryptoUtil.bin2base64(token, urlSafe: true), ); return keyEncryptionKey; } @@ -293,28 +294,28 @@ class Configuration { Uint8List masterKey; try { masterKey = await CryptoUtil.decrypt( - Sodium.base642bin(attributes!.masterKeyEncryptedWithRecoveryKey), - Sodium.hex2bin(recoveryKey), - Sodium.base642bin(attributes.masterKeyDecryptionNonce), + CryptoUtil.base642bin(attributes!.masterKeyEncryptedWithRecoveryKey), + CryptoUtil.hex2bin(recoveryKey), + CryptoUtil.base642bin(attributes.masterKeyDecryptionNonce), ); } catch (e) { _logger.severe(e); rethrow; } - await setKey(Sodium.bin2base64(masterKey)); + await setKey(CryptoUtil.bin2base64(masterKey)); final secretKey = CryptoUtil.decryptSync( - Sodium.base642bin(attributes.encryptedSecretKey), + CryptoUtil.base642bin(attributes.encryptedSecretKey), masterKey, - Sodium.base642bin(attributes.secretKeyDecryptionNonce), + CryptoUtil.base642bin(attributes.secretKeyDecryptionNonce), ); - await setSecretKey(Sodium.bin2base64(secretKey)); + await setSecretKey(CryptoUtil.bin2base64(secretKey)); final token = CryptoUtil.openSealSync( - Sodium.base642bin(getEncryptedToken()!), - Sodium.base642bin(attributes.publicKey), + CryptoUtil.base642bin(getEncryptedToken()!), + CryptoUtil.base642bin(attributes.publicKey), secretKey, ); await setToken( - Sodium.bin2base64(token, variant: Sodium.base64VariantUrlsafe), + CryptoUtil.bin2base64(token, urlSafe: true), ); } @@ -407,27 +408,31 @@ class Configuration { } Uint8List? getKey() { - return _key == null ? null : Sodium.base642bin(_key!); + return _key == null ? null : CryptoUtil.base642bin(_key!); } Uint8List? getSecretKey() { - return _secretKey == null ? null : Sodium.base642bin(_secretKey!); + return _secretKey == null ? null : CryptoUtil.base642bin(_secretKey!); } Uint8List? getAuthSecretKey() { - return _authSecretKey == null ? null : Sodium.base642bin(_authSecretKey!); + return _authSecretKey == null + ? null + : CryptoUtil.base642bin(_authSecretKey!); } Uint8List? getOfflineSecretKey() { - return _offlineAuthKey == null ? null : Sodium.base642bin(_offlineAuthKey!); + return _offlineAuthKey == null + ? null + : CryptoUtil.base642bin(_offlineAuthKey!); } Uint8List getRecoveryKey() { final keyAttributes = getKeyAttributes()!; return CryptoUtil.decryptSync( - Sodium.base642bin(keyAttributes.recoveryKeyEncryptedWithMasterKey), + CryptoUtil.base642bin(keyAttributes.recoveryKeyEncryptedWithMasterKey), getKey()!, - Sodium.base642bin(keyAttributes.recoveryKeyDecryptionNonce), + CryptoUtil.base642bin(keyAttributes.recoveryKeyDecryptionNonce), ); } @@ -454,7 +459,7 @@ class Configuration { iOptions: _secureStorageOptionsIOS, ); } else { - _offlineAuthKey = Sodium.bin2base64(CryptoUtil.generateKey()); + _offlineAuthKey = CryptoUtil.bin2base64(CryptoUtil.generateKey()); await _secureStorage.write( key: offlineAuthSecretKey, value: _offlineAuthKey, diff --git a/auth/lib/core/constants.dart b/auth/lib/core/constants.dart index dcc111c4b0..5685220ac3 100644 --- a/auth/lib/core/constants.dart +++ b/auth/lib/core/constants.dart @@ -7,7 +7,8 @@ const String sentryDSN = "https://ed4ddd6309b847ba8849935e26e9b648@sentry.ente.io/9"; const String sentryTunnel = "https://sentry-reporter.ente.io"; const String roadmapURL = "https://roadmap.ente.io"; -const String githubDiscussionsUrl = "https://github.com/ente-io/ente/discussions"; +const String githubIssuesUrl = + "https://github.com/ente-io/ente/issues?q=is%3Aissue+is%3Aopen+sort%3Areactions-%2B1-desc"; const int microSecondsInDay = 86400000000; const int android11SDKINT = 30; const int galleryLoadStartTime = -8000000000000000; // Wednesday, March 6, 1748 diff --git a/auth/lib/core/errors.dart b/auth/lib/core/errors.dart index c1e83cde01..ba1310b6ca 100644 --- a/auth/lib/core/errors.dart +++ b/auth/lib/core/errors.dart @@ -1,9 +1,9 @@ class InvalidFileError extends ArgumentError { - InvalidFileError(String message) : super(message); + InvalidFileError(String super.message); } class InvalidFileUploadState extends AssertionError { - InvalidFileUploadState(String message) : super(message); + InvalidFileUploadState(String super.message); } class SubscriptionAlreadyClaimedError extends Error {} @@ -30,19 +30,15 @@ class UnauthorizedError extends Error {} class RequestCancelledError extends Error {} class InvalidSyncStatusError extends AssertionError { - InvalidSyncStatusError(String message) : super(message); + InvalidSyncStatusError(String super.message); } class UnauthorizedEditError extends AssertionError {} class InvalidStateError extends AssertionError { - InvalidStateError(String message) : super(message); + InvalidStateError(String super.message); } -class KeyDerivationError extends Error {} - -class LoginKeyDerivationError extends Error {} - class SrpSetupNotCompleteError extends Error {} class AuthenticatorKeyNotFound extends Error {} diff --git a/auth/lib/core/logging/super_logging.dart b/auth/lib/core/logging/super_logging.dart index 39a983874a..08c3f475ed 100644 --- a/auth/lib/core/logging/super_logging.dart +++ b/auth/lib/core/logging/super_logging.dart @@ -235,14 +235,14 @@ class SuperLogging { extraLines = null; } - final str = (config.prefix) + " " + rec.toPrettyString(extraLines); + final str = "${config.prefix} ${rec.toPrettyString(extraLines)}"; // write to stdout printLog(str); // push to log queue if (fileIsEnabled) { - fileQueueEntries.add(str + '\n'); + fileQueueEntries.add('$str\n'); if (fileQueueEntries.length == 1) { flushQueue(); } @@ -275,7 +275,7 @@ class SuperLogging { static var logChunkSize = 800; static void printLog(String text) { - text.chunked(logChunkSize).forEach(print); + text.chunked(logChunkSize).forEach(debugPrint); } /// A queue to be consumed by [setupSentry]. @@ -354,7 +354,7 @@ class SuperLogging { final date = config.dateFmt!.parse(basename(file.path)); dates[file as File] = date; files.add(file); - } on FormatException {} + } on Exception catch (_) {} } final nowTime = DateTime.now(); @@ -374,7 +374,7 @@ class SuperLogging { "deleting log file ${file.path}", ); await file.delete(); - } catch (ignore) {} + } on Exception catch (_) {} } } diff --git a/auth/lib/core/logging/tunneled_transport.dart b/auth/lib/core/logging/tunneled_transport.dart index 1191adc2e3..f9ffd5ad12 100644 --- a/auth/lib/core/logging/tunneled_transport.dart +++ b/auth/lib/core/logging/tunneled_transport.dart @@ -46,7 +46,7 @@ class TunneledTransport implements Transport { _options.logger( SentryLevel.error, 'API returned an error, statusCode = ${response.statusCode}, ' - 'body = ${response.body}', + 'body = ${response.body}', ); } return const SentryId.empty(); @@ -65,8 +65,8 @@ class TunneledTransport implements Transport { } Future _createStreamedRequest( - SentryEnvelope envelope, - ) async { + SentryEnvelope envelope, + ) async { final streamedRequest = StreamedRequest('POST', _tunnel); envelope .envelopeStream(_options) @@ -91,10 +91,10 @@ class _CredentialBuilder { _clock = clock; factory _CredentialBuilder( - Dsn? dsn, - String sdkIdentifier, - ClockProvider clock, - ) { + Dsn? dsn, + String sdkIdentifier, + ClockProvider clock, + ) { final authHeader = _buildAuthHeader( publicKey: dsn?.publicKey, secretKey: dsn?.secretKey, diff --git a/auth/lib/core/network.dart b/auth/lib/core/network.dart index 1942fcbb83..c14c9e758b 100644 --- a/auth/lib/core/network.dart +++ b/auth/lib/core/network.dart @@ -4,9 +4,10 @@ import 'package:dio/dio.dart'; import 'package:ente_auth/core/configuration.dart'; import 'package:ente_auth/core/event_bus.dart'; import 'package:ente_auth/events/endpoint_updated_event.dart'; +import 'package:ente_auth/utils/package_info_util.dart'; +import 'package:ente_auth/utils/platform_util.dart'; import 'package:fk_user_agent/fk_user_agent.dart'; import 'package:flutter/foundation.dart'; -import 'package:package_info_plus/package_info_plus.dart'; import 'package:uuid/uuid.dart'; int kConnectTimeout = 15000; @@ -16,34 +17,41 @@ class Network { late Dio _enteDio; Future init() async { - await FkUserAgent.init(); - final packageInfo = await PackageInfo.fromPlatform(); + if (PlatformUtil.isMobile()) await FkUserAgent.init(); + final packageInfo = await PackageInfoUtil().getPackageInfo(); + final version = PackageInfoUtil().getVersion(packageInfo); + final packageName = PackageInfoUtil().getPackageName(packageInfo); final endpoint = Configuration.instance.getHttpEndpoint(); - + _dio = Dio( BaseOptions( - connectTimeout: kConnectTimeout, + connectTimeout: Duration(milliseconds: kConnectTimeout), headers: { - HttpHeaders.userAgentHeader: FkUserAgent.userAgent, - 'X-Client-Version': packageInfo.version, - 'X-Client-Package': packageInfo.packageName, + HttpHeaders.userAgentHeader: PlatformUtil.isMobile() + ? FkUserAgent.userAgent + : Platform.operatingSystem, + 'X-Client-Version': version, + 'X-Client-Package': packageName, }, ), ); - + _enteDio = Dio( BaseOptions( baseUrl: endpoint, - connectTimeout: kConnectTimeout, + connectTimeout: Duration(milliseconds: kConnectTimeout), headers: { - HttpHeaders.userAgentHeader: FkUserAgent.userAgent, - 'X-Client-Version': packageInfo.version, - 'X-Client-Package': packageInfo.packageName, + if (PlatformUtil.isMobile()) + HttpHeaders.userAgentHeader: FkUserAgent.userAgent + else + HttpHeaders.userAgentHeader: Platform.operatingSystem, + 'X-Client-Version': version, + 'X-Client-Package': packageName, }, ), ); _setupInterceptors(endpoint); - + Bus.instance.on().listen((event) { final endpoint = Configuration.instance.getHttpEndpoint(); _enteDio.options.baseUrl = endpoint; diff --git a/auth/lib/ente_theme_data.dart b/auth/lib/ente_theme_data.dart index bb2a994c41..0316d014f9 100644 --- a/auth/lib/ente_theme_data.dart +++ b/auth/lib/ente_theme_data.dart @@ -5,12 +5,16 @@ import 'package:flutter/material.dart'; final lightThemeData = ThemeData( fontFamily: 'Inter', brightness: Brightness.light, + dividerTheme: const DividerThemeData( + color: Colors.black12, + ), hintColor: const Color.fromRGBO(158, 158, 158, 1), primaryColor: const Color.fromRGBO(255, 110, 64, 1), primaryColorLight: const Color.fromRGBO(0, 0, 0, 0.541), iconTheme: const IconThemeData(color: Colors.black), primaryIconTheme: const IconThemeData(color: Colors.red, opacity: 1.0, size: 50.0), + buttonTheme: const ButtonThemeData(), outlinedButtonTheme: buildOutlinedButtonThemeData( bgDisabled: const Color.fromRGBO(158, 158, 158, 1), bgEnabled: const Color.fromRGBO(0, 0, 0, 1), @@ -72,24 +76,42 @@ final lightThemeData = ThemeData( ? const Color.fromRGBO(255, 255, 255, 1) : const Color.fromRGBO(0, 0, 0, 1); }), - ), radioTheme: RadioThemeData( - fillColor: MaterialStateProperty.resolveWith((Set states) { - if (states.contains(MaterialState.disabled)) { return null; } - if (states.contains(MaterialState.selected)) { return const Color.fromRGBO(102, 187, 106, 1); } - return null; - }), - ), switchTheme: SwitchThemeData( - thumbColor: MaterialStateProperty.resolveWith((Set states) { - if (states.contains(MaterialState.disabled)) { return null; } - if (states.contains(MaterialState.selected)) { return const Color.fromRGBO(102, 187, 106, 1); } - return null; - }), - trackColor: MaterialStateProperty.resolveWith((Set states) { - if (states.contains(MaterialState.disabled)) { return null; } - if (states.contains(MaterialState.selected)) { return const Color.fromRGBO(102, 187, 106, 1); } - return null; - }), - ), colorScheme: const ColorScheme.light( + ), + radioTheme: RadioThemeData( + fillColor: + MaterialStateProperty.resolveWith((Set states) { + if (states.contains(MaterialState.disabled)) { + return null; + } + if (states.contains(MaterialState.selected)) { + return const Color.fromRGBO(102, 187, 106, 1); + } + return null; + }), + ), + switchTheme: SwitchThemeData( + thumbColor: + MaterialStateProperty.resolveWith((Set states) { + if (states.contains(MaterialState.disabled)) { + return null; + } + if (states.contains(MaterialState.selected)) { + return const Color.fromRGBO(102, 187, 106, 1); + } + return null; + }), + trackColor: + MaterialStateProperty.resolveWith((Set states) { + if (states.contains(MaterialState.disabled)) { + return null; + } + if (states.contains(MaterialState.selected)) { + return const Color.fromRGBO(102, 187, 106, 1); + } + return null; + }), + ), + colorScheme: const ColorScheme.light( primary: Colors.black, secondary: Color.fromARGB(255, 163, 163, 163), ).copyWith(background: const Color.fromRGBO(255, 255, 255, 1)), @@ -98,6 +120,9 @@ final lightThemeData = ThemeData( final darkThemeData = ThemeData( fontFamily: 'Inter', brightness: Brightness.dark, + dividerTheme: const DividerThemeData( + color: Colors.white12, + ), primaryColorLight: const Color.fromRGBO(255, 255, 255, 0.702), iconTheme: const IconThemeData(color: Colors.white), primaryIconTheme: @@ -105,6 +130,7 @@ final darkThemeData = ThemeData( hintColor: const Color.fromRGBO(158, 158, 158, 1), buttonTheme: const ButtonThemeData().copyWith( buttonColor: const Color.fromRGBO(45, 194, 98, 1.0), + height: 56, ), textTheme: _buildTextTheme(const Color.fromRGBO(255, 255, 255, 1)), outlinedButtonTheme: buildOutlinedButtonThemeData( @@ -164,24 +190,43 @@ final darkThemeData = ThemeData( return const Color.fromRGBO(158, 158, 158, 1); } }), - ), radioTheme: RadioThemeData( - fillColor: MaterialStateProperty.resolveWith((Set states) { - if (states.contains(MaterialState.disabled)) { return null; } - if (states.contains(MaterialState.selected)) { return const Color.fromRGBO(102, 187, 106, 1); } - return null; - }), - ), switchTheme: SwitchThemeData( - thumbColor: MaterialStateProperty.resolveWith((Set states) { - if (states.contains(MaterialState.disabled)) { return null; } - if (states.contains(MaterialState.selected)) { return const Color.fromRGBO(102, 187, 106, 1); } - return null; - }), - trackColor: MaterialStateProperty.resolveWith((Set states) { - if (states.contains(MaterialState.disabled)) { return null; } - if (states.contains(MaterialState.selected)) { return const Color.fromRGBO(102, 187, 106, 1); } - return null; - }), - ), colorScheme: const ColorScheme.dark(primary: Colors.white).copyWith(background: const Color.fromRGBO(0, 0, 0, 1)), + ), + radioTheme: RadioThemeData( + fillColor: + MaterialStateProperty.resolveWith((Set states) { + if (states.contains(MaterialState.disabled)) { + return null; + } + if (states.contains(MaterialState.selected)) { + return const Color.fromRGBO(102, 187, 106, 1); + } + return null; + }), + ), + switchTheme: SwitchThemeData( + thumbColor: + MaterialStateProperty.resolveWith((Set states) { + if (states.contains(MaterialState.disabled)) { + return null; + } + if (states.contains(MaterialState.selected)) { + return const Color.fromRGBO(102, 187, 106, 1); + } + return null; + }), + trackColor: + MaterialStateProperty.resolveWith((Set states) { + if (states.contains(MaterialState.disabled)) { + return null; + } + if (states.contains(MaterialState.selected)) { + return const Color.fromRGBO(102, 187, 106, 1); + } + return null; + }), + ), + colorScheme: const ColorScheme.dark(primary: Colors.white) + .copyWith(background: const Color.fromRGBO(0, 0, 0, 1)), ); TextTheme _buildTextTheme(Color textColor) { @@ -400,6 +445,7 @@ OutlinedButtonThemeData buildOutlinedButtonThemeData({ shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), + fixedSize: const Size.fromHeight(56), alignment: Alignment.center, padding: const EdgeInsets.fromLTRB(50, 16, 50, 16), textStyle: const TextStyle( @@ -436,7 +482,9 @@ ElevatedButtonThemeData buildElevatedButtonThemeData({ }) { return ElevatedButtonThemeData( style: ElevatedButton.styleFrom( - foregroundColor: onPrimary, backgroundColor: primary, elevation: elevation, + foregroundColor: onPrimary, + backgroundColor: primary, + elevation: elevation, alignment: Alignment.center, textStyle: const TextStyle( fontWeight: FontWeight.w600, diff --git a/auth/lib/gateway/authenticator.dart b/auth/lib/gateway/authenticator.dart index ee19c79b0d..3f3c7c8974 100644 --- a/auth/lib/gateway/authenticator.dart +++ b/auth/lib/gateway/authenticator.dart @@ -25,7 +25,7 @@ class AuthenticatorGateway { try { final response = await _enteDio.get("/authenticator/key"); return AuthKey.fromMap(response.data); - } on DioError catch (e) { + } on DioException catch (e) { if (e.response != null && (e.response!.statusCode ?? 0) == 404) { throw AuthenticatorKeyNotFound(); } else { @@ -90,7 +90,7 @@ class AuthenticatorGateway { } return authEntities; } catch (e) { - if (e is DioError && e.response?.statusCode == 401) { + if (e is DioException && e.response?.statusCode == 401) { throw UnauthorizedError(); } else { rethrow; diff --git a/auth/lib/l10n/arb/app_en.arb b/auth/lib/l10n/arb/app_en.arb index f540fc7176..a2f6e77edf 100644 --- a/auth/lib/l10n/arb/app_en.arb +++ b/auth/lib/l10n/arb/app_en.arb @@ -199,6 +199,10 @@ "recoveryKeySaveDescription": "We don't store this key, please save this 24 word key in a safe place.", "doThisLater": "Do this later", "saveKey": "Save key", + "save": "Save", + "send": "Send", + "saveOrSendDescription": "Do you want to save this to your storage (Downloads folder by default) or send it to other apps?", + "saveOnlyDescription": "Do you want to save this to your storage (Downloads folder by default)?", "back": "Back", "createAccount": "Create account", "passwordStrength": "Password strength: {passwordStrengthValue}", @@ -407,6 +411,7 @@ "doNotSignOut": "Do not sign out", "hearUsWhereTitle": "How did you hear about Ente? (optional)", "hearUsExplanation": "We don't track app installs. It'd help if you told us where you found us!", + "recoveryKeySaved": "Recovery key saved in Downloads folder!", "waitingForBrowserRequest": "Waiting for browser request...", "waitingForVerification": "Waiting for verification...", "passkey": "Passkey", diff --git a/auth/lib/l10n/arb/app_ko.arb b/auth/lib/l10n/arb/app_ko.arb new file mode 100644 index 0000000000..9e26dfeeb6 --- /dev/null +++ b/auth/lib/l10n/arb/app_ko.arb @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/auth/lib/l10n/arb/app_nl.arb b/auth/lib/l10n/arb/app_nl.arb index 0ac2b37913..ad17282041 100644 --- a/auth/lib/l10n/arb/app_nl.arb +++ b/auth/lib/l10n/arb/app_nl.arb @@ -144,6 +144,8 @@ "enterCodeHint": "Voer de 6-cijferige code van je verificatie-app in", "lostDeviceTitle": "Apparaat verloren?", "twoFactorAuthTitle": "Tweestapsverificatie", + "passkeyAuthTitle": "Passkey verificatie", + "verifyPasskey": "Bevestig passkey", "recoverAccount": "Account herstellen", "enterRecoveryKeyHint": "Voer je herstelsleutel in", "recover": "Herstellen", @@ -404,5 +406,15 @@ "signOutOtherDevices": "Afmelden bij andere apparaten", "doNotSignOut": "Niet uitloggen", "hearUsWhereTitle": "Hoe hoorde je over Ente? (optioneel)", - "hearUsExplanation": "Wij gebruiken geen tracking. Het zou helpen als je ons vertelt waar je ons gevonden hebt!" + "hearUsExplanation": "Wij gebruiken geen tracking. Het zou helpen als je ons vertelt waar je ons gevonden hebt!", + "waitingForBrowserRequest": "Wachten op browserverzoek...", + "waitingForVerification": "Wachten op verificatie...", + "passkey": "Passkey", + "developerSettingsWarning": "Weet u zeker dat u de ontwikkelaarsinstellingen wilt wijzigen?", + "developerSettings": "Ontwikkelaarsinstellingen", + "serverEndpoint": "Server eindpunt", + "invalidEndpoint": "Ongeldig eindpunt", + "invalidEndpointMessage": "Sorry, het eindpunt dat u hebt ingevoerd is ongeldig. Voer een geldig eindpunt in en probeer het opnieuw.", + "endpointUpdatedMessage": "Eindpunt met succes bijgewerkt", + "customEndpoint": "Verbonden met {endpoint}" } \ No newline at end of file diff --git a/auth/lib/l10n/arb/app_pl.arb b/auth/lib/l10n/arb/app_pl.arb index f36c690f7b..3221799d04 100644 --- a/auth/lib/l10n/arb/app_pl.arb +++ b/auth/lib/l10n/arb/app_pl.arb @@ -338,5 +338,22 @@ "deleteCodeAuthMessage": "Uwierzytelnij, aby usunąć kod", "showQRAuthMessage": "Uwierzytelnij, aby pokazać kod QR", "confirmAccountDeleteTitle": "Potwierdź usunięcie konta", - "confirmAccountDeleteMessage": "To konto jest połączone z innymi aplikacjami ente, jeśli ich używasz.\n\nTwoje przesłane dane, we wszystkich aplikacjach ente, zostaną zaplanowane do usunięcia, a Twoje konto zostanie trwale usunięte." + "confirmAccountDeleteMessage": "To konto jest połączone z innymi aplikacjami ente, jeśli ich używasz.\n\nTwoje przesłane dane, we wszystkich aplikacjach ente, zostaną zaplanowane do usunięcia, a Twoje konto zostanie trwale usunięte.", + "androidBiometricNotRecognized": "Nie rozpoznano. Spróbuj ponownie.", + "@androidBiometricNotRecognized": { + "description": "Message to let the user know that authentication was failed. It is used on Android side. Maximum 60 characters." + }, + "androidSignInTitle": "Wymagana autoryzacja", + "@androidSignInTitle": { + "description": "Message showed as a title in a dialog which indicates the user that they need to scan biometric to continue. It is used on Android side. Maximum 60 characters." + }, + "goToSettings": "Przejdź do Ustawień", + "@goToSettings": { + "description": "Message showed on a button that the user can click to go to settings pages from the current dialog. It is used on both Android and iOS side. Maximum 30 characters." + }, + "noInternetConnection": "Brak połączenia z Internetem", + "pleaseCheckYourInternetConnectionAndTryAgain": "Proszę sprawdzić połączenie internetowe i spróbować ponownie.", + "hearUsWhereTitle": "Jak usłyszałeś o Ente? (opcjonalnie)", + "waitingForVerification": "Oczekiwanie na weryfikację...", + "developerSettings": "Ustawienia deweloperskie" } \ No newline at end of file diff --git a/auth/lib/l10n/arb/app_pt.arb b/auth/lib/l10n/arb/app_pt.arb index bc43e84220..e6ca32893f 100644 --- a/auth/lib/l10n/arb/app_pt.arb +++ b/auth/lib/l10n/arb/app_pt.arb @@ -47,9 +47,9 @@ }, "copyEmailAction": "Copiar e-mail", "exportLogsAction": "Exportar logs", - "reportABug": "Reportar um bug", + "reportABug": "Reportar um problema", "crashAndErrorReporting": "Reporte de erros e falhas", - "reportBug": "Reportar bug", + "reportBug": "Reportar problema", "emailUsMessage": "Por favor, envie um e-mail para {email}", "@emailUsMessage": { "placeholders": { @@ -163,7 +163,7 @@ "invalidEmailMessage": "Por favor, insira um endereço de e-mail válido.", "deleteAccount": "Excluir conta", "deleteAccountQuery": "Sentiremos muito por vê-lo partir. Você está enfrentando algum problema?", - "yesSendFeedbackAction": "Sim, enviar feedback", + "yesSendFeedbackAction": "Sim, enviar comentário", "noDeleteAccountAction": "Não, excluir conta", "initiateAccountDeleteTitle": "Por favor, autentique-se para iniciar a exclusão de conta", "sendEmail": "Enviar e-mail", diff --git a/auth/lib/main.dart b/auth/lib/main.dart index 02d3e0c126..09b85d8b35 100644 --- a/auth/lib/main.dart +++ b/auth/lib/main.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'dart:io'; import 'package:adaptive_theme/adaptive_theme.dart'; -import 'package:computer/computer.dart'; import "package:ente_auth/app/view/app.dart"; import 'package:ente_auth/core/configuration.dart'; import 'package:ente_auth/core/constants.dart'; @@ -17,11 +16,14 @@ import 'package:ente_auth/services/preference_service.dart'; import 'package:ente_auth/services/update_service.dart'; import 'package:ente_auth/services/user_remote_flag_service.dart'; import 'package:ente_auth/services/user_service.dart'; +import 'package:ente_auth/services/window_listener_service.dart'; import 'package:ente_auth/store/code_store.dart'; import 'package:ente_auth/ui/tools/app_lock.dart'; import 'package:ente_auth/ui/tools/lock_screen.dart'; import 'package:ente_auth/ui/utils/icon_utils.dart'; -import 'package:ente_auth/utils/crypto_util.dart'; +import 'package:ente_auth/utils/platform_util.dart'; +import 'package:ente_auth/utils/window_protocol_handler.dart'; +import 'package:ente_crypto_dart/ente_crypto_dart.dart'; import 'package:flutter/foundation.dart'; import "package:flutter/material.dart"; import 'package:flutter/scheduler.dart'; @@ -29,11 +31,52 @@ import 'package:flutter_displaymode/flutter_displaymode.dart'; import 'package:logging/logging.dart'; import 'package:path_provider/path_provider.dart'; import 'package:privacy_screen/privacy_screen.dart'; +import 'package:tray_manager/tray_manager.dart'; +import 'package:window_manager/window_manager.dart'; final _logger = Logger("main"); +Future initSystemTray() async { + String path = Platform.isWindows + ? 'assets/icons/auth-icon.ico' + : 'assets/icons/auth-icon.png'; + await trayManager.setIcon(path); + Menu menu = Menu( + items: [ + MenuItem( + key: 'hide_window', + label: 'Hide Window', + ), + MenuItem( + key: 'show_window', + label: 'Show Window', + ), + MenuItem.separator(), + MenuItem( + key: 'exit_app', + label: 'Exit App', + ), + ], + ); + await trayManager.setContextMenu(menu); +} + void main() async { WidgetsFlutterBinding.ensureInitialized(); + + initSystemTray().ignore(); + + if (PlatformUtil.isDesktop()) { + await windowManager.ensureInitialized(); + await WindowListenerService.instance.init(); + WindowOptions windowOptions = WindowOptions( + size: WindowListenerService.instance.getWindowSize(), + ); + await windowManager.waitUntilReadyToShow(windowOptions, () async { + await windowManager.show(); + await windowManager.focus(); + }); + } await _runInForeground(); await _setupPrivacyScreen(); if (Platform.isAndroid) { @@ -70,10 +113,14 @@ ThemeMode _themeMode(AdaptiveThemeMode? savedThemeMode) { } Future _runWithLogs(Function() function, {String prefix = ""}) async { + String dir = ""; + try { + dir = "${(await getApplicationSupportDirectory()).path}/logs"; + } catch (_) {} await SuperLogging.main( LogConfig( body: function, - logDirPath: (await getApplicationSupportDirectory()).path + "/logs", + logDirPath: dir, maxLogFiles: 5, sentryDsn: sentryDSN, enableInDebugMode: true, @@ -82,10 +129,19 @@ Future _runWithLogs(Function() function, {String prefix = ""}) async { ); } +void _registerWindowsProtocol() { + const kWindowsScheme = 'ente'; + // Register our protocol only on Windows platform + if (!kIsWeb && Platform.isWindows) { + WindowsProtocolHandler() + .register(kWindowsScheme, executable: null, arguments: null); + } +} + Future _init(bool bool, {String? via}) async { - // Start workers asynchronously. No need to wait for them to start - Computer.shared().turnOn(workersCount: 4, verbose: kDebugMode).ignore(); - CryptoUtil.init(); + _registerWindowsProtocol(); + await initCryptoUtil(); + await PreferenceService.instance.init(); await CodeStore.instance.init(); await Configuration.instance.init(); @@ -100,6 +156,7 @@ Future _init(bool bool, {String? via}) async { } Future _setupPrivacyScreen() async { + if (!PlatformUtil.isMobile()) return; final brightness = SchedulerBinding.instance.platformDispatcher.platformBrightness; bool isInDarkMode = brightness == Brightness.dark; diff --git a/auth/lib/models/code.dart b/auth/lib/models/code.dart index 40c8c64552..7853eb19d0 100644 --- a/auth/lib/models/code.dart +++ b/auth/lib/models/code.dart @@ -57,14 +57,7 @@ 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, ); } @@ -83,35 +76,28 @@ class Code { Algorithm.sha1, Type.totp, 0, - "otpauth://totp/" + - issuer + - ":" + - account + - "?algorithm=SHA1&digits=6&issuer=" + - issuer + - "&period=30&secret=" + - secret, + "otpauth://totp/$issuer:$account?algorithm=SHA1&digits=6&issuer=$issuer&period=30&secret=$secret", ); } static Code fromRawData(String rawData) { Uri uri = Uri.parse(rawData); try { - return Code( - _getAccount(uri), - _getIssuer(uri), - _getDigits(uri), - _getPeriod(uri), - getSanitizedSecret(uri.queryParameters['secret']!), - _getAlgorithm(uri), - _getType(uri), - _getCounter(uri), - rawData, - ); - } catch(e) { + return Code( + _getAccount(uri), + _getIssuer(uri), + _getDigits(uri), + _getPeriod(uri), + getSanitizedSecret(uri.queryParameters['secret']!), + _getAlgorithm(uri), + _getType(uri), + _getCounter(uri), + rawData, + ); + } catch (e) { // if account name contains # without encoding, // rest of the url are treated as url fragment - if(rawData.contains("#")) { + if (rawData.contains("#")) { return Code.fromRawData(rawData.replaceAll("#", '%23')); } else { rethrow; @@ -141,7 +127,7 @@ class Code { if (uri.queryParameters.containsKey("issuer")) { String issuerName = uri.queryParameters['issuer']!; // Handle issuer name with period - // See https://github.com/ente-io/auth/pull/77 + // See https://github.com/ente-io/ente/pull/77 if (issuerName.contains("period=")) { return issuerName.substring(0, issuerName.indexOf("period=")); } diff --git a/auth/lib/models/derived_key_result.dart b/auth/lib/models/derived_key_result.dart deleted file mode 100644 index a071fb1f81..0000000000 --- a/auth/lib/models/derived_key_result.dart +++ /dev/null @@ -1,9 +0,0 @@ -import 'dart:typed_data'; - -class DerivedKeyResult { - final Uint8List key; - final int memLimit; - final int opsLimit; - - DerivedKeyResult(this.key, this.memLimit, this.opsLimit); -} diff --git a/auth/lib/models/encryption_result.dart b/auth/lib/models/encryption_result.dart deleted file mode 100644 index 9da16c573c..0000000000 --- a/auth/lib/models/encryption_result.dart +++ /dev/null @@ -1,15 +0,0 @@ -import 'dart:typed_data'; - -class EncryptionResult { - final Uint8List? encryptedData; - final Uint8List? key; - final Uint8List? header; - final Uint8List? nonce; - - EncryptionResult({ - this.encryptedData, - this.key, - this.header, - this.nonce, - }); -} diff --git a/auth/lib/onboarding/view/onboarding_page.dart b/auth/lib/onboarding/view/onboarding_page.dart index b97c3436a0..a0bc6fb661 100644 --- a/auth/lib/onboarding/view/onboarding_page.dart +++ b/auth/lib/onboarding/view/onboarding_page.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:io'; import 'package:ente_auth/app/view/app.dart'; import 'package:ente_auth/core/configuration.dart'; @@ -28,7 +29,7 @@ import "package:flutter/material.dart"; import 'package:local_auth/local_auth.dart'; class OnboardingPage extends StatefulWidget { - const OnboardingPage({Key? key}) : super(key: key); + const OnboardingPage({super.key}); @override State createState() => _OnboardingPageState(); @@ -86,118 +87,128 @@ class _OnboardingPageState extends State { } } }, - child: Center( - child: SingleChildScrollView( - child: Padding( - padding: - const EdgeInsets.symmetric(vertical: 40.0, horizontal: 40), - child: Column( - children: [ - Column( - children: [ - kDebugMode - ? GestureDetector( - child: const Align( - alignment: Alignment.topRight, - child: Text("Lang"), - ), - onTap: () async { - final locale = await getLocale(); - // ignore: unawaited_futures - routeToPage( - context, - LanguageSelectorPage( - appSupportedLocales, - (locale) async { - await setLocale(locale); - App.setLocale(context, locale); - }, - locale, - ), - ).then((value) { - setState(() {}); - }); - }, - ) - : const SizedBox(), - Image.asset( - "assets/sheild-front-gradient.png", - width: 200, - height: 200, - ), - const SizedBox(height: 12), - const Text( - "ente", - style: TextStyle( - fontWeight: FontWeight.bold, - fontFamily: 'Montserrat', - fontSize: 42, - ), - ), - const SizedBox(height: 4), - Text( - "Authenticator", - style: Theme.of(context).textTheme.headlineMedium, - ), - const SizedBox(height: 32), - Text( - l10n.onBoardingBody, - textAlign: TextAlign.center, - style: - Theme.of(context).textTheme.titleLarge!.copyWith( - color: Colors.white38, + child: SingleChildScrollView( + child: Center( + child: ConstrainedBox( + constraints: + const BoxConstraints.tightFor(height: 800, width: 450), + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 40.0, + horizontal: 40, + ), + child: Column( + children: [ + Column( + children: [ + kDebugMode + ? GestureDetector( + child: const Align( + alignment: Alignment.topRight, + child: Text("Lang"), ), - ), - ], - ), - const SizedBox(height: 100), - Container( - width: double.infinity, - padding: const EdgeInsets.symmetric(horizontal: 20), - child: GradientButton( - onTap: _navigateToSignUpPage, - text: l10n.newUser, + onTap: () async { + final locale = await getLocale(); + // ignore: unawaited_futures + routeToPage( + context, + LanguageSelectorPage( + appSupportedLocales, + (locale) async { + await setLocale(locale); + App.setLocale(context, locale); + }, + locale, + ), + ).then((value) { + setState(() {}); + }); + }, + ) + : const SizedBox(), + Image.asset( + "assets/sheild-front-gradient.png", + width: 200, + height: 200, + ), + const SizedBox(height: 12), + const Text( + "ente", + style: TextStyle( + fontWeight: FontWeight.bold, + fontFamily: 'Montserrat', + fontSize: 42, + ), + ), + const SizedBox(height: 4), + Text( + "Authenticator", + style: Theme.of(context).textTheme.headlineMedium, + ), + const SizedBox(height: 32), + Text( + l10n.onBoardingBody, + textAlign: TextAlign.center, + style: Theme.of(context) + .textTheme + .titleLarge! + .copyWith( + color: Colors.white38, + ), + ), + ], ), - ), - const SizedBox(height: 4), - Container( - width: double.infinity, - padding: const EdgeInsets.fromLTRB(20, 12, 20, 0), - child: Hero( - tag: "log_in", - child: ElevatedButton( - style: Theme.of(context) - .colorScheme - .optionalActionButtonStyle, - onPressed: _navigateToSignInPage, - child: Text( - l10n.existingUser, - style: const TextStyle( - color: Colors.black, // same for both themes + const SizedBox(height: 100), + Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(horizontal: 20), + child: GradientButton( + onTap: _navigateToSignUpPage, + text: l10n.newUser, + ), + ), + const SizedBox(height: 16), + Container( + height: 56, + width: double.infinity, + padding: const EdgeInsets.fromLTRB(20, 0, 20, 0), + child: Hero( + tag: "log_in", + child: ElevatedButton( + style: Theme.of(context) + .colorScheme + .optionalActionButtonStyle, + onPressed: _navigateToSignInPage, + child: Text( + l10n.existingUser, + style: const TextStyle( + color: Colors.black, // same for both themes + ), ), ), ), ), - ), - const SizedBox(height: 4), - Container( - width: double.infinity, - padding: const EdgeInsets.only(top: 20, bottom: 20), - child: GestureDetector( - onTap: _optForOfflineMode, - child: Center( - child: Text( - l10n.useOffline, - style: body.copyWith( - color: - Theme.of(context).colorScheme.mutedTextColor, + const SizedBox(height: 4), + Container( + width: double.infinity, + padding: const EdgeInsets.only(top: 20, bottom: 20), + child: GestureDetector( + onTap: _optForOfflineMode, + child: Center( + child: Text( + l10n.useOffline, + style: body.copyWith( + color: Theme.of(context) + .colorScheme + .mutedTextColor, + ), ), ), ), ), - ), - const DeveloperSettingsWidget(), - ], + const DeveloperSettingsWidget(), + ], + ), ), ), ), @@ -208,7 +219,9 @@ class _OnboardingPageState extends State { } Future _optForOfflineMode() async { - bool canCheckBio = await LocalAuthentication().canCheckBiometrics; + bool canCheckBio = Platform.isMacOS || Platform.isLinux + ? true + : await LocalAuthentication().canCheckBiometrics; if (!canCheckBio) { showToast( context, 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 ee46d79539..3937142d6c 100644 --- a/auth/lib/onboarding/view/setup_enter_secret_key_page.dart +++ b/auth/lib/onboarding/view/setup_enter_secret_key_page.dart @@ -9,7 +9,7 @@ import "package:flutter/material.dart"; class SetupEnterSecretKeyPage extends StatefulWidget { final Code? code; - SetupEnterSecretKeyPage({this.code, Key? key}) : super(key: key); + SetupEnterSecretKeyPage({this.code, super.key}); @override State createState() => @@ -32,7 +32,7 @@ class _SetupEnterSecretKeyPageState extends State { widget.code != null ? safeDecode(widget.code!.account).trim() : null, ); _secretController = TextEditingController( - text: widget.code != null ? widget.code!.secret : null, + text: widget.code?.secret, ); _secretKeyObscured = widget.code != null; super.initState(); @@ -45,8 +45,8 @@ class _SetupEnterSecretKeyPageState extends State { appBar: AppBar( title: Text(l10n.importAccountPageTitle), ), - body: SafeArea( - child: Center( + body: Center( + child: SingleChildScrollView( child: Padding( padding: const EdgeInsets.symmetric(vertical: 40.0, horizontal: 40), child: Column( diff --git a/auth/lib/onboarding/view/view_qr_page.dart b/auth/lib/onboarding/view/view_qr_page.dart index 71f4756a7c..687f810e35 100644 --- a/auth/lib/onboarding/view/view_qr_page.dart +++ b/auth/lib/onboarding/view/view_qr_page.dart @@ -1,4 +1,3 @@ - import 'dart:math'; import "package:ente_auth/l10n/l10n.dart"; @@ -10,7 +9,7 @@ import 'package:qr_flutter/qr_flutter.dart'; class ViewQrPage extends StatelessWidget { final Code? code; - ViewQrPage({this.code, Key? key}) : super(key: key); + ViewQrPage({this.code, super.key}); @override Widget build(BuildContext context) { @@ -22,15 +21,22 @@ class ViewQrPage extends StatelessWidget { appBar: AppBar( title: Text(l10n.qrCode), ), - body: SafeArea( - child: Center( + body: Center( + child: SingleChildScrollView( child: Padding( padding: const EdgeInsets.symmetric(vertical: 40.0, horizontal: 40), child: Column( children: [ - QrImage( + QrImageView( data: code!.rawData, - foregroundColor: Theme.of(context).colorScheme.onBackground, + eyeStyle: QrEyeStyle( + eyeShape: QrEyeShape.square, + color: Theme.of(context).colorScheme.onBackground, + ), + dataModuleStyle: QrDataModuleStyle( + dataModuleShape: QrDataModuleShape.square, + color: Theme.of(context).colorScheme.onBackground, + ), version: QrVersions.auto, size: qrSize, ), diff --git a/auth/lib/services/authenticator_service.dart b/auth/lib/services/authenticator_service.dart index 77d9b0437e..ee0ad32bd4 100644 --- a/auth/lib/services/authenticator_service.dart +++ b/auth/lib/services/authenticator_service.dart @@ -15,9 +15,8 @@ import 'package:ente_auth/models/authenticator/entity_result.dart'; import 'package:ente_auth/models/authenticator/local_auth_entity.dart'; import 'package:ente_auth/store/authenticator_db.dart'; import 'package:ente_auth/store/offline_authenticator_db.dart'; -import 'package:ente_auth/utils/crypto_util.dart'; +import 'package:ente_crypto_dart/ente_crypto_dart.dart'; import 'package:flutter/foundation.dart'; -import 'package:flutter_sodium/flutter_sodium.dart'; import 'package:logging/logging.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -75,10 +74,10 @@ class AuthenticatorService { final key = await getOrCreateAuthDataKey(mode); for (LocalAuthEntity e in result) { try { - final decryptedValue = await CryptoUtil.decryptChaCha( - Sodium.base642bin(e.encryptedData), + final decryptedValue = await CryptoUtil.decryptData( + CryptoUtil.base642bin(e.encryptedData), key, - Sodium.base642bin(e.header), + CryptoUtil.base642bin(e.header), ); final hasSynced = !(e.id == null || e.shouldSync); entities.add( @@ -101,12 +100,13 @@ class AuthenticatorService { AccountMode accountMode, ) async { var key = await getOrCreateAuthDataKey(accountMode); - final encryptedKeyData = await CryptoUtil.encryptChaCha( - utf8.encode(plainText) as Uint8List, + final encryptedKeyData = await CryptoUtil.encryptData( + utf8.encode(plainText), key, ); - String encryptedData = Sodium.bin2base64(encryptedKeyData.encryptedData!); - String header = Sodium.bin2base64(encryptedKeyData.header!); + String encryptedData = + CryptoUtil.bin2base64(encryptedKeyData.encryptedData!); + String header = CryptoUtil.bin2base64(encryptedKeyData.header!); final insertedID = accountMode.isOnline ? await _db.insert(encryptedData, header) : await _offlineDb.insert(encryptedData, header); @@ -123,12 +123,13 @@ class AuthenticatorService { AccountMode accountMode, ) async { var key = await getOrCreateAuthDataKey(accountMode); - final encryptedKeyData = await CryptoUtil.encryptChaCha( - utf8.encode(plainText) as Uint8List, + final encryptedKeyData = await CryptoUtil.encryptData( + utf8.encode(plainText), key, ); - String encryptedData = Sodium.bin2base64(encryptedKeyData.encryptedData!); - String header = Sodium.bin2base64(encryptedKeyData.header!); + String encryptedData = + CryptoUtil.bin2base64(encryptedKeyData.encryptedData!); + String header = CryptoUtil.bin2base64(encryptedKeyData.header!); final int affectedRows = accountMode.isOnline ? await _db.updateEntry(generatedID, encryptedData, header) : await _offlineDb.updateEntry(generatedID, encryptedData, header); @@ -191,25 +192,25 @@ class AuthenticatorService { Future _remoteToLocalSync() async { _logger.info('Initiating remote to local sync'); final int lastSyncTime = _prefs.getInt(_lastEntitySyncTime) ?? 0; - _logger.info("Current sync is " + lastSyncTime.toString()); + _logger.info("Current sync is $lastSyncTime"); const int fetchLimit = 500; final List result = await _gateway.getDiff(lastSyncTime, limit: fetchLimit); - _logger.info(result.length.toString() + " entries fetched from remote"); + _logger.info("${result.length} entries fetched from remote"); if (result.isEmpty) { return; } final maxSyncTime = result.map((e) => e.updatedAt).reduce(max); List deletedIDs = result.where((element) => element.isDeleted).map((e) => e.id).toList(); - _logger.info(deletedIDs.length.toString() + " entries deleted"); + _logger.info("${deletedIDs.length} entries deleted"); result.removeWhere((element) => element.isDeleted); await _db.insertOrReplace(result); if (deletedIDs.isNotEmpty) { await _db.deleteByIDs(ids: deletedIDs); } await _prefs.setInt(_lastEntitySyncTime, maxSyncTime); - _logger.info("Setting synctime to " + maxSyncTime.toString()); + _logger.info("Setting synctime to $maxSyncTime"); if (result.length == fetchLimit) { _logger.info("Diff limit reached, pulling again"); await _remoteToLocalSync(); @@ -223,7 +224,7 @@ class AuthenticatorService { .where((element) => element.shouldSync || element.id == null) .toList(); _logger.info( - pendingUpdate.length.toString() + " entries to be updated at remote", + "${pendingUpdate.length} entries to be updated at remote", ); for (LocalAuthEntity entity in pendingUpdate) { if (entity.id == null) { @@ -262,21 +263,21 @@ class AuthenticatorService { try { final AuthKey response = await _gateway.getKey(); final authKey = CryptoUtil.decryptSync( - Sodium.base642bin(response.encryptedKey), + CryptoUtil.base642bin(response.encryptedKey), _config.getKey()!, - Sodium.base642bin(response.header), + CryptoUtil.base642bin(response.header), ); - await _config.setAuthSecretKey(Sodium.bin2base64(authKey)); + await _config.setAuthSecretKey(CryptoUtil.bin2base64(authKey)); return authKey; } on AuthenticatorKeyNotFound catch (e) { _logger.info("AuthenticatorKeyNotFound generating key ${e.stackTrace}"); final key = CryptoUtil.generateKey(); final encryptedKeyData = CryptoUtil.encryptSync(key, _config.getKey()!); await _gateway.createKey( - Sodium.bin2base64(encryptedKeyData.encryptedData!), - Sodium.bin2base64(encryptedKeyData.nonce!), + CryptoUtil.bin2base64(encryptedKeyData.encryptedData!), + CryptoUtil.bin2base64(encryptedKeyData.nonce!), ); - await _config.setAuthSecretKey(Sodium.bin2base64(key)); + await _config.setAuthSecretKey(CryptoUtil.bin2base64(key)); return key; } catch (e, s) { _logger.severe("Failed to getOrCreateAuthDataKey", e, s); diff --git a/auth/lib/services/billing_service.dart b/auth/lib/services/billing_service.dart index 75a4e5579e..363425cd53 100644 --- a/auth/lib/services/billing_service.dart +++ b/auth/lib/services/billing_service.dart @@ -50,7 +50,7 @@ class BillingService { Future> _fetchPrivateBillingPlans() { return _dio.get( - _config.getHttpEndpoint() + "/billing/user-plans/", + "${_config.getHttpEndpoint()}/billing/user-plans/", options: Options( headers: { "X-Auth-Token": _config.getToken(), @@ -60,7 +60,7 @@ class BillingService { } Future> _fetchPublicBillingPlans() { - return _dio.get(_config.getHttpEndpoint() + "/billing/plans/v2"); + return _dio.get("${_config.getHttpEndpoint()}/billing/plans/v2"); } Future verifySubscription( @@ -70,7 +70,7 @@ class BillingService { }) async { try { final response = await _dio.post( - _config.getHttpEndpoint() + "/billing/verify-subscription", + "${_config.getHttpEndpoint()}/billing/verify-subscription", data: { "paymentProvider": paymentProvider ?? (Platform.isAndroid ? "playstore" : "appstore"), @@ -84,7 +84,7 @@ class BillingService { ), ); return Subscription.fromMap(response.data["subscription"]); - } on DioError catch (e) { + } on DioException catch (e) { if (e.response != null && e.response!.statusCode == 409) { throw SubscriptionAlreadyClaimedError(); } else { @@ -100,7 +100,7 @@ class BillingService { if (_cachedSubscription == null) { try { final response = await _dio.get( - _config.getHttpEndpoint() + "/billing/subscription", + "${_config.getHttpEndpoint()}/billing/subscription", options: Options( headers: { "X-Auth-Token": _config.getToken(), @@ -109,7 +109,7 @@ class BillingService { ); _cachedSubscription = Subscription.fromMap(response.data["subscription"]); - } on DioError catch (e, s) { + } on DioException catch (e, s) { _logger.severe(e, s); rethrow; } @@ -120,7 +120,7 @@ class BillingService { Future cancelStripeSubscription() async { try { final response = await _dio.post( - _config.getHttpEndpoint() + "/billing/stripe/cancel-subscription", + "${_config.getHttpEndpoint()}/billing/stripe/cancel-subscription", options: Options( headers: { "X-Auth-Token": _config.getToken(), @@ -129,7 +129,7 @@ class BillingService { ); final subscription = Subscription.fromMap(response.data["subscription"]); return subscription; - } on DioError catch (e, s) { + } on DioException catch (e, s) { _logger.severe(e, s); rethrow; } @@ -138,7 +138,7 @@ class BillingService { Future activateStripeSubscription() async { try { final response = await _dio.post( - _config.getHttpEndpoint() + "/billing/stripe/activate-subscription", + "${_config.getHttpEndpoint()}/billing/stripe/activate-subscription", options: Options( headers: { "X-Auth-Token": _config.getToken(), @@ -147,7 +147,7 @@ class BillingService { ); final subscription = Subscription.fromMap(response.data["subscription"]); return subscription; - } on DioError catch (e, s) { + } on DioException catch (e, s) { _logger.severe(e, s); rethrow; } @@ -158,7 +158,7 @@ class BillingService { }) async { try { final response = await _dio.get( - _config.getHttpEndpoint() + "/billing/stripe/customer-portal", + "${_config.getHttpEndpoint()}/billing/stripe/customer-portal", queryParameters: { "redirectURL": kWebPaymentRedirectUrl, }, @@ -169,7 +169,7 @@ class BillingService { ), ); return response.data["url"]; - } on DioError catch (e, s) { + } on DioException catch (e, s) { _logger.severe(e, s); rethrow; } diff --git a/auth/lib/services/local_authentication_service.dart b/auth/lib/services/local_authentication_service.dart index b7b000dd9f..44c2a758a7 100644 --- a/auth/lib/services/local_authentication_service.dart +++ b/auth/lib/services/local_authentication_service.dart @@ -1,15 +1,21 @@ +import 'dart:io'; + import 'package:ente_auth/core/configuration.dart'; import 'package:ente_auth/ui/tools/app_lock.dart'; import 'package:ente_auth/utils/auth_util.dart'; import 'package:ente_auth/utils/dialog_util.dart'; import 'package:ente_auth/utils/toast_util.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_local_authentication/flutter_local_authentication.dart'; import 'package:local_auth/local_auth.dart'; +import 'package:logging/logging.dart'; class LocalAuthenticationService { LocalAuthenticationService._privateConstructor(); static final LocalAuthenticationService instance = LocalAuthenticationService._privateConstructor(); + final logger = Logger((LocalAuthenticationService).toString()); Future requestLocalAuthentication( BuildContext context, @@ -38,7 +44,7 @@ class LocalAuthenticationService { String errorDialogContent, [ String errorDialogTitle = "", ]) async { - if (await LocalAuthentication().isDeviceSupported()) { + if (await _isLocalAuthSupportedOnDevice()) { AppLock.of(context)!.disable(); final result = await requestAuthentication( context, @@ -65,6 +71,12 @@ class LocalAuthenticationService { } Future _isLocalAuthSupportedOnDevice() async { - return await LocalAuthentication().isDeviceSupported(); + try { + return Platform.isMacOS || Platform.isLinux + ? await FlutterLocalAuthentication().canAuthenticate() + : await LocalAuthentication().isDeviceSupported(); + } on MissingPluginException { + return false; + } } } diff --git a/auth/lib/services/notification_service.dart b/auth/lib/services/notification_service.dart index 60d12811bd..63410eb7db 100644 --- a/auth/lib/services/notification_service.dart +++ b/auth/lib/services/notification_service.dart @@ -27,8 +27,7 @@ class NotificationService { _flutterLocalNotificationsPlugin.resolvePlatformSpecificImplementation< AndroidFlutterLocalNotificationsPlugin>(); if (implementation != null) { - // ignore: unawaited_futures - implementation.requestPermission(); + await implementation.requestNotificationsPermission(); } } diff --git a/auth/lib/services/update_service.dart b/auth/lib/services/update_service.dart index 09f36df834..e102a5a940 100644 --- a/auth/lib/services/update_service.dart +++ b/auth/lib/services/update_service.dart @@ -4,6 +4,7 @@ import 'dart:io'; import 'package:ente_auth/core/constants.dart'; import 'package:ente_auth/core/network.dart'; import 'package:ente_auth/services/notification_service.dart'; +import 'package:ente_auth/utils/platform_util.dart'; import 'package:logging/logging.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -130,7 +131,8 @@ class UpdateService { bool isIndependent() { return flavor == "independent" || - _packageInfo.packageName.endsWith("independent"); + _packageInfo.packageName.endsWith("independent") || + PlatformUtil.isDesktop(); } } @@ -141,6 +143,7 @@ class LatestVersionInfo { final bool? shouldForceUpdate; final int lastSupportedVersionCode; final String? url; + final String? release; final int? size; final bool? shouldNotify; @@ -151,6 +154,7 @@ class LatestVersionInfo { this.shouldForceUpdate, this.lastSupportedVersionCode, this.url, + this.release, this.size, this.shouldNotify, ); @@ -163,6 +167,7 @@ class LatestVersionInfo { map['shouldForceUpdate'], map['lastSupportedVersionCode'] ?? 1, map['url'], + map['release'], map['size'], map['shouldNotify'], ); diff --git a/auth/lib/services/user_remote_flag_service.dart b/auth/lib/services/user_remote_flag_service.dart index 6961518aa3..74deae5843 100644 --- a/auth/lib/services/user_remote_flag_service.dart +++ b/auth/lib/services/user_remote_flag_service.dart @@ -96,7 +96,7 @@ class UserRemoteFlagService { queryParams["defaultValue"] = defaultValue; } final response = await _dio.get( - _config.getHttpEndpoint() + "/remote-store", + "${_config.getHttpEndpoint()}/remote-store", queryParameters: queryParams, options: Options( headers: { @@ -119,7 +119,7 @@ class UserRemoteFlagService { Future _updateKeyValue(String key, String value) async { try { final response = await _dio.post( - _config.getHttpEndpoint() + "/remote-store/update", + "${_config.getHttpEndpoint()}/remote-store/update", data: { "key": key, "value": value, diff --git a/auth/lib/services/user_service.dart b/auth/lib/services/user_service.dart index b870aa6472..9294680598 100644 --- a/auth/lib/services/user_service.dart +++ b/auth/lib/services/user_service.dart @@ -30,9 +30,9 @@ import 'package:ente_auth/ui/home_page.dart'; import 'package:ente_auth/ui/passkey_page.dart'; import 'package:ente_auth/ui/two_factor_authentication_page.dart'; import 'package:ente_auth/ui/two_factor_recovery_page.dart'; -import 'package:ente_auth/utils/crypto_util.dart'; import 'package:ente_auth/utils/dialog_util.dart'; import 'package:ente_auth/utils/toast_util.dart'; +import 'package:ente_crypto_dart/ente_crypto_dart.dart'; import "package:flutter/foundation.dart"; import 'package:flutter/material.dart'; import 'package:logging/logging.dart'; @@ -80,7 +80,7 @@ class UserService { await dialog.show(); try { final response = await _dio.post( - _config.getHttpEndpoint() + "/users/ott", + "${_config.getHttpEndpoint()}/users/ott", data: {"email": email, "purpose": isChangeEmail ? "change" : ""}, ); await dialog.hide(); @@ -102,7 +102,7 @@ class UserService { return; } unawaited(showGenericErrorDialog(context: context)); - } on DioError catch (e) { + } on DioException catch (e) { await dialog.hide(); _logger.info(e); if (e.response != null && e.response!.statusCode == 403) { @@ -129,7 +129,7 @@ class UserService { String type = "SubCancellation", }) async { await _dio.post( - _config.getHttpEndpoint() + "/anonymous/feedback", + "${_config.getHttpEndpoint()}/anonymous/feedback", data: {"feedback": feedback, "type": "type"}, ); } @@ -173,7 +173,7 @@ class UserService { try { final response = await _enteDio.get("/users/sessions"); return Sessions.fromMap(response.data); - } on DioError catch (e) { + } on DioException catch (e) { _logger.info(e); rethrow; } @@ -187,7 +187,7 @@ class UserService { "token": token, }, ); - } on DioError catch (e) { + } on DioException catch (e) { _logger.info(e); rethrow; } @@ -196,7 +196,7 @@ class UserService { Future leaveFamilyPlan() async { try { await _enteDio.delete("/family/leave"); - } on DioError catch (e) { + } on DioException catch (e) { _logger.warning('failed to leave family plan', e); rethrow; } @@ -306,11 +306,11 @@ class UserService { "ott": ott, }; if (!_config.isLoggedIn()) { - verifyData["source"] = 'auth:' + _getRefSource(); + verifyData["source"] = 'auth:${_getRefSource()}'; } try { final response = await _dio.post( - _config.getHttpEndpoint() + "/users/verify-email", + "${_config.getHttpEndpoint()}/users/verify-email", data: verifyData, ); await dialog.hide(); @@ -346,7 +346,7 @@ class UserService { // should never reach here throw Exception("unexpected response during email verification"); } - } on DioError catch (e) { + } on DioException catch (e) { _logger.info(e); await dialog.hide(); if (e.response != null && e.response!.statusCode == 410) { @@ -410,7 +410,7 @@ class UserService { context.l10n.oops, context.l10n.verificationFailedPleaseTryAgain, ); - } on DioError catch (e) { + } on DioException catch (e) { await dialog.hide(); if (e.response != null && e.response!.statusCode == 403) { // ignore: unawaited_futures @@ -460,7 +460,7 @@ class UserService { Future getSrpAttributes(String email) async { try { final response = await _dio.get( - _config.getHttpEndpoint() + "/users/srp/attributes", + "${_config.getHttpEndpoint()}/users/srp/attributes", queryParameters: { "email": email, }, @@ -470,7 +470,7 @@ class UserService { } else { throw Exception("get-srp-attributes action failed"); } - } on DioError catch (e) { + } on DioException catch (e) { if (e.response != null && e.response!.statusCode == 404) { throw SrpSetupNotCompleteError(); } @@ -523,7 +523,7 @@ class UserService { // ignore: need to calculate secret to get M1, unused_local_variable final clientS = client.calculateSecret(serverB); final clientM = client.calculateClientEvidenceMessage(); - // ignore: unused_local_variable + late Response _; if (setKeysRequest == null) { _ = await _enteDio.post( @@ -573,7 +573,7 @@ class UserService { late Uint8List keyEncryptionKey; _logger.finest('Start deriving key'); keyEncryptionKey = await CryptoUtil.deriveKey( - utf8.encode(userPassword) as Uint8List, + utf8.encode(userPassword), CryptoUtil.base642bin(srpAttributes.kekSalt), srpAttributes.memLimit, srpAttributes.opsLimit, @@ -596,7 +596,7 @@ class UserService { final A = client.generateClientCredentials(salt, identity, password); final createSessionResponse = await _dio.post( - _config.getHttpEndpoint() + "/users/srp/create-session", + "${_config.getHttpEndpoint()}/users/srp/create-session", data: { "srpUserID": srpAttributes.srpUserID, "srpA": base64Encode(SRP6Util.encodeBigInt(A!)), @@ -610,7 +610,7 @@ class UserService { final clientS = client.calculateSecret(serverB); final clientM = client.calculateClientEvidenceMessage(); final response = await _dio.post( - _config.getHttpEndpoint() + "/users/srp/verify-session", + "${_config.getHttpEndpoint()}/users/srp/verify-session", data: { "sessionID": sessionID, "srpUserID": srpAttributes.srpUserID, @@ -709,7 +709,7 @@ class UserService { await dialog.show(); try { final response = await _dio.post( - _config.getHttpEndpoint() + "/users/two-factor/verify", + "${_config.getHttpEndpoint()}/users/two-factor/verify", data: { "sessionID": sessionID, "code": code, @@ -729,7 +729,7 @@ class UserService { (route) => route.isFirst, ); } - } on DioError catch (e) { + } on DioException catch (e) { await dialog.hide(); _logger.severe(e); if (e.response != null && e.response!.statusCode == 404) { @@ -772,7 +772,7 @@ class UserService { await dialog.show(); try { final response = await _dio.get( - _config.getHttpEndpoint() + "/users/two-factor/recover", + "${_config.getHttpEndpoint()}/users/two-factor/recover", queryParameters: { "sessionID": sessionID, "twoFactorType": twoFactorTypeToString(type), @@ -794,7 +794,7 @@ class UserService { (route) => route.isFirst, ); } - } on DioError catch (e) { + } on DioException catch (e) { await dialog.hide(); _logger.severe(e); if (e.response != null && e.response!.statusCode == 404) { @@ -868,7 +868,7 @@ class UserService { } try { final response = await _dio.post( - _config.getHttpEndpoint() + "/users/two-factor/remove", + "${_config.getHttpEndpoint()}/users/two-factor/remove", data: { "sessionID": sessionID, "secret": secret, @@ -891,7 +891,7 @@ class UserService { (route) => route.isFirst, ); } - } on DioError catch (e) { + } on DioException catch (e) { await dialog.hide(); _logger.severe(e); if (e.response != null && e.response!.statusCode == 404) { diff --git a/auth/lib/services/window_listener_service.dart b/auth/lib/services/window_listener_service.dart new file mode 100644 index 0000000000..9ceefef9a3 --- /dev/null +++ b/auth/lib/services/window_listener_service.dart @@ -0,0 +1,36 @@ +import 'dart:async'; +import 'dart:ui'; + +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:window_manager/window_manager.dart'; + +class WindowListenerService { + late SharedPreferences _preferences; + + WindowListenerService._privateConstructor(); + + static final WindowListenerService instance = + WindowListenerService._privateConstructor(); + + Future init() async { + _preferences = await SharedPreferences.getInstance(); + } + + Size getWindowSize() { + final double windowWidth = _preferences.getDouble('windowWidth') ?? 450.0; + final double windowHeight = _preferences.getDouble('windowHeight') ?? 800.0; + return Size(windowWidth, windowHeight); + } + + Future onWindowResize() async { + // Save the window size to shared preferences + await _preferences.setDouble( + 'windowWidth', + (await windowManager.getSize()).width, + ); + await _preferences.setDouble( + 'windowHeight', + (await windowManager.getSize()).height, + ); + } +} diff --git a/auth/lib/store/authenticator_db.dart b/auth/lib/store/authenticator_db.dart index 0f05e83c05..deb57bc814 100644 --- a/auth/lib/store/authenticator_db.dart +++ b/auth/lib/store/authenticator_db.dart @@ -3,10 +3,12 @@ import 'dart:io'; import 'package:ente_auth/models/authenticator/auth_entity.dart'; import 'package:ente_auth/models/authenticator/local_auth_entity.dart'; +import 'package:ente_auth/utils/directory_utils.dart'; import 'package:flutter/foundation.dart'; import 'package:path/path.dart'; import 'package:path_provider/path_provider.dart'; import 'package:sqflite/sqflite.dart'; +import 'package:sqflite_common_ffi/sqflite_ffi.dart'; class AuthenticatorDB { static const _databaseName = "ente.authenticator.db"; @@ -25,6 +27,16 @@ class AuthenticatorDB { } Future _initDatabase() async { + if (Platform.isWindows || Platform.isLinux) { + var databaseFactory = databaseFactoryFfi; + return await databaseFactory.openDatabase( + await DirectoryUtils.getDatabasePath(_databaseName), + options: OpenDatabaseOptions( + version: _databaseVersion, + onCreate: _onCreate, + ), + ); + } final Directory documentsDirectory = await getApplicationDocumentsDirectory(); final String path = join(documentsDirectory.path, _databaseName); @@ -166,7 +178,7 @@ class AuthenticatorDB { batch.delete(entityTable, where: whereID, whereArgs: [id]); } } - await batch.commit(); + final _ = await batch.commit(); debugPrint("Done"); } diff --git a/auth/lib/store/offline_authenticator_db.dart b/auth/lib/store/offline_authenticator_db.dart index 402d6590cb..d1af51fff3 100644 --- a/auth/lib/store/offline_authenticator_db.dart +++ b/auth/lib/store/offline_authenticator_db.dart @@ -3,10 +3,11 @@ import 'dart:io'; import 'package:ente_auth/models/authenticator/auth_entity.dart'; import 'package:ente_auth/models/authenticator/local_auth_entity.dart'; +import 'package:ente_auth/utils/directory_utils.dart'; import 'package:flutter/foundation.dart'; import 'package:path/path.dart'; import 'package:path_provider/path_provider.dart'; -import 'package:sqflite/sqflite.dart'; +import 'package:sqflite_common_ffi/sqflite_ffi.dart'; class OfflineAuthenticatorDB { static const _databaseName = "ente.offline_authenticator.db"; @@ -26,6 +27,16 @@ class OfflineAuthenticatorDB { } Future _initDatabase() async { + if (Platform.isWindows || Platform.isLinux) { + var databaseFactory = databaseFactoryFfi; + return await databaseFactory.openDatabase( + await DirectoryUtils.getDatabasePath(_databaseName), + options: OpenDatabaseOptions( + version: _databaseVersion, + onCreate: _onCreate, + ), + ); + } final Directory documentsDirectory = await getApplicationDocumentsDirectory(); final String path = join(documentsDirectory.path, _databaseName); @@ -152,7 +163,7 @@ class OfflineAuthenticatorDB { batch.delete(entityTable, where: whereID, whereArgs: [id]); } } - await batch.commit(); + final _ = await batch.commit(); debugPrint("Done"); } diff --git a/auth/lib/theme/colors.dart b/auth/lib/theme/colors.dart index 1d4a8517c2..9ac9d2d7e2 100644 --- a/auth/lib/theme/colors.dart +++ b/auth/lib/theme/colors.dart @@ -204,6 +204,7 @@ const Color _warning700 = Color.fromRGBO(234, 63, 63, 1); const Color _warning500 = Color.fromRGBO(255, 101, 101, 1); const Color _warning800 = Color(0xFFF53434); const Color warning500 = Color.fromRGBO(255, 101, 101, 1); +// ignore: unused_element const Color _warning400 = Color.fromRGBO(255, 111, 111, 1); const Color _caution500 = Color.fromRGBO(255, 194, 71, 1); diff --git a/auth/lib/ui/account/change_email_dialog.dart b/auth/lib/ui/account/change_email_dialog.dart index 7cfa10f4a8..828970cd73 100644 --- a/auth/lib/ui/account/change_email_dialog.dart +++ b/auth/lib/ui/account/change_email_dialog.dart @@ -5,7 +5,7 @@ import 'package:ente_auth/utils/email_util.dart'; import 'package:flutter/material.dart'; class ChangeEmailDialog extends StatefulWidget { - const ChangeEmailDialog({Key? key}) : super(key: key); + const ChangeEmailDialog({super.key}); @override State createState() => _ChangeEmailDialogState(); diff --git a/auth/lib/ui/account/delete_account_page.dart b/auth/lib/ui/account/delete_account_page.dart index 0567449749..b8779e8cbb 100644 --- a/auth/lib/ui/account/delete_account_page.dart +++ b/auth/lib/ui/account/delete_account_page.dart @@ -7,15 +7,15 @@ import 'package:ente_auth/services/local_authentication_service.dart'; import 'package:ente_auth/services/user_service.dart'; import 'package:ente_auth/ui/common/dialogs.dart'; import 'package:ente_auth/ui/common/gradient_button.dart'; -import 'package:ente_auth/utils/crypto_util.dart'; import 'package:ente_auth/utils/email_util.dart'; +import 'package:ente_auth/utils/platform_util.dart'; +import 'package:ente_crypto_dart/ente_crypto_dart.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_sodium/flutter_sodium.dart'; class DeleteAccountPage extends StatelessWidget { const DeleteAccountPage({ - Key? key, - }) : super(key: key); + super.key, + }); @override Widget build(BuildContext context) { @@ -150,6 +150,8 @@ class DeleteAccountPage extends StatelessWidget { l10n.initiateAccountDeleteTitle, ); + await PlatformUtil.refocusWindows(); + if (hasAuthenticated) { final choice = await showChoiceDialogOld( context, @@ -164,8 +166,10 @@ class DeleteAccountPage extends StatelessWidget { return; } final decryptChallenge = CryptoUtil.openSealSync( - Sodium.base642bin(response.encryptedChallenge), - Sodium.base642bin(Configuration.instance.getKeyAttributes()!.publicKey), + CryptoUtil.base642bin(response.encryptedChallenge), + CryptoUtil.base642bin( + Configuration.instance.getKeyAttributes()!.publicKey, + ), Configuration.instance.getSecretKey()!, ); final challengeResponseStr = utf8.decode(decryptChallenge); diff --git a/auth/lib/ui/account/email_entry_page.dart b/auth/lib/ui/account/email_entry_page.dart index a20f6e245d..e728ecd8c7 100644 --- a/auth/lib/ui/account/email_entry_page.dart +++ b/auth/lib/ui/account/email_entry_page.dart @@ -5,7 +5,7 @@ import 'package:ente_auth/l10n/l10n.dart'; import 'package:ente_auth/services/user_service.dart'; import 'package:ente_auth/theme/ente_theme.dart'; import 'package:ente_auth/ui/common/dynamic_fab.dart'; -import 'package:ente_auth/ui/common/web_page.dart'; +import 'package:ente_auth/utils/platform_util.dart'; import 'package:ente_auth/utils/toast_util.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -14,7 +14,7 @@ import 'package:step_progress_indicator/step_progress_indicator.dart'; import "package:styled_text/styled_text.dart"; class EmailEntryPage extends StatefulWidget { - const EmailEntryPage({Key? key}) : super(key: key); + const EmailEntryPage({super.key}); @override State createState() => _EmailEntryPageState(); @@ -190,6 +190,7 @@ class _EmailEntryPageState extends State { padding: const EdgeInsets.fromLTRB(20, 0, 20, 0), child: TextFormField( keyboardType: TextInputType.text, + textInputAction: TextInputAction.next, controller: _passwordController1, obscureText: !_password1Visible, enableSuggestions: true, @@ -427,15 +428,10 @@ class _EmailEntryPageState extends State { tags: { 'u-terms': StyledTextActionTag( (String? text, Map attrs) => - Navigator.of(context).push( - MaterialPageRoute( - builder: (BuildContext context) { - return WebPage( - context.l10n.termsOfServicesTitle, - "https://ente.io/terms", - ); - }, - ), + PlatformUtil.openWebView( + context, + context.l10n.termsOfServicesTitle, + "https://ente.io/terms", ), style: const TextStyle( decoration: TextDecoration.underline, @@ -443,15 +439,10 @@ class _EmailEntryPageState extends State { ), 'u-policy': StyledTextActionTag( (String? text, Map attrs) => - Navigator.of(context).push( - MaterialPageRoute( - builder: (BuildContext context) { - return WebPage( - context.l10n.privacyPolicyTitle, - "https://ente.io/privacy", - ); - }, - ), + PlatformUtil.openWebView( + context, + context.l10n.privacyPolicyTitle, + "https://ente.io/privacy", ), style: const TextStyle( decoration: TextDecoration.underline, @@ -494,15 +485,10 @@ class _EmailEntryPageState extends State { tags: { 'underline': StyledTextActionTag( (String? text, Map attrs) => - Navigator.of(context).push( - MaterialPageRoute( - builder: (BuildContext context) { - return WebPage( - context.l10n.encryption, - "https://ente.io/architecture", - ); - }, - ), + PlatformUtil.openWebView( + context, + context.l10n.encryption, + "https://ente.io/architecture", ), style: const TextStyle( decoration: TextDecoration.underline, diff --git a/auth/lib/ui/account/login_page.dart b/auth/lib/ui/account/login_page.dart index df28f98a1d..5b8aa0daa2 100644 --- a/auth/lib/ui/account/login_page.dart +++ b/auth/lib/ui/account/login_page.dart @@ -6,13 +6,13 @@ import 'package:ente_auth/models/api/user/srp.dart'; import 'package:ente_auth/services/user_service.dart'; import 'package:ente_auth/ui/account/login_pwd_verification_page.dart'; import 'package:ente_auth/ui/common/dynamic_fab.dart'; -import 'package:ente_auth/ui/common/web_page.dart'; +import 'package:ente_auth/utils/platform_util.dart'; import 'package:flutter/material.dart'; import 'package:logging/logging.dart'; import "package:styled_text/styled_text.dart"; class LoginPage extends StatefulWidget { - const LoginPage({Key? key}) : super(key: key); + const LoginPage({super.key}); @override State createState() => _LoginPageState(); @@ -25,6 +25,36 @@ class _LoginPageState extends State { Color? _emailInputFieldColor; final Logger _logger = Logger('_LoginPageState'); + Future onPressed() async { + await UserService.instance.setEmail(_email!); + Configuration.instance.resetVolatilePassword(); + SrpAttributes? attr; + bool isEmailVerificationEnabled = true; + try { + attr = await UserService.instance.getSrpAttributes(_email!); + isEmailVerificationEnabled = attr.isEmailMFAEnabled; + } catch (e) { + if (e is! SrpSetupNotCompleteError) { + _logger.severe('Error getting SRP attributes', e); + } + } + if (attr != null && !isEmailVerificationEnabled) { + await Navigator.of(context).push( + MaterialPageRoute( + builder: (BuildContext context) { + return LoginPasswordVerificationPage( + srpAttributes: attr!, + ); + }, + ), + ); + } else { + await UserService.instance + .sendOtt(context, _email!, isCreateAccountScreen: false); + } + FocusScope.of(context).unfocus(); + } + @override void initState() { _email = _config.getEmail(); @@ -60,36 +90,7 @@ class _LoginPageState extends State { isKeypadOpen: isKeypadOpen, isFormValid: _emailIsValid, buttonText: context.l10n.logInLabel, - onPressedFunction: () async { - await UserService.instance.setEmail(_email!); - Configuration.instance.resetVolatilePassword(); - SrpAttributes? attr; - bool isEmailVerificationEnabled = true; - try { - attr = await UserService.instance.getSrpAttributes(_email!); - isEmailVerificationEnabled = attr.isEmailMFAEnabled; - } catch (e) { - if (e is! SrpSetupNotCompleteError) { - _logger.severe('Error getting SRP attributes', e); - } - } - if (attr != null && !isEmailVerificationEnabled) { - // ignore: unawaited_futures - Navigator.of(context).push( - MaterialPageRoute( - builder: (BuildContext context) { - return LoginPasswordVerificationPage( - srpAttributes: attr!, - ); - }, - ), - ); - } else { - await UserService.instance - .sendOtt(context, _email!, isCreateAccountScreen: false); - } - FocusScope.of(context).unfocus(); - }, + onPressedFunction: onPressed, ), floatingActionButtonLocation: fabLocation(), floatingActionButtonAnimator: NoScalingAnimation(), @@ -116,6 +117,8 @@ class _LoginPageState extends State { padding: const EdgeInsets.fromLTRB(20, 24, 20, 0), child: TextFormField( autofillHints: const [AutofillHints.email], + onFieldSubmitted: + _emailIsValid ? (value) => onPressed() : null, decoration: InputDecoration( fillColor: _emailInputFieldColor, filled: true, @@ -179,15 +182,10 @@ class _LoginPageState extends State { tags: { 'u-terms': StyledTextActionTag( (String? text, Map attrs) => - Navigator.of(context).push( - MaterialPageRoute( - builder: (BuildContext context) { - return WebPage( - context.l10n.termsOfServicesTitle, - "https://ente.io/terms", - ); - }, - ), + PlatformUtil.openWebView( + context, + context.l10n.termsOfServicesTitle, + "https://ente.io/terms", ), style: const TextStyle( decoration: TextDecoration.underline, @@ -195,15 +193,10 @@ class _LoginPageState extends State { ), 'u-policy': StyledTextActionTag( (String? text, Map attrs) => - Navigator.of(context).push( - MaterialPageRoute( - builder: (BuildContext context) { - return WebPage( - context.l10n.privacyPolicyTitle, - "https://ente.io/privacy", - ); - }, - ), + PlatformUtil.openWebView( + context, + context.l10n.privacyPolicyTitle, + "https://ente.io/privacy", ), style: const TextStyle( decoration: TextDecoration.underline, diff --git a/auth/lib/ui/account/login_pwd_verification_page.dart b/auth/lib/ui/account/login_pwd_verification_page.dart index 3bd39a1b77..2d2754ec16 100644 --- a/auth/lib/ui/account/login_pwd_verification_page.dart +++ b/auth/lib/ui/account/login_pwd_verification_page.dart @@ -1,6 +1,5 @@ import "package:dio/dio.dart"; import 'package:ente_auth/core/configuration.dart'; -import "package:ente_auth/core/errors.dart"; import "package:ente_auth/l10n/l10n.dart"; import "package:ente_auth/models/api/user/srp.dart"; import "package:ente_auth/services/user_service.dart"; @@ -9,6 +8,7 @@ import 'package:ente_auth/ui/common/dynamic_fab.dart'; import "package:ente_auth/ui/components/buttons/button_widget.dart"; import "package:ente_auth/utils/dialog_util.dart"; import "package:ente_auth/utils/email_util.dart"; +import "package:ente_crypto_dart/ente_crypto_dart.dart"; import 'package:flutter/material.dart'; import "package:logging/logging.dart"; @@ -19,8 +19,7 @@ import "package:logging/logging.dart"; // volatile password. class LoginPasswordVerificationPage extends StatefulWidget { final SrpAttributes srpAttributes; - const LoginPasswordVerificationPage({Key? key, required this.srpAttributes}) - : super(key: key); + const LoginPasswordVerificationPage({super.key, required this.srpAttributes}); @override State createState() => @@ -36,6 +35,11 @@ class _LoginPasswordVerificationPageState bool _passwordInFocus = false; bool _passwordVisible = false; + Future onPressed() async { + FocusScope.of(context).unfocus(); + await verifyPassword(context, _passwordController.text); + } + @override void initState() { super.initState(); @@ -77,10 +81,7 @@ class _LoginPasswordVerificationPageState isKeypadOpen: isKeypadOpen, isFormValid: _passwordController.text.isNotEmpty, buttonText: context.l10n.logInLabel, - onPressedFunction: () async { - FocusScope.of(context).unfocus(); - await verifyPassword(context, _passwordController.text); - }, + onPressedFunction: onPressed, ), floatingActionButtonLocation: fabLocation(), floatingActionButtonAnimator: NoScalingAnimation(), @@ -101,7 +102,7 @@ class _LoginPasswordVerificationPageState password, dialog, ); - } on DioError catch (e, s) { + } on DioException catch (e, s) { await dialog.hide(); if (e.response != null && e.response!.statusCode == 401) { _logger.severe('server reject, failed verify SRP login', e, s); @@ -112,7 +113,7 @@ class _LoginPasswordVerificationPageState ); } else { _logger.severe('API failure during SRP login', e, s); - if (e.type == DioErrorType.other) { + if (e.type == DioExceptionType.unknown) { await _showContactSupportDialog( context, context.l10n.noInternetConnection, @@ -229,6 +230,9 @@ class _LoginPasswordVerificationPageState Padding( padding: const EdgeInsets.fromLTRB(20, 24, 20, 0), child: TextFormField( + onFieldSubmitted: _passwordController.text.isNotEmpty + ? (_) => onPressed() + : null, key: const ValueKey("passwordInputField"), autofillHints: const [AutofillHints.password], decoration: InputDecoration( diff --git a/auth/lib/ui/account/ott_verification_page.dart b/auth/lib/ui/account/ott_verification_page.dart index 8bcaf3e349..cc9661defc 100644 --- a/auth/lib/ui/account/ott_verification_page.dart +++ b/auth/lib/ui/account/ott_verification_page.dart @@ -17,8 +17,8 @@ class OTTVerificationPage extends StatefulWidget { this.isChangeEmail = false, this.isCreateAccountScreen = false, this.isResetPasswordScreen = false, - Key? key, - }) : super(key: key); + super.key, + }); @override State createState() => _OTTVerificationPageState(); @@ -27,6 +27,23 @@ class OTTVerificationPage extends StatefulWidget { class _OTTVerificationPageState extends State { final _verificationCodeController = TextEditingController(); + Future onPressed() async { + if (widget.isChangeEmail) { + await UserService.instance.changeEmail( + context, + widget.email, + _verificationCodeController.text, + ); + } else { + await UserService.instance.verifyEmail( + context, + _verificationCodeController.text, + isResettingPasswordScreen: widget.isResetPasswordScreen, + ); + } + FocusScope.of(context).unfocus(); + } + @override Widget build(BuildContext context) { final l10n = context.l10n; @@ -68,22 +85,9 @@ class _OTTVerificationPageState extends State { body: _getBody(), floatingActionButton: DynamicFAB( isKeypadOpen: isKeypadOpen, - isFormValid: !(_verificationCodeController.text.isEmpty), + isFormValid: _verificationCodeController.text.isNotEmpty, buttonText: l10n.verify, - onPressedFunction: () { - if (widget.isChangeEmail) { - UserService.instance.changeEmail( - context, - widget.email, - _verificationCodeController.text, - ); - } else { - UserService.instance - .verifyEmail(context, _verificationCodeController.text, - isResettingPasswordScreen: widget.isResetPasswordScreen,); - } - FocusScope.of(context).unfocus(); - }, + onPressedFunction: onPressed, ), floatingActionButtonLocation: fabLocation(), floatingActionButtonAnimator: NoScalingAnimation(), @@ -160,6 +164,9 @@ class _OTTVerificationPageState extends State { padding: const EdgeInsets.fromLTRB(20, 16, 20, 16), child: TextFormField( style: Theme.of(context).textTheme.titleMedium, + onFieldSubmitted: _verificationCodeController.text.isNotEmpty + ? (_) => onPressed() + : null, decoration: InputDecoration( filled: true, hintText: l10n.tapToEnterCode, diff --git a/auth/lib/ui/account/password_entry_page.dart b/auth/lib/ui/account/password_entry_page.dart index 7411d34a8e..9b1ce61812 100644 --- a/auth/lib/ui/account/password_entry_page.dart +++ b/auth/lib/ui/account/password_entry_page.dart @@ -4,11 +4,11 @@ import 'package:ente_auth/models/key_gen_result.dart'; import 'package:ente_auth/services/user_service.dart'; import 'package:ente_auth/ui/account/recovery_key_page.dart'; import 'package:ente_auth/ui/common/dynamic_fab.dart'; -import 'package:ente_auth/ui/common/web_page.dart'; import 'package:ente_auth/ui/components/models/button_type.dart'; import 'package:ente_auth/ui/home_page.dart'; import 'package:ente_auth/utils/dialog_util.dart'; import 'package:ente_auth/utils/navigation_util.dart'; +import 'package:ente_auth/utils/platform_util.dart'; import 'package:ente_auth/utils/toast_util.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -25,7 +25,7 @@ enum PasswordEntryMode { class PasswordEntryPage extends StatefulWidget { final PasswordEntryMode mode; - const PasswordEntryPage({required this.mode, Key? key}) : super(key: key); + const PasswordEntryPage({required this.mode, super.key}); @override State createState() => _PasswordEntryPageState(); @@ -149,227 +149,239 @@ class _PasswordEntryPageState extends State { children: [ Expanded( child: AutofillGroup( - child: ListView( - children: [ - Padding( - padding: - const EdgeInsets.symmetric(vertical: 30, horizontal: 20), - child: Text( - buttonTextAndHeading, - style: Theme.of(context).textTheme.headlineMedium, + child: FocusTraversalGroup( + policy: OrderedTraversalPolicy(), + child: ListView( + children: [ + Padding( + padding: const EdgeInsets.symmetric( + vertical: 30, + horizontal: 20, + ), + child: Text( + buttonTextAndHeading, + style: Theme.of(context).textTheme.headlineMedium, + ), ), - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 20), - child: Text( - widget.mode == PasswordEntryMode.set - ? context.l10n.enterPasswordToEncrypt - : context.l10n.enterNewPasswordToEncrypt, - textAlign: TextAlign.start, - style: Theme.of(context) - .textTheme - .titleMedium! - .copyWith(fontSize: 14), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Text( + widget.mode == PasswordEntryMode.set + ? context.l10n.enterPasswordToEncrypt + : context.l10n.enterNewPasswordToEncrypt, + textAlign: TextAlign.start, + style: Theme.of(context) + .textTheme + .titleMedium! + .copyWith(fontSize: 14), + ), ), - ), - const Padding(padding: EdgeInsets.all(8)), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 20), - child: StyledText( - text: context.l10n.passwordWarning, - style: Theme.of(context) - .textTheme - .titleMedium! - .copyWith(fontSize: 14), - tags: { - 'underline': StyledTextTag( - style: - Theme.of(context).textTheme.titleMedium!.copyWith( - fontSize: 14, - decoration: TextDecoration.underline, + const Padding(padding: EdgeInsets.all(8)), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: StyledText( + text: context.l10n.passwordWarning, + style: Theme.of(context) + .textTheme + .titleMedium! + .copyWith(fontSize: 14), + tags: { + 'underline': StyledTextTag( + style: + Theme.of(context).textTheme.titleMedium!.copyWith( + fontSize: 14, + decoration: TextDecoration.underline, + ), + ), + }, + ), + ), + const Padding(padding: EdgeInsets.all(12)), + Visibility( + // hidden textForm for suggesting auto-fill service for saving + // password + visible: false, + child: TextFormField( + autofillHints: const [ + AutofillHints.email, + ], + autocorrect: false, + keyboardType: TextInputType.emailAddress, + initialValue: email, + textInputAction: TextInputAction.next, + ), + ), + Padding( + padding: const EdgeInsets.fromLTRB(20, 0, 20, 0), + child: TextFormField( + autofillHints: const [AutofillHints.newPassword], + onFieldSubmitted: (_) { + do { + FocusScope.of(context).nextFocus(); + } while (FocusScope.of(context).focusedChild!.context == + null); + }, + decoration: InputDecoration( + fillColor: + _isPasswordValid ? _validFieldValueColor : null, + filled: true, + hintText: context.l10n.password, + contentPadding: const EdgeInsets.all(20), + border: UnderlineInputBorder( + borderSide: BorderSide.none, + borderRadius: BorderRadius.circular(6), + ), + suffixIcon: _password1InFocus + ? IconButton( + icon: Icon( + _password1Visible + ? Icons.visibility + : Icons.visibility_off, + color: Theme.of(context).iconTheme.color, + size: 20, ), + onPressed: () { + setState(() { + _password1Visible = !_password1Visible; + }); + }, + ) + : _isPasswordValid + ? Icon( + Icons.check, + color: Theme.of(context) + .inputDecorationTheme + .focusedBorder! + .borderSide + .color, + ) + : null, ), - }, - ), - ), - const Padding(padding: EdgeInsets.all(12)), - Visibility( - // hidden textForm for suggesting auto-fill service for saving - // password - visible: false, - child: TextFormField( - autofillHints: const [ - AutofillHints.email, - ], - autocorrect: false, - keyboardType: TextInputType.emailAddress, - initialValue: email, - textInputAction: TextInputAction.next, - ), - ), - Padding( - padding: const EdgeInsets.fromLTRB(20, 0, 20, 0), - child: TextFormField( - autofillHints: const [AutofillHints.newPassword], - decoration: InputDecoration( - fillColor: - _isPasswordValid ? _validFieldValueColor : null, - filled: true, - hintText: context.l10n.password, - contentPadding: const EdgeInsets.all(20), - border: UnderlineInputBorder( - borderSide: BorderSide.none, - borderRadius: BorderRadius.circular(6), - ), - suffixIcon: _password1InFocus - ? IconButton( - icon: Icon( - _password1Visible - ? Icons.visibility - : Icons.visibility_off, - color: Theme.of(context).iconTheme.color, - size: 20, - ), - onPressed: () { - setState(() { - _password1Visible = !_password1Visible; - }); - }, - ) - : _isPasswordValid - ? Icon( - Icons.check, - color: Theme.of(context) - .inputDecorationTheme - .focusedBorder! - .borderSide - .color, - ) - : null, - ), - obscureText: !_password1Visible, - controller: _passwordController1, - autofocus: false, - autocorrect: false, - keyboardType: TextInputType.visiblePassword, - onChanged: (password) { - setState(() { - _passwordInInputBox = password; - _passwordStrength = estimatePasswordStrength(password); - _isPasswordValid = - _passwordStrength >= kMildPasswordStrengthThreshold; - _passwordsMatch = _passwordInInputBox == - _passwordInInputConfirmationBox; - }); - }, - textInputAction: TextInputAction.next, - focusNode: _password1FocusNode, - ), - ), - const SizedBox(height: 8), - Padding( - padding: const EdgeInsets.fromLTRB(20, 0, 20, 0), - child: TextFormField( - keyboardType: TextInputType.visiblePassword, - controller: _passwordController2, - obscureText: !_password2Visible, - autofillHints: const [AutofillHints.newPassword], - onEditingComplete: () => TextInput.finishAutofillContext(), - decoration: InputDecoration( - fillColor: _passwordsMatch ? _validFieldValueColor : null, - filled: true, - hintText: context.l10n.confirmPassword, - contentPadding: const EdgeInsets.symmetric( - horizontal: 20, - vertical: 20, - ), - suffixIcon: _password2InFocus - ? IconButton( - icon: Icon( - _password2Visible - ? Icons.visibility - : Icons.visibility_off, - color: Theme.of(context).iconTheme.color, - size: 20, - ), - onPressed: () { - setState(() { - _password2Visible = !_password2Visible; - }); - }, - ) - : _passwordsMatch - ? Icon( - Icons.check, - color: Theme.of(context) - .inputDecorationTheme - .focusedBorder! - .borderSide - .color, - ) - : null, - border: UnderlineInputBorder( - borderSide: BorderSide.none, - borderRadius: BorderRadius.circular(6), - ), - ), - focusNode: _password2FocusNode, - onChanged: (cnfPassword) { - setState(() { - _passwordInInputConfirmationBox = cnfPassword; - if (_passwordInInputBox != '') { + obscureText: !_password1Visible, + controller: _passwordController1, + autofocus: false, + autocorrect: false, + keyboardType: TextInputType.visiblePassword, + onChanged: (password) { + setState(() { + _passwordInInputBox = password; + _passwordStrength = + estimatePasswordStrength(password); + _isPasswordValid = _passwordStrength >= + kMildPasswordStrengthThreshold; _passwordsMatch = _passwordInInputBox == _passwordInInputConfirmationBox; - } - }); - }, - ), - ), - Opacity( - opacity: - (_passwordInInputBox != '') && _password1InFocus ? 1 : 0, - child: Padding( - padding: - const EdgeInsets.symmetric(horizontal: 20, vertical: 8), - child: Text( - context.l10n.passwordStrength(passwordStrengthText), - style: TextStyle( - color: passwordStrengthColor, - ), + }); + }, + textInputAction: TextInputAction.next, + focusNode: _password1FocusNode, ), ), - ), - const SizedBox(height: 8), - GestureDetector( - behavior: HitTestBehavior.translucent, - onTap: () { - Navigator.of(context).push( - MaterialPageRoute( - builder: (BuildContext context) { - return WebPage( - context.l10n.howItWorks, - "https://ente.io/architecture", - ); - }, - ), - ); - }, - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 20), - child: RichText( - text: TextSpan( - text: context.l10n.howItWorks, - style: - Theme.of(context).textTheme.titleMedium!.copyWith( - fontSize: 14, - decoration: TextDecoration.underline, + const SizedBox(height: 8), + Padding( + padding: const EdgeInsets.fromLTRB(20, 0, 20, 0), + child: TextFormField( + keyboardType: TextInputType.visiblePassword, + controller: _passwordController2, + obscureText: !_password2Visible, + autofillHints: const [AutofillHints.newPassword], + onEditingComplete: () => + TextInput.finishAutofillContext(), + decoration: InputDecoration( + fillColor: + _passwordsMatch ? _validFieldValueColor : null, + filled: true, + hintText: context.l10n.confirmPassword, + contentPadding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 20, + ), + suffixIcon: _password2InFocus + ? IconButton( + icon: Icon( + _password2Visible + ? Icons.visibility + : Icons.visibility_off, + color: Theme.of(context).iconTheme.color, + size: 20, ), + onPressed: () { + setState(() { + _password2Visible = !_password2Visible; + }); + }, + ) + : _passwordsMatch + ? Icon( + Icons.check, + color: Theme.of(context) + .inputDecorationTheme + .focusedBorder! + .borderSide + .color, + ) + : null, + border: UnderlineInputBorder( + borderSide: BorderSide.none, + borderRadius: BorderRadius.circular(6), + ), + ), + focusNode: _password2FocusNode, + onChanged: (cnfPassword) { + setState(() { + _passwordInInputConfirmationBox = cnfPassword; + if (_passwordInInputBox != '') { + _passwordsMatch = _passwordInInputBox == + _passwordInInputConfirmationBox; + } + }); + }, + ), + ), + Opacity( + opacity: (_passwordInInputBox != '') && _password1InFocus + ? 1 + : 0, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 20, + vertical: 8, + ), + child: Text( + context.l10n.passwordStrength(passwordStrengthText), + style: TextStyle( + color: passwordStrengthColor, + ), ), ), ), - ), - const Padding(padding: EdgeInsets.all(20)), - ], + const SizedBox(height: 8), + GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: () { + PlatformUtil.openWebView( + context, + context.l10n.howItWorks, + "https://ente.io/architecture", + ); + }, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: RichText( + text: TextSpan( + text: context.l10n.howItWorks, + style: + Theme.of(context).textTheme.titleMedium!.copyWith( + fontSize: 14, + decoration: TextDecoration.underline, + ), + ), + ), + ), + ), + const Padding(padding: EdgeInsets.all(20)), + ], + ), ), ), ), @@ -463,6 +475,7 @@ class _PasswordEntryPageState extends State { showGenericErrorDialog(context: context); } } + // ignore: unawaited_futures routeToPage( context, diff --git a/auth/lib/ui/account/password_reentry_page.dart b/auth/lib/ui/account/password_reentry_page.dart index f729f935ed..261f41db50 100644 --- a/auth/lib/ui/account/password_reentry_page.dart +++ b/auth/lib/ui/account/password_reentry_page.dart @@ -9,14 +9,14 @@ import 'package:ente_auth/ui/account/recovery_page.dart'; import 'package:ente_auth/ui/common/dynamic_fab.dart'; import 'package:ente_auth/ui/components/buttons/button_widget.dart'; import 'package:ente_auth/ui/home_page.dart'; -import 'package:ente_auth/utils/crypto_util.dart'; import 'package:ente_auth/utils/dialog_util.dart'; import 'package:ente_auth/utils/email_util.dart'; +import 'package:ente_crypto_dart/ente_crypto_dart.dart'; import 'package:flutter/material.dart'; import 'package:logging/logging.dart'; class PasswordReentryPage extends StatefulWidget { - const PasswordReentryPage({Key? key}) : super(key: key); + const PasswordReentryPage({super.key}); @override State createState() => _PasswordReentryPageState(); @@ -261,8 +261,8 @@ class _PasswordReentryPageState extends State { ), Padding( padding: const EdgeInsets.symmetric(horizontal: 20), - child: Wrap( - alignment: WrapAlignment.spaceBetween, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ GestureDetector( behavior: HitTestBehavior.opaque, @@ -275,13 +275,17 @@ class _PasswordReentryPageState extends State { ), ); }, - child: Text( - context.l10n.forgotPassword, - style: - Theme.of(context).textTheme.titleMedium!.copyWith( - fontSize: 14, - decoration: TextDecoration.underline, - ), + child: Center( + child: Text( + context.l10n.forgotPassword, + style: Theme.of(context) + .textTheme + .titleMedium! + .copyWith( + fontSize: 14, + decoration: TextDecoration.underline, + ), + ), ), ), GestureDetector( @@ -297,13 +301,17 @@ class _PasswordReentryPageState extends State { Navigator.of(context) .popUntil((route) => route.isFirst); }, - child: Text( - context.l10n.changeEmail, - style: - Theme.of(context).textTheme.titleMedium!.copyWith( - fontSize: 14, - decoration: TextDecoration.underline, - ), + child: Center( + child: Text( + context.l10n.changeEmail, + style: Theme.of(context) + .textTheme + .titleMedium! + .copyWith( + fontSize: 14, + decoration: TextDecoration.underline, + ), + ), ), ), ], diff --git a/auth/lib/ui/account/recovery_key_page.dart b/auth/lib/ui/account/recovery_key_page.dart index c5ca66fdc9..a74b2daeca 100644 --- a/auth/lib/ui/account/recovery_key_page.dart +++ b/auth/lib/ui/account/recovery_key_page.dart @@ -1,3 +1,4 @@ +import 'dart:convert'; import 'dart:io' as io; import 'package:bip39/bip39.dart' as bip39; @@ -7,7 +8,10 @@ import 'package:ente_auth/core/constants.dart'; import 'package:ente_auth/ente_theme_data.dart'; import 'package:ente_auth/l10n/l10n.dart'; import 'package:ente_auth/ui/common/gradient_button.dart'; +import 'package:ente_auth/utils/platform_util.dart'; +import 'package:ente_auth/utils/share_utils.dart'; import 'package:ente_auth/utils/toast_util.dart'; +import 'package:file_saver/file_saver.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:share_plus/share_plus.dart'; @@ -27,7 +31,7 @@ class RecoveryKeyPage extends StatefulWidget { const RecoveryKeyPage( this.recoveryKey, this.doneText, { - Key? key, + super.key, this.showAppBar, this.onDone, this.isDismissible, @@ -35,7 +39,7 @@ class RecoveryKeyPage extends StatefulWidget { this.text, this.subText, this.showProgressBar = false, - }) : super(key: key); + }); @override State createState() => _RecoveryKeyPageState(); @@ -44,7 +48,7 @@ class RecoveryKeyPage extends StatefulWidget { class _RecoveryKeyPageState extends State { bool _hasTriedToSave = false; final _recoveryKeyFile = io.File( - Configuration.instance.getTempDirectory() + "ente-recovery-key.txt", + "${Configuration.instance.getTempDirectory()}ente-recovery-key.txt", ); @override @@ -61,6 +65,21 @@ class _RecoveryKeyPageState extends State { ? 32 : 120; + Future copy() async { + await Clipboard.setData( + ClipboardData( + text: recoveryKey, + ), + ); + showShortToast( + context, + context.l10n.recoveryKeyCopiedToClipboard, + ); + setState(() { + _hasTriedToSave = true; + }); + } + return Scaffold( appBar: widget.showProgressBar ? AppBar( @@ -113,62 +132,73 @@ class _RecoveryKeyPageState extends State { style: Theme.of(context).textTheme.titleMedium, ), const Padding(padding: EdgeInsets.only(top: 24)), - DottedBorder( - color: const Color.fromARGB(255, 105, 17, 127), - //color of dotted/dash line - strokeWidth: 1, - //thickness of dash/dots - dashPattern: const [6, 6], - radius: const Radius.circular(8), - //dash patterns, 10 is dash width, 6 is space width - child: SizedBox( - //inner container - // height: 120, //height of inner container - width: double - .infinity, //width to 100% match to parent container. - // ignore: prefer_const_literals_to_create_immutables - child: Column( - children: [ - GestureDetector( - onTap: () async { - await Clipboard.setData( - ClipboardData(text: recoveryKey), - ); - showShortToast( - context, - context.l10n.recoveryKeyCopiedToClipboard, - ); - setState(() { - _hasTriedToSave = true; - }); - }, - child: Container( - decoration: BoxDecoration( - border: Border.all( - color: const Color.fromRGBO( - 49, - 155, - 86, - .2, - ), + Container( + padding: const EdgeInsets.all(1), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + gradient: const LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Color(0x8E9610D6), + Color(0x8E9F4FC6), + ], + stops: [0.0, 0.9753], + ), + ), + child: DottedBorder( + padding: EdgeInsets.zero, + borderType: BorderType.RRect, + strokeWidth: 1, + color: const Color(0xFF6B6B6B), + dashPattern: const [6, 6], + radius: const Radius.circular(8), + child: SizedBox( + width: double.infinity, + child: Stack( + children: [ + Column( + children: [ + Builder( + builder: (context) { + final content = Container( + padding: const EdgeInsets.all(20), + width: double.infinity, + child: Text( + recoveryKey, + textAlign: TextAlign.justify, + style: Theme.of(context) + .textTheme + .bodyLarge, + ), + ); + + if (PlatformUtil.isMobile()) { + return GestureDetector( + onTap: () async => await copy(), + child: content, + ); + } else { + return SelectableRegion( + focusNode: FocusNode(), + selectionControls: + PlatformUtil.selectionControls, + child: content, + ); + } + }, ), - borderRadius: const BorderRadius.all( - Radius.circular(2), - ), - color: Theme.of(context) - .colorScheme - .recoveryKeyBoxColor, - ), - padding: const EdgeInsets.all(20), - width: double.infinity, - child: Text( - recoveryKey, - style: - Theme.of(context).textTheme.bodyLarge, + ], + ), + Positioned( + right: 0, + top: 0, + child: PlatformCopy( + onPressed: copy, ), ), - ), - ], + ], + ), ), ), ), @@ -193,7 +223,7 @@ class _RecoveryKeyPageState extends State { ), ), ], - ), // columnEnds + ), ), ), ); @@ -207,12 +237,15 @@ class _RecoveryKeyPageState extends State { final List childrens = []; if (!_hasTriedToSave) { childrens.add( - ElevatedButton( - style: Theme.of(context).colorScheme.optionalActionButtonStyle, - onPressed: () async { - await _saveKeys(); - }, - child: Text(context.l10n.doThisLater), + SizedBox( + height: 56, + child: ElevatedButton( + style: Theme.of(context).colorScheme.optionalActionButtonStyle, + onPressed: () async { + await _saveKeys(); + }, + child: Text(context.l10n.doThisLater), + ), ), ); childrens.add(const SizedBox(height: 10)); @@ -221,19 +254,32 @@ class _RecoveryKeyPageState extends State { childrens.add( GradientButton( onTap: () async { - await _shareRecoveryKey(recoveryKey); + await shareDialog( + context, + context.l10n.recoveryKey, + saveAction: () async { + await _saveRecoveryKey(recoveryKey); + }, + sendAction: () async { + await _shareRecoveryKey(recoveryKey); + }, + ); }, text: context.l10n.saveKey, ), ); + if (_hasTriedToSave) { childrens.add(const SizedBox(height: 10)); childrens.add( - ElevatedButton( - child: Text(widget.doneText), - onPressed: () async { - await _saveKeys(); - }, + SizedBox( + height: 56, + child: ElevatedButton( + child: Text(widget.doneText), + onPressed: () async { + await _saveKeys(); + }, + ), ), ); } @@ -241,11 +287,34 @@ class _RecoveryKeyPageState extends State { return childrens; } + Future _saveRecoveryKey(String recoveryKey) async { + final bytes = utf8.encode(recoveryKey); + final time = DateTime.now().millisecondsSinceEpoch; + + await PlatformUtil.shareFile( + "ente_recovery_key_$time", + "txt", + bytes, + MimeType.text, + ); + + if (mounted) { + showToast( + context, + context.l10n.recoveryKeySaved, + ); + setState(() { + _hasTriedToSave = true; + }); + } + } + Future _shareRecoveryKey(String recoveryKey) async { if (_recoveryKeyFile.existsSync()) { await _recoveryKeyFile.delete(); } _recoveryKeyFile.writeAsStringSync(recoveryKey); + // ignore: deprecated_member_use await Share.shareFiles([_recoveryKeyFile.path]); Future.delayed(const Duration(milliseconds: 500), () { if (mounted) { @@ -264,3 +333,24 @@ class _RecoveryKeyPageState extends State { widget.onDone!(); } } + +class PlatformCopy extends StatelessWidget { + const PlatformCopy({ + super.key, + required this.onPressed, + }); + + final void Function() onPressed; + + @override + Widget build(BuildContext context) { + return IconButton( + onPressed: () => onPressed(), + visualDensity: VisualDensity.compact, + icon: const Icon( + Icons.copy, + size: 16, + ), + ); + } +} diff --git a/auth/lib/ui/account/recovery_page.dart b/auth/lib/ui/account/recovery_page.dart index f0d5199d1a..137b8ce437 100644 --- a/auth/lib/ui/account/recovery_page.dart +++ b/auth/lib/ui/account/recovery_page.dart @@ -1,8 +1,5 @@ - - -import 'dart:ui'; - import 'package:ente_auth/core/configuration.dart'; +import 'package:ente_auth/l10n/l10n.dart'; import 'package:ente_auth/ui/account/password_entry_page.dart'; import 'package:ente_auth/ui/common/dynamic_fab.dart'; import 'package:ente_auth/utils/dialog_util.dart'; @@ -10,7 +7,7 @@ import 'package:ente_auth/utils/toast_util.dart'; import 'package:flutter/material.dart'; class RecoveryPage extends StatefulWidget { - const RecoveryPage({Key? key}) : super(key: key); + const RecoveryPage({super.key}); @override State createState() => _RecoveryPageState(); @@ -19,6 +16,36 @@ class RecoveryPage extends StatefulWidget { class _RecoveryPageState extends State { final _recoveryKey = TextEditingController(); + Future onPressed() async { + FocusScope.of(context).unfocus(); + final dialog = createProgressDialog(context, "Decrypting..."); + await dialog.show(); + try { + await Configuration.instance.recover(_recoveryKey.text.trim()); + await dialog.hide(); + showToast(context, "Recovery successful!"); + await Navigator.of(context).pushReplacement( + MaterialPageRoute( + builder: (BuildContext context) { + return const PopScope( + canPop: false, + child: PasswordEntryPage( + mode: PasswordEntryMode.reset, + ), + ); + }, + ), + ); + } catch (e) { + await dialog.hide(); + String errMessage = 'The recovery key you entered is incorrect'; + if (e is AssertionError) { + errMessage = '$errMessage : ${e.message}'; + } + await showErrorDialog(context, "Incorrect recovery key", errMessage); + } + } + @override Widget build(BuildContext context) { final isKeypadOpen = MediaQuery.of(context).viewInsets.bottom > 100; @@ -46,37 +73,7 @@ class _RecoveryPageState extends State { isKeypadOpen: isKeypadOpen, isFormValid: _recoveryKey.text.isNotEmpty, buttonText: 'Recover', - onPressedFunction: () async { - FocusScope.of(context).unfocus(); - final dialog = createProgressDialog(context, "Decrypting..."); - await dialog.show(); - try { - await Configuration.instance.recover(_recoveryKey.text.trim()); - await dialog.hide(); - showToast(context, "Recovery successful!"); - // ignore: unawaited_futures - Navigator.of(context).pushReplacement( - MaterialPageRoute( - builder: (BuildContext context) { - return WillPopScope( - onWillPop: () async => false, - child: const PasswordEntryPage( - mode: PasswordEntryMode.reset, - ), - ); - }, - ), - ); - } catch (e) { - await dialog.hide(); - String errMessage = 'The recovery key you entered is incorrect'; - if (e is AssertionError) { - errMessage = '$errMessage : ${e.message}'; - } - // ignore: unawaited_futures - showErrorDialog(context, "Incorrect recovery key", errMessage); - } - }, + onPressedFunction: onPressed, ), floatingActionButtonLocation: fabLocation(), floatingActionButtonAnimator: NoScalingAnimation(), @@ -89,7 +86,7 @@ class _RecoveryPageState extends State { padding: const EdgeInsets.symmetric(vertical: 30, horizontal: 20), child: Text( - 'Forgot password', + context.l10n.forgotPassword, style: Theme.of(context).textTheme.headlineMedium, ), ), @@ -140,12 +137,14 @@ class _RecoveryPageState extends State { padding: const EdgeInsets.symmetric(horizontal: 20), child: Center( child: Text( - "No recovery key?", - style: - Theme.of(context).textTheme.titleMedium!.copyWith( - fontSize: 14, - decoration: TextDecoration.underline, - ), + context.l10n.noRecoveryKeyTitle, + style: Theme.of(context) + .textTheme + .titleMedium! + .copyWith( + fontSize: 14, + decoration: TextDecoration.underline, + ), ), ), ), diff --git a/auth/lib/ui/account/request_pwd_verification_page.dart b/auth/lib/ui/account/request_pwd_verification_page.dart index d852859fa7..5901d3bd45 100644 --- a/auth/lib/ui/account/request_pwd_verification_page.dart +++ b/auth/lib/ui/account/request_pwd_verification_page.dart @@ -5,10 +5,9 @@ import 'package:ente_auth/core/configuration.dart'; import "package:ente_auth/l10n/l10n.dart"; import "package:ente_auth/theme/ente_theme.dart"; import 'package:ente_auth/ui/common/dynamic_fab.dart'; -import "package:ente_auth/utils/crypto_util.dart"; import "package:ente_auth/utils/dialog_util.dart"; +import 'package:ente_crypto_dart/ente_crypto_dart.dart'; import 'package:flutter/material.dart'; -import "package:flutter_sodium/flutter_sodium.dart"; import "package:logging/logging.dart"; typedef OnPasswordVerifiedFn = Future Function(Uint8List bytes); @@ -17,8 +16,11 @@ class RequestPasswordVerificationPage extends StatefulWidget { final OnPasswordVerifiedFn onPasswordVerified; final Function? onPasswordError; - const RequestPasswordVerificationPage( - {super.key, required this.onPasswordVerified, this.onPasswordError,}); + const RequestPasswordVerificationPage({ + super.key, + required this.onPasswordVerified, + this.onPasswordError, + }); @override State createState() => @@ -82,15 +84,15 @@ class _RequestPasswordVerificationPageState try { final attributes = Configuration.instance.getKeyAttributes()!; final Uint8List keyEncryptionKey = await CryptoUtil.deriveKey( - utf8.encode(_passwordController.text) as Uint8List, - Sodium.base642bin(attributes.kekSalt), + utf8.encode(_passwordController.text), + CryptoUtil.base642bin(attributes.kekSalt), attributes.memLimit, attributes.opsLimit, ); CryptoUtil.decryptSync( - Sodium.base642bin(attributes.encryptedKey), + CryptoUtil.base642bin(attributes.encryptedKey), keyEncryptionKey, - Sodium.base642bin(attributes.keyDecryptionNonce), + CryptoUtil.base642bin(attributes.keyDecryptionNonce), ); await dialog.show(); // pop diff --git a/auth/lib/ui/account/sessions_page.dart b/auth/lib/ui/account/sessions_page.dart index 31b7e7a415..1815b20e23 100644 --- a/auth/lib/ui/account/sessions_page.dart +++ b/auth/lib/ui/account/sessions_page.dart @@ -11,7 +11,7 @@ import 'package:flutter/material.dart'; import 'package:logging/logging.dart'; class SessionsPage extends StatefulWidget { - const SessionsPage({Key? key}) : super(key: key); + const SessionsPage({super.key}); @override State createState() => _SessionsPageState(); diff --git a/auth/lib/ui/account/verify_recovery_page.dart b/auth/lib/ui/account/verify_recovery_page.dart index 2633afe331..03ed81fdf0 100644 --- a/auth/lib/ui/account/verify_recovery_page.dart +++ b/auth/lib/ui/account/verify_recovery_page.dart @@ -1,5 +1,3 @@ -import 'dart:ui'; - import 'package:bip39/bip39.dart' as bip39; import 'package:dio/dio.dart'; import 'package:ente_auth/core/configuration.dart'; @@ -12,12 +10,13 @@ import 'package:ente_auth/ui/common/gradient_button.dart'; import 'package:ente_auth/ui/components/buttons/button_widget.dart'; import 'package:ente_auth/utils/dialog_util.dart'; import 'package:ente_auth/utils/navigation_util.dart'; +import 'package:ente_auth/utils/platform_util.dart'; +import 'package:ente_crypto_dart/ente_crypto_dart.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_sodium/flutter_sodium.dart'; import 'package:logging/logging.dart'; class VerifyRecoveryPage extends StatefulWidget { - const VerifyRecoveryPage({Key? key}) : super(key: key); + const VerifyRecoveryPage({super.key}); @override State createState() => _VerifyRecoveryPageState(); @@ -34,14 +33,14 @@ class _VerifyRecoveryPageState extends State { try { final String inputKey = _recoveryKey.text.trim(); final String recoveryKey = - Sodium.bin2hex(Configuration.instance.getRecoveryKey()); + CryptoUtil.bin2hex(Configuration.instance.getRecoveryKey()); final String recoveryKeyWords = bip39.entropyToMnemonic(recoveryKey); if (inputKey == recoveryKey || inputKey == recoveryKeyWords) { try { await UserRemoteFlagService.instance.markRecoveryVerificationAsDone(); } catch (e) { await dialog.hide(); - if (e is DioError && e.type == DioErrorType.other) { + if (e is DioException && e.type == DioExceptionType.unknown) { await showErrorDialog( context, "No internet connection", @@ -88,12 +87,14 @@ class _VerifyRecoveryPageState extends State { context, "Please authenticate to view your recovery key", ); + await PlatformUtil.refocusWindows(); + if (hasAuthenticated) { String recoveryKey; try { - recoveryKey = Sodium.bin2hex(Configuration.instance.getRecoveryKey()); - // ignore: unawaited_futures - routeToPage( + recoveryKey = + CryptoUtil.bin2hex(Configuration.instance.getRecoveryKey()); + await routeToPage( context, RecoveryKeyPage( recoveryKey, diff --git a/auth/lib/ui/code_timer_progress.dart b/auth/lib/ui/code_timer_progress.dart index 1dbb4358eb..b524a0c238 100644 --- a/auth/lib/ui/code_timer_progress.dart +++ b/auth/lib/ui/code_timer_progress.dart @@ -6,12 +6,12 @@ class CodeTimerProgress extends StatefulWidget { final int period; CodeTimerProgress({ - Key? key, + super.key, required this.period, - }) : super(key: key); + }); @override - _CodeTimerProgressState createState() => _CodeTimerProgressState(); + State createState() => _CodeTimerProgressState(); } class _CodeTimerProgressState extends State diff --git a/auth/lib/ui/code_widget.dart b/auth/lib/ui/code_widget.dart index 7e0f78be35..f97e865ec7 100644 --- a/auth/lib/ui/code_widget.dart +++ b/auth/lib/ui/code_widget.dart @@ -14,9 +14,11 @@ import 'package:ente_auth/store/code_store.dart'; import 'package:ente_auth/ui/code_timer_progress.dart'; import 'package:ente_auth/ui/utils/icon_utils.dart'; import 'package:ente_auth/utils/dialog_util.dart'; +import 'package:ente_auth/utils/platform_util.dart'; import 'package:ente_auth/utils/toast_util.dart'; import 'package:ente_auth/utils/totp_util.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_context_menu/flutter_context_menu.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; import 'package:logging/logging.dart'; import 'package:move_to_background/move_to_background.dart'; @@ -24,7 +26,7 @@ import 'package:move_to_background/move_to_background.dart'; class CodeWidget extends StatefulWidget { final Code code; - const CodeWidget(this.code, {Key? key}) : super(key: key); + const CodeWidget(this.code, {super.key}); @override State createState() => _CodeWidgetState(); @@ -84,83 +86,121 @@ class _CodeWidgetState extends State { final l10n = context.l10n; return Container( margin: const EdgeInsets.only(left: 16, right: 16, bottom: 8, top: 8), - child: Slidable( - key: ValueKey(widget.code.hashCode), - endActionPane: ActionPane( - extentRatio: 0.60, - motion: const ScrollMotion(), - children: [ - const SizedBox( - width: 4, - ), - SlidableAction( - onPressed: _onShowQrPressed, - backgroundColor: Colors.grey.withOpacity(0.1), - borderRadius: const BorderRadius.all(Radius.circular(12.0)), - foregroundColor: - Theme.of(context).colorScheme.inverseBackgroundColor, - icon: Icons.qr_code_2_outlined, - label: "QR", - padding: const EdgeInsets.only(left: 4, right: 0), - spacing: 8, - ), - const SizedBox( - width: 4, - ), - SlidableAction( - onPressed: _onEditPressed, - backgroundColor: Colors.grey.withOpacity(0.1), - borderRadius: const BorderRadius.all(Radius.circular(12.0)), - foregroundColor: - Theme.of(context).colorScheme.inverseBackgroundColor, - icon: Icons.edit_outlined, - label: l10n.edit, - padding: const EdgeInsets.only(left: 4, right: 0), - spacing: 8, - ), - const SizedBox( - width: 4, - ), - SlidableAction( - onPressed: _onDeletePressed, - backgroundColor: Colors.grey.withOpacity(0.1), - borderRadius: const BorderRadius.all(Radius.circular(12.0)), - foregroundColor: const Color(0xFFFE4A49), - icon: Icons.delete, - label: l10n.delete, - padding: const EdgeInsets.only(left: 0, right: 0), - spacing: 8, - ), - ], - ), - child: ClipRRect( - borderRadius: BorderRadius.circular(8), - child: Container( - color: Theme.of(context).colorScheme.codeCardBackgroundColor, - child: Material( - color: Colors.transparent, - child: InkWell( - customBorder: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), - onTap: () { - _copyCurrentOTPToClipboard(); - }, - onDoubleTap: isMaskingEnabled - ? () { - setState( - () { - _hideCode = !_hideCode; - }, - ); - } - : null, - onLongPress: () { - _copyCurrentOTPToClipboard(); - }, - child: _getCardContents(l10n), + child: Builder( + builder: (context) { + if (PlatformUtil.isDesktop()) { + return ContextMenuRegion( + contextMenu: ContextMenu( + entries: [ + MenuItem( + label: 'QR', + icon: Icons.qr_code_2_outlined, + onSelected: () => _onShowQrPressed(null), + ), + MenuItem( + label: l10n.edit, + icon: Icons.edit, + onSelected: () => _onEditPressed(null), + ), + const MenuDivider(), + MenuItem( + label: l10n.delete, + value: "Delete", + icon: Icons.delete, + onSelected: () => _onDeletePressed(null), + ), + ], + padding: const EdgeInsets.all(8.0), ), + child: _clippedCard(l10n), + ); + } + + return Slidable( + key: ValueKey(widget.code.hashCode), + endActionPane: ActionPane( + extentRatio: 0.60, + motion: const ScrollMotion(), + children: [ + const SizedBox( + width: 4, + ), + SlidableAction( + onPressed: _onShowQrPressed, + backgroundColor: Colors.grey.withOpacity(0.1), + borderRadius: const BorderRadius.all(Radius.circular(12.0)), + foregroundColor: + Theme.of(context).colorScheme.inverseBackgroundColor, + icon: Icons.qr_code_2_outlined, + label: "QR", + padding: const EdgeInsets.only(left: 4, right: 0), + spacing: 8, + ), + const SizedBox( + width: 4, + ), + SlidableAction( + onPressed: _onEditPressed, + backgroundColor: Colors.grey.withOpacity(0.1), + borderRadius: const BorderRadius.all(Radius.circular(12.0)), + foregroundColor: + Theme.of(context).colorScheme.inverseBackgroundColor, + icon: Icons.edit_outlined, + label: l10n.edit, + padding: const EdgeInsets.only(left: 4, right: 0), + spacing: 8, + ), + const SizedBox( + width: 4, + ), + SlidableAction( + onPressed: _onDeletePressed, + backgroundColor: Colors.grey.withOpacity(0.1), + borderRadius: const BorderRadius.all(Radius.circular(12.0)), + foregroundColor: const Color(0xFFFE4A49), + icon: Icons.delete, + label: l10n.delete, + padding: const EdgeInsets.only(left: 0, right: 0), + spacing: 8, + ), + ], ), + child: Builder( + builder: (context) => _clippedCard(l10n), + ), + ); + }, + ), + ); + } + + Widget _clippedCard(AppLocalizations l10n) { + return ClipRRect( + borderRadius: BorderRadius.circular(8), + child: Container( + color: Theme.of(context).colorScheme.codeCardBackgroundColor, + child: Material( + color: Colors.transparent, + child: InkWell( + customBorder: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + onTap: () { + _copyCurrentOTPToClipboard(); + }, + onDoubleTap: isMaskingEnabled + ? () { + setState( + () { + _hideCode = !_hideCode; + }, + ); + } + : null, + onLongPress: () { + _copyCurrentOTPToClipboard(); + }, + child: _getCardContents(l10n), ), ), ), @@ -373,9 +413,10 @@ class _CodeWidgetState extends State { } Future _onEditPressed(_) async { - bool _isAuthSuccessful = await LocalAuthenticationService.instance + bool isAuthSuccessful = await LocalAuthenticationService.instance .requestLocalAuthentication(context, context.l10n.editCodeAuthMessage); - if (!_isAuthSuccessful) { + await PlatformUtil.refocusWindows(); + if (!isAuthSuccessful) { return; } final Code? code = await Navigator.of(context).push( @@ -391,9 +432,10 @@ class _CodeWidgetState extends State { } Future _onShowQrPressed(_) async { - bool _isAuthSuccessful = await LocalAuthenticationService.instance + bool isAuthSuccessful = await LocalAuthenticationService.instance .requestLocalAuthentication(context, context.l10n.showQRAuthMessage); - if (!_isAuthSuccessful) { + await PlatformUtil.refocusWindows(); + if (!isAuthSuccessful) { return; } // ignore: unused_local_variable @@ -407,14 +449,15 @@ class _CodeWidgetState extends State { } void _onDeletePressed(_) async { - bool _isAuthSuccessful = + bool isAuthSuccessful = await LocalAuthenticationService.instance.requestLocalAuthentication( context, context.l10n.deleteCodeAuthMessage, ); - if (!_isAuthSuccessful) { + if (!isAuthSuccessful) { return; } + FocusScope.of(context).requestFocus(); final l10n = context.l10n; await showChoiceActionSheet( context, @@ -451,7 +494,7 @@ class _CodeWidgetState extends State { code = code.replaceAll(RegExp(r'\d'), '•'); } if (code.length == 6) { - return code.substring(0, 3) + " " + code.substring(3, 6); + return "${code.substring(0, 3)} ${code.substring(3, 6)}"; } return code; } diff --git a/auth/lib/ui/common/bottom_shadow.dart b/auth/lib/ui/common/bottom_shadow.dart index 08a73564f7..a57e5232a6 100644 --- a/auth/lib/ui/common/bottom_shadow.dart +++ b/auth/lib/ui/common/bottom_shadow.dart @@ -5,8 +5,7 @@ import 'package:flutter/material.dart'; class BottomShadowWidget extends StatelessWidget { final double offsetDy; final Color? shadowColor; - const BottomShadowWidget({this.offsetDy = 28, this.shadowColor, Key? key}) - : super(key: key); + const BottomShadowWidget({this.offsetDy = 28, this.shadowColor, super.key}); @override Widget build(BuildContext context) { diff --git a/auth/lib/ui/common/DividerWithPadding.dart b/auth/lib/ui/common/divider_with_padding.dart similarity index 92% rename from auth/lib/ui/common/DividerWithPadding.dart rename to auth/lib/ui/common/divider_with_padding.dart index 94b20d9c23..0470bdf340 100644 --- a/auth/lib/ui/common/DividerWithPadding.dart +++ b/auth/lib/ui/common/divider_with_padding.dart @@ -1,17 +1,15 @@ - - import 'package:flutter/material.dart'; class DividerWithPadding extends StatelessWidget { final double left, top, right, bottom, thinckness; const DividerWithPadding({ - Key? key, + super.key, this.left = 0, this.top = 0, this.right = 0, this.bottom = 0, this.thinckness = 0.5, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/auth/lib/ui/common/dynamic_fab.dart b/auth/lib/ui/common/dynamic_fab.dart index 0f9771627a..8ccd9b8554 100644 --- a/auth/lib/ui/common/dynamic_fab.dart +++ b/auth/lib/ui/common/dynamic_fab.dart @@ -10,12 +10,12 @@ class DynamicFAB extends StatelessWidget { final Function? onPressedFunction; const DynamicFAB({ - Key? key, + super.key, this.isKeypadOpen, this.buttonText, this.isFormValid, this.onPressedFunction, - }) : super(key: key); + }); @override Widget build(BuildContext context) { @@ -60,6 +60,7 @@ class DynamicFAB extends StatelessWidget { } else { return Container( width: double.infinity, + height: 56, padding: const EdgeInsets.symmetric(horizontal: 20), child: OutlinedButton( onPressed: diff --git a/auth/lib/ui/common/gradient_button.dart b/auth/lib/ui/common/gradient_button.dart index 5b021f6133..8a24c68325 100644 --- a/auth/lib/ui/common/gradient_button.dart +++ b/auth/lib/ui/common/gradient_button.dart @@ -1,5 +1,3 @@ - - import 'package:flutter/material.dart'; class GradientButton extends StatelessWidget { @@ -15,17 +13,21 @@ class GradientButton extends StatelessWidget { // padding between the text and icon final double paddingValue; + // used when two icons are in row + final bool reversedGradient; + const GradientButton({ - Key? key, + super.key, this.linearGradientColors = const [ Color.fromARGB(255, 133, 44, 210), Color.fromARGB(255, 187, 26, 93), ], + this.reversedGradient = false, this.onTap, this.text = '', this.iconData, this.paddingValue = 0.0, - }) : super(key: key); + }); @override Widget build(BuildContext context) { @@ -71,7 +73,9 @@ class GradientButton extends StatelessWidget { gradient: LinearGradient( begin: const Alignment(0.1, -0.9), end: const Alignment(-0.6, 0.9), - colors: linearGradientColors, + colors: reversedGradient + ? linearGradientColors.reversed.toList() + : linearGradientColors, ), borderRadius: BorderRadius.circular(8), ), diff --git a/auth/lib/ui/common/linear_progress_dialog.dart b/auth/lib/ui/common/linear_progress_dialog.dart index 85bcc8c5b2..08c46d6c97 100644 --- a/auth/lib/ui/common/linear_progress_dialog.dart +++ b/auth/lib/ui/common/linear_progress_dialog.dart @@ -1,12 +1,10 @@ - - import 'package:ente_auth/ente_theme_data.dart'; import 'package:flutter/material.dart'; class LinearProgressDialog extends StatefulWidget { final String message; - const LinearProgressDialog(this.message, {Key? key}) : super(key: key); + const LinearProgressDialog(this.message, {super.key}); @override LinearProgressDialogState createState() => LinearProgressDialogState(); @@ -29,8 +27,8 @@ class LinearProgressDialogState extends State { @override Widget build(BuildContext context) { - return WillPopScope( - onWillPop: () async => false, + return PopScope( + canPop: false, child: AlertDialog( title: Text( widget.message, diff --git a/auth/lib/ui/common/loading_widget.dart b/auth/lib/ui/common/loading_widget.dart index cf38f3e6b2..d375486205 100644 --- a/auth/lib/ui/common/loading_widget.dart +++ b/auth/lib/ui/common/loading_widget.dart @@ -11,8 +11,8 @@ class EnteLoadingWidget extends StatelessWidget { this.size = 14, this.padding = 5, this.alignment = Alignment.center, - Key? key, - }) : super(key: key); + super.key, + }); @override Widget build(BuildContext context) { diff --git a/auth/lib/ui/common/progress_dialog.dart b/auth/lib/ui/common/progress_dialog.dart index 269a2b9b3e..adabff2ee0 100644 --- a/auth/lib/ui/common/progress_dialog.dart +++ b/auth/lib/ui/common/progress_dialog.dart @@ -153,8 +153,8 @@ class ProgressDialog { barrierColor: _barrierColor, builder: (BuildContext context) { _dismissingContext = context; - return WillPopScope( - onWillPop: () async => _barrierDismissible, + return PopScope( + canPop: _barrierDismissible, child: Dialog( backgroundColor: _backgroundColor, insetAnimationCurve: _insetAnimCurve, @@ -198,6 +198,7 @@ class _Body extends StatefulWidget { @override State createState() { + // ignore: no_logic_in_create_state return _dialog; } } diff --git a/auth/lib/ui/common/rename_dialog.dart b/auth/lib/ui/common/rename_dialog.dart index 03c68d3b10..ad93d1abaa 100644 --- a/auth/lib/ui/common/rename_dialog.dart +++ b/auth/lib/ui/common/rename_dialog.dart @@ -8,8 +8,7 @@ class RenameDialog extends StatefulWidget { final String type; final int maxLength; - const RenameDialog(this.name, this.type, {Key? key, this.maxLength = 100}) - : super(key: key); + const RenameDialog(this.name, this.type, {super.key, this.maxLength = 100}); @override State createState() => _RenameDialogState(); diff --git a/auth/lib/ui/common/web_page.dart b/auth/lib/ui/common/web_page.dart index 838855c7f5..a714bbeb26 100644 --- a/auth/lib/ui/common/web_page.dart +++ b/auth/lib/ui/common/web_page.dart @@ -6,7 +6,7 @@ class WebPage extends StatefulWidget { final String title; final String url; - const WebPage(this.title, this.url, {Key? key}) : super(key: key); + const WebPage(this.title, this.url, {super.key}); @override State createState() => _WebPageState(); @@ -28,9 +28,9 @@ class _WebPageState extends State { ), backgroundColor: Colors.black, body: InAppWebView( - initialUrlRequest: URLRequest(url: Uri.parse(widget.url)), - initialOptions: InAppWebViewGroupOptions( - crossPlatform: InAppWebViewOptions(transparentBackground: true), + initialUrlRequest: URLRequest(url: WebUri(widget.url)), + initialSettings: InAppWebViewSettings( + transparentBackground: true, ), onLoadStop: (c, url) { setState(() { diff --git a/auth/lib/ui/components/buttons/button_widget.dart b/auth/lib/ui/components/buttons/button_widget.dart index 28d62f4a66..1932cc02bd 100644 --- a/auth/lib/ui/components/buttons/button_widget.dart +++ b/auth/lib/ui/components/buttons/button_widget.dart @@ -57,7 +57,7 @@ class ButtonWidget extends StatelessWidget { final ValueNotifier? progressStatus; const ButtonWidget({ - Key? key, + super.key, required this.buttonType, this.buttonSize = ButtonSize.large, this.icon, @@ -71,7 +71,7 @@ class ButtonWidget extends StatelessWidget { this.shouldSurfaceExecutionStates = true, this.progressStatus, this.shouldShowSuccessConfirmation = false, - }) : super(key: key); + }); @override Widget build(BuildContext context) { @@ -155,7 +155,7 @@ class ButtonChildWidget extends StatefulWidget { final bool shouldShowSuccessConfirmation; const ButtonChildWidget({ - Key? key, + super.key, required this.buttonStyle, required this.buttonType, required this.isDisabled, @@ -168,7 +168,7 @@ class ButtonChildWidget extends StatefulWidget { this.labelText, this.icon, this.buttonAction, - }) : super(key: key); + }); @override State createState() => _ButtonChildWidgetState(); diff --git a/auth/lib/ui/components/buttons/icon_button_widget.dart b/auth/lib/ui/components/buttons/icon_button_widget.dart index 9b0591d4cc..eb5554318e 100644 --- a/auth/lib/ui/components/buttons/icon_button_widget.dart +++ b/auth/lib/ui/components/buttons/icon_button_widget.dart @@ -17,7 +17,7 @@ class IconButtonWidget extends StatefulWidget { final Color? pressedColor; final Color? iconColor; const IconButtonWidget({ - Key? key, + super.key, required this.icon, required this.iconButtonType, this.disableGestureDetector = false, @@ -25,7 +25,7 @@ class IconButtonWidget extends StatefulWidget { this.defaultColor, this.pressedColor, this.iconColor, - }) : super(key: key); + }); @override State createState() => _IconButtonWidgetState(); diff --git a/auth/lib/ui/components/captioned_text_widget.dart b/auth/lib/ui/components/captioned_text_widget.dart index f03c8551fd..438d906a96 100644 --- a/auth/lib/ui/components/captioned_text_widget.dart +++ b/auth/lib/ui/components/captioned_text_widget.dart @@ -13,8 +13,8 @@ class CaptionedTextWidget extends StatelessWidget { this.textStyle, this.makeTextBold = false, this.textColor, - Key? key, - }) : super(key: key); + super.key, + }); @override Widget build(BuildContext context) { diff --git a/auth/lib/ui/components/divider_widget.dart b/auth/lib/ui/components/divider_widget.dart index 489d591459..657eb024f9 100644 --- a/auth/lib/ui/components/divider_widget.dart +++ b/auth/lib/ui/components/divider_widget.dart @@ -14,12 +14,12 @@ class DividerWidget extends StatelessWidget { final bool divColorHasBlur; final EdgeInsets? padding; const DividerWidget({ - Key? key, + super.key, required this.dividerType, this.bgColor = Colors.transparent, this.divColorHasBlur = true, this.padding, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/auth/lib/ui/components/expandable_menu_item_widget.dart b/auth/lib/ui/components/expandable_menu_item_widget.dart index 9628078be4..0d3bce9286 100644 --- a/auth/lib/ui/components/expandable_menu_item_widget.dart +++ b/auth/lib/ui/components/expandable_menu_item_widget.dart @@ -13,8 +13,8 @@ class ExpandableMenuItemWidget extends StatefulWidget { required this.title, required this.selectionOptionsWidget, required this.leadingIcon, - Key? key, - }) : super(key: key); + super.key, + }); @override State createState() => diff --git a/auth/lib/ui/components/home_header_widget.dart b/auth/lib/ui/components/home_header_widget.dart index 268084eefa..0079cc7fa7 100644 --- a/auth/lib/ui/components/home_header_widget.dart +++ b/auth/lib/ui/components/home_header_widget.dart @@ -1,13 +1,10 @@ -import 'dart:ui'; - import 'package:ente_auth/core/event_bus.dart'; import 'package:ente_auth/events/opened_settings_event.dart'; import 'package:flutter/material.dart'; class HomeHeaderWidget extends StatefulWidget { final Widget centerWidget; - const HomeHeaderWidget({required this.centerWidget, Key? key}) - : super(key: key); + const HomeHeaderWidget({required this.centerWidget, super.key}); @override State createState() => _HomeHeaderWidgetState(); @@ -16,7 +13,7 @@ class HomeHeaderWidget extends StatefulWidget { class _HomeHeaderWidgetState extends State { @override Widget build(BuildContext context) { - final hasNotch = window.viewPadding.top > 65; + final hasNotch = View.of(context).viewPadding.top > 65; return Padding( padding: EdgeInsets.fromLTRB(4, hasNotch ? 4 : 8, 4, 4), child: Row( diff --git a/auth/lib/ui/components/menu_item_child_widgets.dart b/auth/lib/ui/components/menu_item_child_widgets.dart index 81b59bc542..1201f5e1ed 100644 --- a/auth/lib/ui/components/menu_item_child_widgets.dart +++ b/auth/lib/ui/components/menu_item_child_widgets.dart @@ -12,7 +12,7 @@ class TrailingWidget extends StatefulWidget { final double trailingExtraMargin; final bool showExecutionStates; const TrailingWidget({ - Key? key, + super.key, required this.executionStateNotifier, this.trailingIcon, this.trailingIconColor, @@ -20,7 +20,7 @@ class TrailingWidget extends StatefulWidget { required this.trailingIconIsMuted, required this.trailingExtraMargin, required this.showExecutionStates, - }) : super(key: key); + }); @override State createState() => _TrailingWidgetState(); } @@ -101,11 +101,11 @@ class ExpansionTrailingIcon extends StatelessWidget { final IconData? trailingIcon; final Color? trailingIconColor; const ExpansionTrailingIcon({ - Key? key, + super.key, required this.isExpanded, this.trailingIcon, this.trailingIconColor, - }) : super(key: key); + }); @override Widget build(BuildContext context) { @@ -138,12 +138,12 @@ class LeadingWidget extends StatelessWidget { // leadIconSize deafult value is 20. final double leadingIconSize; const LeadingWidget({ - Key? key, + super.key, required this.leadingIconSize, this.leadingIcon, this.leadingIconColor, this.leadingIconWidget, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/auth/lib/ui/components/menu_item_widget.dart b/auth/lib/ui/components/menu_item_widget.dart index ef2249f2ae..f281edd333 100644 --- a/auth/lib/ui/components/menu_item_widget.dart +++ b/auth/lib/ui/components/menu_item_widget.dart @@ -86,8 +86,8 @@ class MenuItemWidget extends StatefulWidget { this.showOnlyLoadingState = false, this.surfaceExecutionStates = true, this.alwaysShowSuccessState = false, - Key? key, - }) : super(key: key); + super.key, + }); @override State createState() => _MenuItemWidgetState(); diff --git a/auth/lib/ui/components/menu_section_description_widget.dart b/auth/lib/ui/components/menu_section_description_widget.dart index 6e521a5361..bcbb9f2fab 100644 --- a/auth/lib/ui/components/menu_section_description_widget.dart +++ b/auth/lib/ui/components/menu_section_description_widget.dart @@ -3,8 +3,7 @@ import 'package:flutter/material.dart'; class MenuSectionDescriptionWidget extends StatelessWidget { final String content; - const MenuSectionDescriptionWidget({Key? key, required this.content}) - : super(key: key); + const MenuSectionDescriptionWidget({super.key, required this.content}); @override Widget build(BuildContext context) { diff --git a/auth/lib/ui/components/notification_warning_widget.dart b/auth/lib/ui/components/notification_warning_widget.dart index a799ffcd3d..ab5e6dec1a 100644 --- a/auth/lib/ui/components/notification_warning_widget.dart +++ b/auth/lib/ui/components/notification_warning_widget.dart @@ -21,14 +21,14 @@ class NotificationWidget extends StatelessWidget { final NotificationType type; const NotificationWidget({ - Key? key, + super.key, required this.startIcon, required this.actionIcon, required this.text, required this.onTap, this.subText, this.type = NotificationType.warning, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/auth/lib/ui/components/title_bar_title_widget.dart b/auth/lib/ui/components/title_bar_title_widget.dart index bc20fe5b1d..0aa2534aa2 100644 --- a/auth/lib/ui/components/title_bar_title_widget.dart +++ b/auth/lib/ui/components/title_bar_title_widget.dart @@ -6,11 +6,11 @@ class TitleBarTitleWidget extends StatelessWidget { final bool isTitleH2; final IconData? icon; const TitleBarTitleWidget({ - Key? key, + super.key, this.title, this.isTitleH2 = false, this.icon, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/auth/lib/ui/components/title_bar_widget.dart b/auth/lib/ui/components/title_bar_widget.dart index 8e22d4f65f..16b532b967 100644 --- a/auth/lib/ui/components/title_bar_widget.dart +++ b/auth/lib/ui/components/title_bar_widget.dart @@ -14,7 +14,7 @@ class TitleBarWidget extends StatelessWidget { final bool isOnTopOfScreen; final Color? backgroundColor; const TitleBarWidget({ - Key? key, + super.key, this.leading, this.title, this.caption, @@ -25,7 +25,7 @@ class TitleBarWidget extends StatelessWidget { this.isFlexibleSpaceDisabled = false, this.isOnTopOfScreen = true, this.backgroundColor, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/auth/lib/ui/components/toggle_switch_widget.dart b/auth/lib/ui/components/toggle_switch_widget.dart index 8ae99f77bd..3fc0d153cc 100644 --- a/auth/lib/ui/components/toggle_switch_widget.dart +++ b/auth/lib/ui/components/toggle_switch_widget.dart @@ -13,8 +13,8 @@ class ToggleSwitchWidget extends StatefulWidget { const ToggleSwitchWidget({ required this.value, required this.onChanged, - Key? key, - }) : super(key: key); + super.key, + }); @override State createState() => _ToggleSwitchWidgetState(); diff --git a/auth/lib/ui/home/coach_mark_widget.dart b/auth/lib/ui/home/coach_mark_widget.dart index f628fabd4e..b0e15d7d2b 100644 --- a/auth/lib/ui/home/coach_mark_widget.dart +++ b/auth/lib/ui/home/coach_mark_widget.dart @@ -21,37 +21,43 @@ class CoachMarkWidget extends StatelessWidget { children: [ Expanded( child: Container( + width: double.infinity, color: Theme.of(context).colorScheme.background.withOpacity(0.1), child: BackdropFilter( filter: ImageFilter.blur(sigmaX: 8, sigmaY: 8), - child: Column( + child: Row( mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, children: [ - const Icon( - Icons.swipe_left, - size: 42, - ), - const SizedBox( - height: 12, - ), - Text( - l10n.swipeHint, - style: Theme.of(context).textTheme.titleLarge, - textAlign: TextAlign.center, - ), - const SizedBox( - height: 16, - ), - SizedBox( - width: 160, - child: OutlinedButton( - onPressed: () async { - await PreferenceService.instance - .setHasShownCoachMark(true); - Bus.instance.fire(CodesUpdatedEvent()); - }, - child: Text(l10n.ok), - ), + Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon( + Icons.swipe_left, + size: 42, + ), + const SizedBox( + height: 24, + ), + Text( + l10n.swipeHint, + style: Theme.of(context).textTheme.titleLarge, + ), + const SizedBox( + height: 36, + ), + SizedBox( + width: 160, + child: OutlinedButton( + onPressed: () async { + await PreferenceService.instance + .setHasShownCoachMark(true); + Bus.instance.fire(CodesUpdatedEvent()); + }, + child: Text(l10n.ok), + ), + ), + ], ), ], ), diff --git a/auth/lib/ui/home/home_empty_state.dart b/auth/lib/ui/home/home_empty_state.dart index 8a185beefd..0768f04919 100644 --- a/auth/lib/ui/home/home_empty_state.dart +++ b/auth/lib/ui/home/home_empty_state.dart @@ -3,6 +3,7 @@ import 'package:ente_auth/theme/ente_theme.dart'; import 'package:ente_auth/ui/settings/data/import_page.dart'; import 'package:ente_auth/ui/settings/faq.dart'; import 'package:ente_auth/utils/navigation_util.dart'; +import 'package:ente_auth/utils/platform_util.dart'; import 'package:flutter/material.dart'; class HomeEmptyStateWidget extends StatelessWidget { @@ -10,86 +11,90 @@ class HomeEmptyStateWidget extends StatelessWidget { final VoidCallback? onManuallySetupTap; const HomeEmptyStateWidget({ - Key? key, + super.key, required this.onScanTap, required this.onManuallySetupTap, - }) : super(key: key); + }); @override Widget build(BuildContext context) { final l10n = context.l10n; return SingleChildScrollView( child: Center( - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 40.0, horizontal: 40), - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Column( - children: [ - Image.asset( - "assets/wallet-front-gradient.png", - width: 200, - height: 200, - ), - Text( - l10n.setupFirstAccount, - textAlign: TextAlign.center, - style: Theme.of(context).textTheme.headlineMedium, - ), - const SizedBox(height: 64), - SizedBox( - width: 400, - child: OutlinedButton( - onPressed: onScanTap, - child: Text(l10n.importScanQrCode), + child: ConstrainedBox( + constraints: const BoxConstraints.tightFor(height: 800, width: 450), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 40.0, horizontal: 40), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + children: [ + Image.asset( + "assets/wallet-front-gradient.png", + width: 200, + height: 200, ), - ), - const SizedBox(height: 18), - SizedBox( - width: 400, - child: OutlinedButton( - onPressed: onManuallySetupTap, - child: Text(l10n.importEnterSetupKey), - ), - ), - const SizedBox(height: 54), - InkWell( - onTap: () { - routeToPage(context, ImportCodePage()); - }, - child: Text( - l10n.importCodes, + Text( + l10n.setupFirstAccount, textAlign: TextAlign.center, - style: getEnteTextTheme(context) - .bodyFaint - .copyWith(decoration: TextDecoration.underline), + style: Theme.of(context).textTheme.headlineMedium, ), - ), - const SizedBox(height: 18), - InkWell( - onTap: () { - showModalBottomSheet( - backgroundColor: - Theme.of(context).colorScheme.background, - barrierColor: Colors.black87, - context: context, - builder: (context) { - return const FAQQuestionsWidget(); - }, - ); - }, - child: Text( - l10n.faq, - textAlign: TextAlign.center, - style: getEnteTextTheme(context) - .bodyFaint - .copyWith(decoration: TextDecoration.underline), + const SizedBox(height: 64), + if (PlatformUtil.isMobile()) + SizedBox( + width: 400, + child: OutlinedButton( + onPressed: onScanTap, + child: Text(l10n.importScanQrCode), + ), + ), + const SizedBox(height: 18), + SizedBox( + width: 400, + child: OutlinedButton( + onPressed: onManuallySetupTap, + child: Text(l10n.importEnterSetupKey), + ), ), - ), - ], - ), - ], + const SizedBox(height: 54), + InkWell( + onTap: () { + routeToPage(context, const ImportCodePage()); + }, + child: Text( + l10n.importCodes, + textAlign: TextAlign.center, + style: getEnteTextTheme(context) + .bodyFaint + .copyWith(decoration: TextDecoration.underline), + ), + ), + const SizedBox(height: 18), + InkWell( + onTap: () { + showModalBottomSheet( + backgroundColor: + Theme.of(context).colorScheme.background, + barrierColor: Colors.black87, + context: context, + builder: (context) { + return const FAQQuestionsWidget(); + }, + ); + }, + child: Text( + l10n.faq, + textAlign: TextAlign.center, + style: getEnteTextTheme(context) + .bodyFaint + .copyWith(decoration: TextDecoration.underline), + ), + ), + ], + ), + ], + ), ), ), ), diff --git a/auth/lib/ui/home/speed_dial_label_widget.dart b/auth/lib/ui/home/speed_dial_label_widget.dart index 40c945893e..4889ffb6a9 100644 --- a/auth/lib/ui/home/speed_dial_label_widget.dart +++ b/auth/lib/ui/home/speed_dial_label_widget.dart @@ -6,8 +6,8 @@ class SpeedDialLabelWidget extends StatelessWidget { const SpeedDialLabelWidget( this.label, { - Key? key, - }) : super(key: key); + super.key, + }); @override Widget build(BuildContext context) { diff --git a/auth/lib/ui/home_page.dart b/auth/lib/ui/home_page.dart index 792a4c917c..341e1ae696 100644 --- a/auth/lib/ui/home_page.dart +++ b/auth/lib/ui/home_page.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:io'; +import 'package:app_links/app_links.dart'; import 'package:ente_auth/core/configuration.dart'; import 'package:ente_auth/core/event_bus.dart'; import 'package:ente_auth/ente_theme_data.dart'; @@ -22,28 +23,32 @@ import 'package:ente_auth/ui/home/speed_dial_label_widget.dart'; import 'package:ente_auth/ui/scanner_page.dart'; import 'package:ente_auth/ui/settings_page.dart'; import 'package:ente_auth/utils/dialog_util.dart'; +import 'package:ente_auth/utils/platform_util.dart'; import 'package:ente_auth/utils/totp_util.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_speed_dial/flutter_speed_dial.dart'; +import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; import 'package:logging/logging.dart'; import 'package:move_to_background/move_to_background.dart'; -import 'package:uni_links/uni_links.dart'; class HomePage extends StatefulWidget { - const HomePage({Key? key}) : super(key: key); + const HomePage({super.key}); @override State createState() => _HomePageState(); } class _HomePageState extends State { - static final _settingsPage = SettingsPage( + late final _settingsPage = SettingsPage( emailNotifier: UserService.instance.emailValueNotifier, + scaffoldKey: scaffoldKey, ); bool _hasLoaded = false; bool _isSettingsOpen = false; final Logger _logger = Logger("HomePage"); + final scaffoldKey = GlobalKey(); final TextEditingController _textController = TextEditingController(); bool _showSearchBox = false; @@ -144,28 +149,24 @@ class _HomePageState extends State { @override Widget build(BuildContext context) { final l10n = context.l10n; - return WillPopScope( - onWillPop: () async { + return PopScope( + onPopInvoked: (_) async { if (_isSettingsOpen) { - Navigator.pop(context); - return false; - } - if (Platform.isAndroid) { - // ignore: unawaited_futures - MoveToBackground.moveTaskToBack(); - return false; - } else { - return true; + scaffoldKey.currentState!.closeDrawer(); + return; + } else if (!Platform.isAndroid) { + Navigator.of(context).pop(); + return; } + await MoveToBackground.moveTaskToBack(); }, + canPop: false, child: Scaffold( + key: scaffoldKey, drawerEnableOpenDragGesture: !Platform.isAndroid, - drawer: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 428), - child: Drawer( - width: double.infinity, - child: _settingsPage, - ), + drawer: Drawer( + width: 428, + child: _settingsPage, ), onDrawerChanged: (isOpened) => _isSettingsOpen = isOpened, body: SafeArea( @@ -179,7 +180,7 @@ class _HomePageState extends State { resizeToAvoidBottomInset: false, appBar: AppBar( title: !_showSearchBox - ? const Text('ente Auth') + ? const Text('Ente Auth') : TextField( autofocus: _searchText.isEmpty, controller: _textController, @@ -205,6 +206,7 @@ class _HomePageState extends State { _showSearchBox = !_showSearchBox; if (!_showSearchBox) { _textController.clear(); + _searchText = ""; } else { _searchText = _textController.text; } @@ -233,10 +235,13 @@ class _HomePageState extends State { onManuallySetupTap: _redirectToManualEntryPage, ); } else { - final list = ListView.builder( + final list = AlignedGridView.count( + crossAxisCount: (MediaQuery.sizeOf(context).width ~/ 400) + .clamp(1, double.infinity) + .toInt(), itemBuilder: ((context, index) { try { - return CodeWidget(_filteredCodes[index]); + return ClipRect(child: CodeWidget(_filteredCodes[index])); } catch (e) { return const Text("Failed"); } @@ -255,7 +260,11 @@ class _HomePageState extends State { children: [ Expanded( child: _filteredCodes.isNotEmpty - ? ListView.builder( + ? AlignedGridView.count( + crossAxisCount: + (MediaQuery.sizeOf(context).width ~/ 400) + .clamp(1, double.infinity) + .toInt(), itemBuilder: ((context, index) { Code? code; try { @@ -290,8 +299,10 @@ class _HomePageState extends State { Future _initDeepLinks() async { // Platform messages may fail, so we use a try/catch PlatformException. + final appLinks = AppLinks(); try { - final String? initialLink = await getInitialLink(); + String? initialLink; + initialLink = await appLinks.getInitialAppLinkString(); // Parse the link and warn the user, if it is not correct, // but keep in mind it could be `null`. if (initialLink != null) { @@ -307,14 +318,16 @@ class _HomePageState extends State { } // Attach a listener to the stream - linkStream.listen( - (String? link) { - _handleDeeplink(context, link); - }, - onError: (err) { - _logger.severe(err); - }, - ); + if (!kIsWeb && !Platform.isLinux) { + appLinks.stringLinkStream.listen( + (link) { + _handleDeeplink(context, link); + }, + onError: (err) { + _logger.severe(err); + }, + ); + } return false; } @@ -343,6 +356,14 @@ class _HomePageState extends State { } Widget _getFab() { + if (PlatformUtil.isDesktop()) { + return FloatingActionButton( + onPressed: () => _redirectToManualEntryPage(), + child: const Icon(Icons.add), + elevation: 8.0, + shape: const CircleBorder(), + ); + } return SpeedDial( icon: Icons.add, activeIcon: Icons.close, diff --git a/auth/lib/ui/linear_progress_widget.dart b/auth/lib/ui/linear_progress_widget.dart index 6296e5d4ab..a5dbb2140e 100644 --- a/auth/lib/ui/linear_progress_widget.dart +++ b/auth/lib/ui/linear_progress_widget.dart @@ -6,8 +6,8 @@ class LinearProgressWidget extends StatelessWidget { const LinearProgressWidget({ required this.color, required this.fractionOfStorage, - Key? key, - }) : super(key: key); + super.key, + }); @override Widget build(BuildContext context) { diff --git a/auth/lib/ui/passkey_page.dart b/auth/lib/ui/passkey_page.dart index 3c60af36e6..8c2e54e980 100644 --- a/auth/lib/ui/passkey_page.dart +++ b/auth/lib/ui/passkey_page.dart @@ -1,5 +1,6 @@ import 'dart:convert'; +import 'package:app_links/app_links.dart'; import 'package:ente_auth/core/configuration.dart'; import 'package:ente_auth/l10n/l10n.dart'; import 'package:ente_auth/models/account/two_factor.dart'; @@ -9,7 +10,6 @@ import 'package:ente_auth/ui/components/models/button_type.dart'; import 'package:ente_auth/utils/dialog_util.dart'; import 'package:flutter/material.dart'; import 'package:logging/logging.dart'; -import 'package:uni_links/uni_links.dart'; import 'package:url_launcher/url_launcher_string.dart'; class PasskeyPage extends StatefulWidget { @@ -17,8 +17,8 @@ class PasskeyPage extends StatefulWidget { const PasskeyPage( this.sessionID, { - Key? key, - }) : super(key: key); + super.key, + }); @override State createState() => _PasskeyPageState(); @@ -77,8 +77,9 @@ class _PasskeyPageState extends State { } Future _initDeepLinks() async { + final appLinks = AppLinks(); // Attach a listener to the stream - linkStream.listen( + appLinks.stringLinkStream.listen( _handleDeeplink, onError: (err) { _logger.severe(err); diff --git a/auth/lib/ui/scanner_gauth_page.dart b/auth/lib/ui/scanner_gauth_page.dart index 785f1d3d3a..41d0a762dd 100644 --- a/auth/lib/ui/scanner_gauth_page.dart +++ b/auth/lib/ui/scanner_gauth_page.dart @@ -9,7 +9,7 @@ import 'package:flutter/material.dart'; import 'package:qr_code_scanner/qr_code_scanner.dart'; class ScannerGoogleAuthPage extends StatefulWidget { - const ScannerGoogleAuthPage({Key? key}) : super(key: key); + const ScannerGoogleAuthPage({super.key}); @override State createState() => ScannerGoogleAuthPageState(); @@ -85,7 +85,7 @@ class ScannerGoogleAuthPageState extends State { } catch (e) { controller.dispose(); Navigator.of(context).pop(); - showToast(context, "Error " + e.toString()); + showToast(context, "Error $e"); } }); } diff --git a/auth/lib/ui/scanner_page.dart b/auth/lib/ui/scanner_page.dart index 0159f0cfab..6a77936316 100644 --- a/auth/lib/ui/scanner_page.dart +++ b/auth/lib/ui/scanner_page.dart @@ -6,7 +6,7 @@ import 'package:flutter/material.dart'; import 'package:qr_code_scanner/qr_code_scanner.dart'; class ScannerPage extends StatefulWidget { - const ScannerPage({Key? key}) : super(key: key); + const ScannerPage({super.key}); @override State createState() => ScannerPageState(); diff --git a/auth/lib/ui/settings/about_section_widget.dart b/auth/lib/ui/settings/about_section_widget.dart index 4fcd2c27cd..a96e1f0ad3 100644 --- a/auth/lib/ui/settings/about_section_widget.dart +++ b/auth/lib/ui/settings/about_section_widget.dart @@ -1,19 +1,19 @@ import 'package:ente_auth/l10n/l10n.dart'; import 'package:ente_auth/services/update_service.dart'; import 'package:ente_auth/theme/ente_theme.dart'; -import 'package:ente_auth/ui/common/web_page.dart'; import 'package:ente_auth/ui/components/captioned_text_widget.dart'; import 'package:ente_auth/ui/components/expandable_menu_item_widget.dart'; import 'package:ente_auth/ui/components/menu_item_widget.dart'; import 'package:ente_auth/ui/settings/app_update_dialog.dart'; import 'package:ente_auth/ui/settings/common_settings.dart'; import 'package:ente_auth/utils/dialog_util.dart'; +import 'package:ente_auth/utils/platform_util.dart'; import 'package:ente_auth/utils/toast_util.dart'; import 'package:flutter/material.dart'; import 'package:url_launcher/url_launcher.dart'; class AboutSectionWidget extends StatelessWidget { - const AboutSectionWidget({Key? key}) : super(key: key); + const AboutSectionWidget({super.key}); @override Widget build(BuildContext context) { @@ -104,8 +104,8 @@ class AboutMenuItemWidget extends StatelessWidget { required this.title, required this.url, this.webPageTitle, - Key? key, - }) : super(key: key); + super.key, + }); @override Widget build(BuildContext context) { @@ -117,13 +117,10 @@ class AboutMenuItemWidget extends StatelessWidget { trailingIcon: Icons.chevron_right_outlined, trailingIconIsMuted: true, onTap: () async { - // ignore: unawaited_futures - Navigator.of(context).push( - MaterialPageRoute( - builder: (BuildContext context) { - return WebPage(webPageTitle ?? title, url); - }, - ), + await PlatformUtil.openWebView( + context, + webPageTitle ?? title, + url, ); }, ); diff --git a/auth/lib/ui/settings/account_section_widget.dart b/auth/lib/ui/settings/account_section_widget.dart index 60e426b090..d51b2dd873 100644 --- a/auth/lib/ui/settings/account_section_widget.dart +++ b/auth/lib/ui/settings/account_section_widget.dart @@ -13,11 +13,12 @@ import 'package:ente_auth/ui/components/menu_item_widget.dart'; import 'package:ente_auth/ui/settings/common_settings.dart'; import 'package:ente_auth/utils/dialog_util.dart'; import 'package:ente_auth/utils/navigation_util.dart'; +import 'package:ente_auth/utils/platform_util.dart'; +import 'package:ente_crypto_dart/ente_crypto_dart.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_sodium/flutter_sodium.dart'; class AccountSectionWidget extends StatelessWidget { - AccountSectionWidget({Key? key}) : super(key: key); + AccountSectionWidget({super.key}); @override Widget build(BuildContext context) { @@ -47,6 +48,7 @@ class AccountSectionWidget extends StatelessWidget { context, l10n.authToChangeYourEmail, ); + await PlatformUtil.refocusWindows(); if (hasAuthenticated) { // ignore: unawaited_futures showDialog( @@ -106,7 +108,7 @@ class AccountSectionWidget extends StatelessWidget { String recoveryKey; try { recoveryKey = - Sodium.bin2hex(Configuration.instance.getRecoveryKey()); + CryptoUtil.bin2hex(Configuration.instance.getRecoveryKey()); } catch (e) { // ignore: unawaited_futures showGenericErrorDialog(context: context); diff --git a/auth/lib/ui/settings/app_update_dialog.dart b/auth/lib/ui/settings/app_update_dialog.dart index 12049017c2..176abc4b19 100644 --- a/auth/lib/ui/settings/app_update_dialog.dart +++ b/auth/lib/ui/settings/app_update_dialog.dart @@ -1,13 +1,14 @@ import 'package:ente_auth/l10n/l10n.dart'; import 'package:ente_auth/services/update_service.dart'; import 'package:ente_auth/theme/ente_theme.dart'; +import 'package:ente_auth/utils/platform_util.dart'; import 'package:flutter/material.dart'; import 'package:url_launcher/url_launcher_string.dart'; class AppUpdateDialog extends StatefulWidget { final LatestVersionInfo? latestVersionInfo; - const AppUpdateDialog(this.latestVersionInfo, {Key? key}) : super(key: key); + const AppUpdateDialog(this.latestVersionInfo, {super.key}); @override State createState() => _AppUpdateDialogState(); @@ -23,7 +24,7 @@ class _AppUpdateDialogState extends State { Padding( padding: const EdgeInsets.fromLTRB(8, 4, 0, 4), child: Text( - "- " + log, + "- $log", style: Theme.of(context).textTheme.bodySmall!.copyWith( fontSize: 14, ), @@ -68,7 +69,9 @@ class _AppUpdateDialogState extends State { ), ), onPressed: () => launchUrlString( - widget.latestVersionInfo!.url!, + PlatformUtil.isDesktop() + ? widget.latestVersionInfo!.release! + : widget.latestVersionInfo!.url!, mode: LaunchMode.externalApplication, ), child: Text( @@ -80,8 +83,8 @@ class _AppUpdateDialogState extends State { ); final shouldForceUpdate = UpdateService.instance.shouldForceUpdate(widget.latestVersionInfo); - return WillPopScope( - onWillPop: () async => !shouldForceUpdate, + return PopScope( + canPop: !shouldForceUpdate, child: AlertDialog( title: Column( crossAxisAlignment: CrossAxisAlignment.start, diff --git a/auth/lib/ui/settings/app_version_widget.dart b/auth/lib/ui/settings/app_version_widget.dart index 3b15c6480f..58c693f7c7 100644 --- a/auth/lib/ui/settings/app_version_widget.dart +++ b/auth/lib/ui/settings/app_version_widget.dart @@ -4,8 +4,8 @@ import 'package:package_info_plus/package_info_plus.dart'; class AppVersionWidget extends StatefulWidget { const AppVersionWidget({ - Key? key, - }) : super(key: key); + super.key, + }); @override State createState() => _AppVersionWidgetState(); @@ -48,7 +48,7 @@ class _AppVersionWidgetState extends State { return Padding( padding: const EdgeInsets.all(20), child: Text( - "Version: " + snapshot.data!, + "Version: ${snapshot.data!}", style: Theme.of(context).textTheme.bodySmall, ), ); diff --git a/auth/lib/ui/settings/danger_section_widget.dart b/auth/lib/ui/settings/danger_section_widget.dart index 2e7eccc16c..4f8160c38e 100644 --- a/auth/lib/ui/settings/danger_section_widget.dart +++ b/auth/lib/ui/settings/danger_section_widget.dart @@ -11,7 +11,7 @@ import 'package:ente_auth/utils/navigation_util.dart'; import 'package:flutter/material.dart'; class DangerSectionWidget extends StatelessWidget { - const DangerSectionWidget({Key? key}) : super(key: key); + const DangerSectionWidget({super.key}); @override Widget build(BuildContext context) { diff --git a/auth/lib/ui/settings/data/data_section_widget.dart b/auth/lib/ui/settings/data/data_section_widget.dart index f9c3ed1171..f32739d239 100644 --- a/auth/lib/ui/settings/data/data_section_widget.dart +++ b/auth/lib/ui/settings/data/data_section_widget.dart @@ -10,7 +10,9 @@ import 'package:ente_auth/utils/navigation_util.dart'; import 'package:flutter/material.dart'; class DataSectionWidget extends StatelessWidget { - DataSectionWidget({Key? key}) : super(key: key); + // final _logger = Logger("AccountSectionWidget"); + + DataSectionWidget({super.key}); @override Widget build(BuildContext context) { @@ -35,8 +37,7 @@ class DataSectionWidget extends StatelessWidget { trailingIcon: Icons.chevron_right_outlined, trailingIconIsMuted: true, onTap: () async { - // ignore: unawaited_futures - routeToPage(context, ImportCodePage()); + await routeToPage(context, const ImportCodePage()); }, ), sectionOptionSpacing, diff --git a/auth/lib/ui/settings/data/export_widget.dart b/auth/lib/ui/settings/data/export_widget.dart index c9066ccce1..ef438301cf 100644 --- a/auth/lib/ui/settings/data/export_widget.dart +++ b/auth/lib/ui/settings/data/export_widget.dart @@ -9,14 +9,14 @@ import 'package:ente_auth/store/code_store.dart'; import 'package:ente_auth/ui/components/buttons/button_widget.dart'; import 'package:ente_auth/ui/components/dialog_widget.dart'; import 'package:ente_auth/ui/components/models/button_type.dart'; -import 'package:ente_auth/utils/crypto_util.dart'; import 'package:ente_auth/utils/dialog_util.dart'; +import 'package:ente_auth/utils/platform_util.dart'; +import 'package:ente_auth/utils/share_utils.dart'; import 'package:ente_auth/utils/toast_util.dart'; +import 'package:ente_crypto_dart/ente_crypto_dart.dart'; import 'package:file_saver/file_saver.dart'; import 'package:flutter/cupertino.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_sodium/flutter_sodium.dart'; import 'package:intl/intl.dart'; import 'package:logging/logging.dart'; import 'package:share_plus/share_plus.dart'; @@ -76,17 +76,17 @@ Future _requestForEncryptionPassword( try { final kekSalt = CryptoUtil.getSaltToDeriveKey(); final derivedKeyResult = await CryptoUtil.deriveSensitiveKey( - utf8.encode(password) as Uint8List, + utf8.encode(password), kekSalt, ); String exportPlainText = await _getAuthDataForExport(); // Encrypt the key with this derived key - final encResult = await CryptoUtil.encryptChaCha( - utf8.encode(exportPlainText) as Uint8List, + final encResult = await CryptoUtil.encryptData( + utf8.encode(exportPlainText), derivedKeyResult.key, ); - final encContent = Sodium.bin2base64(encResult.encryptedData!); - final encNonce = Sodium.bin2base64(encResult.header!); + final encContent = CryptoUtil.bin2base64(encResult.encryptedData!); + final encNonce = CryptoUtil.bin2base64(encResult.header!); final EnteAuthExport data = EnteAuthExport( version: 1, encryptedData: encContent, @@ -94,7 +94,7 @@ Future _requestForEncryptionPassword( kdfParams: KDFParams( memLimit: derivedKeyResult.memLimit, opsLimit: derivedKeyResult.opsLimit, - salt: Sodium.bin2base64(kekSalt), + salt: CryptoUtil.bin2base64(kekSalt), ), ); // get json value of data @@ -126,46 +126,55 @@ Future _showExportWarningDialog(BuildContext context) async { Future _exportCodes(BuildContext context, String fileContent) async { DateTime now = DateTime.now().toUtc(); String formattedDate = DateFormat('yyyy-MM-dd').format(now); - String exportFileName = 'ente-auth-codes-$formattedDate.txt'; - final _codeFile = File( - Configuration.instance.getTempDirectory() + exportFileName, - ); + String exportFileName = 'ente-auth-codes-$formattedDate'; + String exportFileExtension = 'txt'; final hasAuthenticated = await LocalAuthenticationService.instance .requestLocalAuthentication(context, context.l10n.authToExportCodes); + await PlatformUtil.refocusWindows(); if (!hasAuthenticated) { return; } - if (_codeFile.existsSync()) { - await _codeFile.delete(); - } - _codeFile.writeAsStringSync(fileContent); - final Size size = MediaQuery.of(context).size; - - if (Platform.isAndroid) { - await FileSaver.instance.saveAs( - name: exportFileName, - filePath: _codeFile.path, - mimeType: MimeType.text, - ext: 'txt', - ); - } else { - await Share.shareFiles( - [_codeFile.path], - sharePositionOrigin: Rect.fromLTWH(0, 0, size.width, size.height / 2), - ); - } - Future.delayed(const Duration(seconds: 30), () async { - if (_codeFile.existsSync()) { - _codeFile.deleteSync(); - } - }); + Future.delayed( + const Duration(milliseconds: 1200), + () async => await shareDialog( + context, + context.l10n.exportCodes, + saveAction: () async { + await PlatformUtil.shareFile( + exportFileName, + exportFileExtension, + CryptoUtil.strToBin(fileContent), + MimeType.text, + ); + }, + sendAction: () async { + final codeFile = File( + "${Configuration.instance.getTempDirectory()}$exportFileName.$exportFileExtension", + ); + if (codeFile.existsSync()) { + await codeFile.delete(); + } + codeFile.writeAsStringSync(fileContent); + final Size size = MediaQuery.of(context).size; + await Share.shareXFiles( + [XFile(codeFile.path)], + sharePositionOrigin: Rect.fromLTWH(0, 0, size.width, size.height / 2), + ); + Future.delayed(const Duration(seconds: 30), () async { + if (codeFile.existsSync()) { + codeFile.deleteSync(); + } + }); + }, + ), + ); } Future _getAuthDataForExport() async { final codes = await CodeStore.instance.getAllCodes(); String data = ""; for (final code in codes) { - data += code.rawData + "\n"; + data += "${code.rawData}\n"; } return data; } diff --git a/auth/lib/ui/settings/data/import/aegis_import.dart b/auth/lib/ui/settings/data/import/aegis_import.dart index 5042d618f5..b801e64a5f 100644 --- a/auth/lib/ui/settings/data/import/aegis_import.dart +++ b/auth/lib/ui/settings/data/import/aegis_import.dart @@ -91,7 +91,7 @@ Future _processAegisExportFile( final jsonString = await file.readAsString(); final decodedJson = jsonDecode(jsonString); final isEncrypted = decodedJson['header']['slots'] != null; - var aegisDB; + Map? aegisDB; if (isEncrypted) { String? password; try { @@ -127,7 +127,7 @@ Future _processAegisExportFile( aegisDB = decodedJson['db']; } final parsedCodes = []; - for (var item in aegisDB['entries']) { + for (var item in aegisDB?['entries']) { var kind = item['type']; var account = item['name']; var issuer = item['issuer']; diff --git a/auth/lib/ui/settings/data/import/encrypted_ente_import.dart b/auth/lib/ui/settings/data/import/encrypted_ente_import.dart index 15f8692064..511c9bbf96 100644 --- a/auth/lib/ui/settings/data/import/encrypted_ente_import.dart +++ b/auth/lib/ui/settings/data/import/encrypted_ente_import.dart @@ -11,21 +11,20 @@ import 'package:ente_auth/ui/components/buttons/button_widget.dart'; import 'package:ente_auth/ui/components/dialog_widget.dart'; import 'package:ente_auth/ui/components/models/button_type.dart'; import 'package:ente_auth/ui/settings/data/import/import_success.dart'; -import 'package:ente_auth/utils/crypto_util.dart'; import 'package:ente_auth/utils/dialog_util.dart'; import 'package:ente_auth/utils/toast_util.dart'; +import 'package:ente_crypto_dart/ente_crypto_dart.dart'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_sodium/flutter_sodium.dart'; import 'package:logging/logging.dart'; Future showEncryptedImportInstruction(BuildContext context) async { final l10n = context.l10n; final result = await showDialogWidget( context: context, - title: l10n.importFromApp("ente Auth"), + title: l10n.importFromApp("Ente Auth"), body: l10n.importEnteEncGuide, buttons: [ ButtonWidget( @@ -80,21 +79,21 @@ Future _decryptExportData( try { await progressDialog.show(); final derivedKey = await CryptoUtil.deriveKey( - utf8.encode(password) as Uint8List, - Sodium.base642bin(enteAuthExport.kdfParams.salt), + utf8.encode(password), + CryptoUtil.base642bin(enteAuthExport.kdfParams.salt), enteAuthExport.kdfParams.memLimit, enteAuthExport.kdfParams.opsLimit, ); Uint8List? decryptedContent; // Encrypt the key with this derived key try { - decryptedContent = await CryptoUtil.decryptChaCha( - Sodium.base642bin(enteAuthExport.encryptedData), + decryptedContent = await CryptoUtil.decryptData( + CryptoUtil.base642bin(enteAuthExport.encryptedData), derivedKey, - Sodium.base642bin(enteAuthExport.encryptionNonce), + CryptoUtil.base642bin(enteAuthExport.encryptionNonce), ); - } catch (e,s) { - Logger("encryptedImport").warning('failed to decrypt',e,s); + } catch (e, s) { + Logger("encryptedImport").warning('failed to decrypt', e, s); showToast(context, l10n.incorrectPasswordTitle); isPasswordIncorrect = true; } diff --git a/auth/lib/ui/settings/data/import/google_auth_import.dart b/auth/lib/ui/settings/data/import/google_auth_import.dart index 2c83bc206c..12df41a142 100644 --- a/auth/lib/ui/settings/data/import/google_auth_import.dart +++ b/auth/lib/ui/settings/data/import/google_auth_import.dart @@ -1,6 +1,5 @@ import 'dart:async'; import 'dart:convert'; -import 'dart:typed_data'; import 'package:base32/base32.dart'; import 'package:ente_auth/l10n/l10n.dart'; import 'package:ente_auth/models/code.dart'; @@ -12,6 +11,8 @@ import 'package:ente_auth/ui/components/dialog_widget.dart'; import 'package:ente_auth/ui/components/models/button_type.dart'; import 'package:ente_auth/ui/scanner_gauth_page.dart'; import 'package:ente_auth/ui/settings/data/import/import_success.dart'; +import 'package:ente_auth/utils/platform_util.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:logging/logging.dart'; @@ -24,13 +25,14 @@ Future showGoogleAuthInstruction(BuildContext context) async { title: l10n.importFromApp("Google Authenticator"), body: l10n.importGoogleAuthGuide, buttons: [ - ButtonWidget( - buttonType: ButtonType.primary, - labelText: l10n.scanAQrCode, - isInAlert: true, - buttonSize: ButtonSize.large, - buttonAction: ButtonAction.first, - ), + if (PlatformUtil.isMobile()) + ButtonWidget( + buttonType: ButtonType.primary, + labelText: l10n.scanAQrCode, + isInAlert: true, + buttonSize: ButtonSize.large, + buttonAction: ButtonAction.first, + ), ButtonWidget( buttonType: ButtonType.secondary, labelText: context.l10n.cancel, diff --git a/auth/lib/ui/settings/data/import/import_service.dart b/auth/lib/ui/settings/data/import/import_service.dart index 0e42197baa..a315c16796 100644 --- a/auth/lib/ui/settings/data/import/import_service.dart +++ b/auth/lib/ui/settings/data/import/import_service.dart @@ -1,4 +1,3 @@ -import 'package:ente_auth/ui/settings/data/import/2fas_import.dart'; import 'package:ente_auth/ui/settings/data/import/aegis_import.dart'; import 'package:ente_auth/ui/settings/data/import/bitwarden_import.dart'; import 'package:ente_auth/ui/settings/data/import/encrypted_ente_import.dart'; @@ -6,6 +5,7 @@ import 'package:ente_auth/ui/settings/data/import/google_auth_import.dart'; import 'package:ente_auth/ui/settings/data/import/lastpass_import.dart'; import 'package:ente_auth/ui/settings/data/import/plain_text_import.dart'; import 'package:ente_auth/ui/settings/data/import/raivo_plain_text_import.dart'; +import 'package:ente_auth/ui/settings/data/import/two_fas_import.dart'; import 'package:ente_auth/ui/settings/data/import_page.dart'; import 'package:flutter/cupertino.dart'; diff --git a/auth/lib/ui/settings/data/import/import_success.dart b/auth/lib/ui/settings/data/import/import_success.dart index cc77248e8b..5827248f8d 100644 --- a/auth/lib/ui/settings/data/import/import_success.dart +++ b/auth/lib/ui/settings/data/import/import_success.dart @@ -11,9 +11,9 @@ Future importSuccessDialog(BuildContext context, int count) async { firstButtonLabel: context.l10n.ok, firstButtonOnTap: () async { Navigator.of(context).pop(); - if(Navigator.of(context).canPop()) { - Navigator.of(context).pop(); - } + // if(Navigator.of(context).canPop()) { + // Navigator.of(context).pop(); + // } }, firstButtonType: ButtonType.primary, ); diff --git a/auth/lib/ui/settings/data/import/plain_text_import.dart b/auth/lib/ui/settings/data/import/plain_text_import.dart index a8e64bb5f7..03bc50dcea 100644 --- a/auth/lib/ui/settings/data/import/plain_text_import.dart +++ b/auth/lib/ui/settings/data/import/plain_text_import.dart @@ -1,6 +1,6 @@ import 'dart:async'; +import 'dart:convert'; import 'dart:io'; -import 'dart:ui'; import 'package:ente_auth/ente_theme_data.dart'; import 'package:ente_auth/l10n/l10n.dart'; @@ -48,10 +48,8 @@ class PlainTextImport extends StatelessWidget { ], ); } - } - Future showImportInstructionDialog(BuildContext context) async { final l10n = context.l10n; final AlertDialog alert = AlertDialog( @@ -94,7 +92,6 @@ Future showImportInstructionDialog(BuildContext context) async { ); } - Future _pickImportFile(BuildContext context) async { final l10n = context.l10n; FilePickerResult? result = await FilePicker.platform.pickFiles(); @@ -108,7 +105,7 @@ Future _pickImportFile(BuildContext context) async { final codes = await file.readAsString(); List splitCodes = codes.split(","); if (splitCodes.length == 1) { - splitCodes = codes.split("\n"); + splitCodes = const LineSplitter().convert(codes); } final parsedCodes = []; for (final code in splitCodes) { diff --git a/auth/lib/ui/settings/data/import/2fas_import.dart b/auth/lib/ui/settings/data/import/two_fas_import.dart similarity index 98% rename from auth/lib/ui/settings/data/import/2fas_import.dart rename to auth/lib/ui/settings/data/import/two_fas_import.dart index b6e75f3c60..ae5a05b0bb 100644 --- a/auth/lib/ui/settings/data/import/2fas_import.dart +++ b/auth/lib/ui/settings/data/import/two_fas_import.dart @@ -170,8 +170,8 @@ Future _process2FasExportFile( } String decrypt2FasVault(dynamic data, {required String password}) { - int ITERATION_COUNT = 10000; - int KEY_SIZE = 256; + int iterationCount = 10000; + int keySize = 256; final String encryptedServices = data["servicesEncrypted"]; var split = encryptedServices.split(":"); final encryptedData = base64.decode(split[0]); @@ -181,11 +181,11 @@ String decrypt2FasVault(dynamic data, {required String password}) { final pbkdf2 = PBKDF2KeyDerivator(HMac(SHA256Digest(), 64)); final params = Pbkdf2Parameters( salt, - ITERATION_COUNT, - KEY_SIZE ~/ 8, + iterationCount, + keySize ~/ 8, ); pbkdf2.init(params); - Uint8List key = Uint8List(KEY_SIZE ~/ 8); + Uint8List key = Uint8List(keySize ~/ 8); pbkdf2.deriveKey(Uint8List.fromList(utf8.encode(password)), 0, key, 0); final decrypted = decrypt(key, iv, encryptedData); final utf8Decode = utf8.decode(decrypted); diff --git a/auth/lib/ui/settings/data/import_page.dart b/auth/lib/ui/settings/data/import_page.dart index cd0f2114ac..c5ad2206e3 100644 --- a/auth/lib/ui/settings/data/import_page.dart +++ b/auth/lib/ui/settings/data/import_page.dart @@ -21,7 +21,9 @@ enum ImportType { } class ImportCodePage extends StatelessWidget { - final List importOptions = [ + const ImportCodePage({super.key}); + + static const List importOptions = [ ImportType.plainText, ImportType.encrypted, ImportType.twoFas, @@ -32,8 +34,6 @@ class ImportCodePage extends StatelessWidget { ImportType.lastpass, ]; - ImportCodePage({super.key}); - String getTitle(BuildContext context, ImportType type) { switch (type) { case ImportType.plainText: diff --git a/auth/lib/ui/settings/debug_section_widget.dart b/auth/lib/ui/settings/debug_section_widget.dart index 259937ff7e..03406f7911 100644 --- a/auth/lib/ui/settings/debug_section_widget.dart +++ b/auth/lib/ui/settings/debug_section_widget.dart @@ -3,9 +3,9 @@ import 'package:ente_auth/l10n/l10n.dart'; import 'package:ente_auth/ui/settings/common_settings.dart'; import 'package:ente_auth/ui/settings/settings_section_title.dart'; import 'package:ente_auth/ui/settings/settings_text_item.dart'; +import 'package:ente_crypto_dart/ente_crypto_dart.dart'; import 'package:expandable/expandable.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_sodium/flutter_sodium.dart'; class DebugSectionWidget extends StatelessWidget { const DebugSectionWidget({super.key}); @@ -51,7 +51,7 @@ class DebugSectionWidget extends StatelessWidget { "Key", style: TextStyle(fontWeight: FontWeight.bold), ), - Text(Sodium.bin2base64(Configuration.instance.getKey()!)), + Text(CryptoUtil.bin2base64(Configuration.instance.getKey()!)), const Padding(padding: EdgeInsets.all(12)), const Text( "Encrypted Key", diff --git a/auth/lib/ui/settings/developer_settings_page.dart b/auth/lib/ui/settings/developer_settings_page.dart index 96139e9827..5bd72be406 100644 --- a/auth/lib/ui/settings/developer_settings_page.dart +++ b/auth/lib/ui/settings/developer_settings_page.dart @@ -11,7 +11,7 @@ class DeveloperSettingsPage extends StatefulWidget { const DeveloperSettingsPage({super.key}); @override - _DeveloperSettingsPageState createState() => _DeveloperSettingsPageState(); + State createState() => _DeveloperSettingsPageState(); } class _DeveloperSettingsPageState extends State { @@ -27,7 +27,7 @@ class _DeveloperSettingsPageState extends State { @override Widget build(BuildContext context) { _logger.info( - "Current endpoint is: " + Configuration.instance.getHttpEndpoint(), + "Current endpoint is: ${Configuration.instance.getHttpEndpoint()}", ); return Scaffold( appBar: AppBar( @@ -49,7 +49,7 @@ class _DeveloperSettingsPageState extends State { GradientButton( onTap: () async { String url = _urlController.text; - _logger.info("Entered endpoint: " + url); + _logger.info("Entered endpoint: $url"); try { final uri = Uri.parse(url); if ((uri.scheme == "http" || uri.scheme == "https")) { @@ -79,7 +79,7 @@ class _DeveloperSettingsPageState extends State { Future _ping(String endpoint) async { try { - final response = await Dio().get(endpoint + '/ping'); + final response = await Dio().get('$endpoint/ping'); if (response.data['message'] != 'pong') { throw Exception('Invalid response'); } diff --git a/auth/lib/ui/settings/developer_settings_widget.dart b/auth/lib/ui/settings/developer_settings_widget.dart index 0fb32301ca..32f05f929d 100644 --- a/auth/lib/ui/settings/developer_settings_widget.dart +++ b/auth/lib/ui/settings/developer_settings_widget.dart @@ -15,7 +15,7 @@ class DeveloperSettingsWidget extends StatelessWidget { padding: const EdgeInsets.only(bottom: 20), child: Text( context.l10n.customEndpoint( - endpointURI.host + ":" + endpointURI.port.toString(), + "${endpointURI.host}:${endpointURI.port}", ), style: Theme.of(context).textTheme.bodySmall, ), diff --git a/auth/lib/ui/settings/faq.dart b/auth/lib/ui/settings/faq.dart index d9aa88f1b0..5f441912eb 100644 --- a/auth/lib/ui/settings/faq.dart +++ b/auth/lib/ui/settings/faq.dart @@ -8,8 +8,8 @@ import 'package:flutter/material.dart'; class FAQQuestionsWidget extends StatelessWidget { const FAQQuestionsWidget({ - Key? key, - }) : super(key: key); + super.key, + }); @override Widget build(BuildContext context) { @@ -64,9 +64,9 @@ class FAQQuestionsWidget extends StatelessWidget { class FaqWidget extends StatelessWidget { const FaqWidget({ - Key? key, + super.key, required this.faq, - }) : super(key: key); + }); final FaqItem? faq; diff --git a/auth/lib/ui/settings/general_section_widget.dart b/auth/lib/ui/settings/general_section_widget.dart index 4518be8de3..2b74cbf80d 100644 --- a/auth/lib/ui/settings/general_section_widget.dart +++ b/auth/lib/ui/settings/general_section_widget.dart @@ -17,7 +17,7 @@ import 'package:ente_auth/utils/toast_util.dart'; import 'package:flutter/material.dart'; class AdvancedSectionWidget extends StatefulWidget { - const AdvancedSectionWidget({Key? key}) : super(key: key); + const AdvancedSectionWidget({super.key}); @override State createState() => _AdvancedSectionWidgetState(); diff --git a/auth/lib/ui/settings/language_picker.dart b/auth/lib/ui/settings/language_picker.dart index 997e530685..1d0aa32e80 100644 --- a/auth/lib/ui/settings/language_picker.dart +++ b/auth/lib/ui/settings/language_picker.dart @@ -18,53 +18,58 @@ class LanguageSelectorPage extends StatelessWidget { this.supportedLocales, this.onLocaleChanged, this.currentLocale, { - Key? key, - }) : super(key: key); + super.key, + }); @override Widget build(BuildContext context) { final l10n = context.l10n; return Scaffold( - body: CustomScrollView( - primary: false, - slivers: [ - TitleBarWidget( - flexibleSpaceTitle: TitleBarTitleWidget( - title: l10n.selectLanguage, - ), - ), - SliverList( - delegate: SliverChildBuilderDelegate( - (context, index) { - return Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 20, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - ClipRRect( - borderRadius: - const BorderRadius.all(Radius.circular(8)), - child: ItemsWidget( - supportedLocales, - onLocaleChanged, - currentLocale, - ), + body: Center( + child: Container( + constraints: const BoxConstraints.tightFor(width: 450), + child: CustomScrollView( + primary: false, + slivers: [ + TitleBarWidget( + flexibleSpaceTitle: TitleBarTitleWidget( + title: l10n.selectLanguage, + ), + ), + SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) { + return Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 20, ), - // MenuSectionDescriptionWidget( - // content: context.l10n.maxDeviceLimitSpikeHandling(50), - // ) - ], - ), - ); - }, - childCount: 1, - ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ClipRRect( + borderRadius: + const BorderRadius.all(Radius.circular(8)), + child: ItemsWidget( + supportedLocales, + onLocaleChanged, + currentLocale, + ), + ), + // MenuSectionDescriptionWidget( + // content: context.l10n.maxDeviceLimitSpikeHandling(50), + // ) + ], + ), + ); + }, + childCount: 1, + ), + ), + const SliverPadding(padding: EdgeInsets.symmetric(vertical: 12)), + ], ), - const SliverPadding(padding: EdgeInsets.symmetric(vertical: 12)), - ], + ), ), ); } @@ -79,8 +84,8 @@ class ItemsWidget extends StatefulWidget { this.supportedLocales, this.onLocaleChanged, this.currentLocale, { - Key? key, - }) : super(key: key); + super.key, + }); @override State createState() => _ItemsWidgetState(); diff --git a/auth/lib/ui/settings/made_with_love_widget.dart b/auth/lib/ui/settings/made_with_love_widget.dart index ff1715efd7..a8e12c106b 100644 --- a/auth/lib/ui/settings/made_with_love_widget.dart +++ b/auth/lib/ui/settings/made_with_love_widget.dart @@ -4,8 +4,8 @@ import 'package:url_launcher/url_launcher.dart'; class MadeWithLoveWidget extends StatelessWidget { const MadeWithLoveWidget({ - Key? key, - }) : super(key: key); + super.key, + }); @override Widget build(BuildContext context) { diff --git a/auth/lib/ui/settings/security_section_widget.dart b/auth/lib/ui/settings/security_section_widget.dart index f4cc3324f3..4e50d38fbd 100644 --- a/auth/lib/ui/settings/security_section_widget.dart +++ b/auth/lib/ui/settings/security_section_widget.dart @@ -16,15 +16,16 @@ import 'package:ente_auth/ui/components/expandable_menu_item_widget.dart'; import 'package:ente_auth/ui/components/menu_item_widget.dart'; import 'package:ente_auth/ui/components/toggle_switch_widget.dart'; import 'package:ente_auth/ui/settings/common_settings.dart'; -import 'package:ente_auth/utils/crypto_util.dart'; import 'package:ente_auth/utils/dialog_util.dart'; import 'package:ente_auth/utils/navigation_util.dart'; +import 'package:ente_auth/utils/platform_util.dart'; import 'package:ente_auth/utils/toast_util.dart'; +import 'package:ente_crypto_dart/ente_crypto_dart.dart'; import 'package:flutter/material.dart'; import 'package:logging/logging.dart'; class SecuritySectionWidget extends StatefulWidget { - const SecuritySectionWidget({Key? key}) : super(key: key); + const SecuritySectionWidget({super.key}); @override State createState() => _SecuritySectionWidgetState(); @@ -94,6 +95,7 @@ class _SecuritySectionWidgetState extends State { ); final isEmailMFAEnabled = UserService.instance.hasEmailMFAEnabled(); + await PlatformUtil.refocusWindows(); if (hasAuthenticated) { await updateEmailMFA(!isEmailMFAEnabled); if (mounted) { @@ -117,6 +119,7 @@ class _SecuritySectionWidgetState extends State { context, context.l10n.authToViewYourActiveSessions, ); + await PlatformUtil.refocusWindows(); if (hasAuthenticated) { // ignore: unawaited_futures Navigator.of(context).push( @@ -149,6 +152,7 @@ class _SecuritySectionWidgetState extends State { context.l10n.lockScreenEnablePreSteps, ); if (hasAuthenticated) { + FocusScope.of(context).requestFocus(); setState(() {}); } }, diff --git a/auth/lib/ui/settings/settings_section_title.dart b/auth/lib/ui/settings/settings_section_title.dart index 08dbc69ec4..8b7e549cfa 100644 --- a/auth/lib/ui/settings/settings_section_title.dart +++ b/auth/lib/ui/settings/settings_section_title.dart @@ -8,9 +8,9 @@ class SettingsSectionTitle extends StatelessWidget { const SettingsSectionTitle( this.title, { - Key? key, + super.key, this.color, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/auth/lib/ui/settings/settings_text_item.dart b/auth/lib/ui/settings/settings_text_item.dart index 6273c7cad1..afaf56432c 100644 --- a/auth/lib/ui/settings/settings_text_item.dart +++ b/auth/lib/ui/settings/settings_text_item.dart @@ -8,10 +8,10 @@ class SettingsTextItem extends StatelessWidget { final String text; final IconData icon; const SettingsTextItem({ - Key? key, + super.key, required this.text, required this.icon, - }) : super(key: key); + }); @override Widget build(BuildContext context) { diff --git a/auth/lib/ui/settings/social_section_widget.dart b/auth/lib/ui/settings/social_section_widget.dart index 62b200453b..cda18366c5 100644 --- a/auth/lib/ui/settings/social_section_widget.dart +++ b/auth/lib/ui/settings/social_section_widget.dart @@ -7,11 +7,12 @@ import 'package:ente_auth/ui/components/captioned_text_widget.dart'; import 'package:ente_auth/ui/components/expandable_menu_item_widget.dart'; import 'package:ente_auth/ui/components/menu_item_widget.dart'; import 'package:ente_auth/ui/settings/common_settings.dart'; +import 'package:ente_auth/utils/platform_util.dart'; import 'package:flutter/material.dart'; import 'package:url_launcher/url_launcher_string.dart'; class SocialSectionWidget extends StatelessWidget { - const SocialSectionWidget({Key? key}) : super(key: key); + const SocialSectionWidget({super.key}); @override Widget build(BuildContext context) { @@ -31,8 +32,10 @@ class SocialSectionWidget extends StatelessWidget { final List options = [ sectionOptionSpacing, - SocialsMenuItemWidget(l10n.rateUsOnStore(ratePlace), rateUrl), - sectionOptionSpacing, + if (PlatformUtil.isMobile()) ...[ + SocialsMenuItemWidget(l10n.rateUsOnStore(ratePlace), rateUrl), + sectionOptionSpacing, + ], SocialsMenuItemWidget( l10n.blog, "https://ente.io/blog", @@ -65,11 +68,11 @@ class SocialsMenuItemWidget extends StatelessWidget { final bool launchInExternalApp; const SocialsMenuItemWidget( - this.text, - this.url, { - Key? key, - this.launchInExternalApp = true, - }) : super(key: key); + this.text, + this.url, { + super.key, + this.launchInExternalApp = true, + }); @override Widget build(BuildContext context) { diff --git a/auth/lib/ui/settings/support_dev_widget.dart b/auth/lib/ui/settings/support_dev_widget.dart index 63fa92c278..849b954153 100644 --- a/auth/lib/ui/settings/support_dev_widget.dart +++ b/auth/lib/ui/settings/support_dev_widget.dart @@ -10,8 +10,8 @@ import 'package:url_launcher/url_launcher.dart'; class SupportDevWidget extends StatelessWidget { const SupportDevWidget({ - Key? key, - }) : super(key: key); + super.key, + }); @override Widget build(BuildContext context) { diff --git a/auth/lib/ui/settings/support_section_widget.dart b/auth/lib/ui/settings/support_section_widget.dart index 418c1d4302..1343d23476 100644 --- a/auth/lib/ui/settings/support_section_widget.dart +++ b/auth/lib/ui/settings/support_section_widget.dart @@ -12,7 +12,7 @@ import 'package:logging/logging.dart'; import 'package:url_launcher/url_launcher_string.dart'; class SupportSectionWidget extends StatefulWidget { - const SupportSectionWidget({Key? key}) : super(key: key); + const SupportSectionWidget({super.key}); @override State createState() => _SupportSectionWidgetState(); @@ -64,7 +64,7 @@ class _SupportSectionWidgetState extends State { onTap: () async { // ignore: unawaited_futures launchUrlString( - githubDiscussionsUrl, + githubIssuesUrl, mode: LaunchMode.externalApplication, ); }, diff --git a/auth/lib/ui/settings/theme_switch_widget.dart b/auth/lib/ui/settings/theme_switch_widget.dart index 69e4513f16..c0495b65b6 100644 --- a/auth/lib/ui/settings/theme_switch_widget.dart +++ b/auth/lib/ui/settings/theme_switch_widget.dart @@ -11,7 +11,7 @@ import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; class ThemeSwitchWidget extends StatefulWidget { - const ThemeSwitchWidget({Key? key}) : super(key: key); + const ThemeSwitchWidget({super.key}); @override State createState() => _ThemeSwitchWidgetState(); diff --git a/auth/lib/ui/settings/title_bar_widget.dart b/auth/lib/ui/settings/title_bar_widget.dart index e11a5c2875..8d9aff85c3 100644 --- a/auth/lib/ui/settings/title_bar_widget.dart +++ b/auth/lib/ui/settings/title_bar_widget.dart @@ -2,7 +2,12 @@ import 'package:ente_auth/l10n/l10n.dart'; import 'package:flutter/material.dart'; class SettingsTitleBarWidget extends StatelessWidget { - const SettingsTitleBarWidget({Key? key}) : super(key: key); + const SettingsTitleBarWidget({ + super.key, + required this.scaffoldKey, + }); + + final GlobalKey scaffoldKey; @override Widget build(BuildContext context) { @@ -17,7 +22,7 @@ class SettingsTitleBarWidget extends StatelessWidget { IconButton( visualDensity: const VisualDensity(horizontal: -2, vertical: -2), onPressed: () { - Navigator.pop(context); + scaffoldKey.currentState?.closeDrawer(); }, icon: const Icon(Icons.keyboard_double_arrow_left_outlined), ), diff --git a/auth/lib/ui/settings_page.dart b/auth/lib/ui/settings_page.dart index cfe5ba874f..48fd6467ca 100644 --- a/auth/lib/ui/settings_page.dart +++ b/auth/lib/ui/settings_page.dart @@ -26,18 +26,24 @@ import 'package:ente_auth/ui/settings/theme_switch_widget.dart'; import 'package:ente_auth/ui/settings/title_bar_widget.dart'; import 'package:ente_auth/utils/dialog_util.dart'; import 'package:ente_auth/utils/navigation_util.dart'; +import 'package:ente_auth/utils/platform_util.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; class SettingsPage extends StatelessWidget { final ValueNotifier emailNotifier; + final GlobalKey scaffoldKey; - SettingsPage({Key? key, required this.emailNotifier}) : super(key: key); + SettingsPage({ + super.key, + required this.emailNotifier, + required this.scaffoldKey, + }); @override Widget build(BuildContext context) { - final _hasLoggedIn = Configuration.instance.hasConfiguredAccount(); - if (_hasLoggedIn) { + final hasLoggedIn = Configuration.instance.hasConfiguredAccount(); + if (hasLoggedIn) { UserService.instance.getUserDetailsV2().ignore(); } final enteColorScheme = getEnteColorScheme(context); @@ -50,11 +56,11 @@ class SettingsPage extends StatelessWidget { } Widget _getBody(BuildContext context, EnteColorScheme colorScheme) { - final _hasLoggedIn = Configuration.instance.hasConfiguredAccount(); + final hasLoggedIn = Configuration.instance.hasConfiguredAccount(); final enteTextTheme = getEnteTextTheme(context); const sectionSpacing = SizedBox(height: 8); final List contents = []; - if (_hasLoggedIn) { + if (hasLoggedIn) { contents.add( Container( padding: const EdgeInsets.symmetric(horizontal: 8), @@ -111,6 +117,7 @@ class SettingsPage extends StatelessWidget { context, context.l10n.authToInitiateSignIn, ); + await PlatformUtil.refocusWindows(); if (!hasAuthenticated) { return; } @@ -163,7 +170,9 @@ class SettingsPage extends StatelessWidget { child: Column( mainAxisSize: MainAxisSize.min, children: [ - const SettingsTitleBarWidget(), + SettingsTitleBarWidget( + scaffoldKey: scaffoldKey, + ), Padding( padding: const EdgeInsets.fromLTRB(16, 16, 16, 24), child: Column( diff --git a/auth/lib/ui/tools/app_lock.dart b/auth/lib/ui/tools/app_lock.dart index 2e7e0f54b1..df55f81164 100644 --- a/auth/lib/ui/tools/app_lock.dart +++ b/auth/lib/ui/tools/app_lock.dart @@ -37,7 +37,7 @@ class AppLock extends StatefulWidget { final Locale locale; const AppLock({ - Key? key, + super.key, required this.builder, required this.lockScreen, required this.savedThemeMode, @@ -46,7 +46,7 @@ class AppLock extends StatefulWidget { this.backgroundLockLatency = const Duration(seconds: 0), this.darkTheme, this.lightTheme, - }) : super(key: key); + }); static _AppLockState? of(BuildContext context) => context.findAncestorStateOfType<_AppLockState>(); @@ -135,9 +135,9 @@ class _AppLockState extends State with WidgetsBindingObserver { } Widget get _lockScreen { - return WillPopScope( + return PopScope( child: this.widget.lockScreen, - onWillPop: () => Future.value(false), + canPop: false, ); } diff --git a/auth/lib/ui/tools/debug/log_file_viewer.dart b/auth/lib/ui/tools/debug/log_file_viewer.dart index cc9321aa43..bcee33bc21 100644 --- a/auth/lib/ui/tools/debug/log_file_viewer.dart +++ b/auth/lib/ui/tools/debug/log_file_viewer.dart @@ -1,12 +1,12 @@ import 'dart:io'; -import 'dart:ui'; import 'package:ente_auth/ui/common/loading_widget.dart'; +import 'package:ente_auth/utils/platform_util.dart'; import 'package:flutter/material.dart'; class LogFileViewer extends StatefulWidget { final File file; - const LogFileViewer(this.file, {Key? key}) : super(key: key); + const LogFileViewer(this.file, {super.key}); @override State createState() => _LogFileViewerState(); @@ -42,13 +42,17 @@ class _LogFileViewerState extends State { return Container( padding: const EdgeInsets.only(left: 12, top: 8, right: 12), child: SingleChildScrollView( - child: Text( - _logs!, - style: const TextStyle( - fontFeatures: [ - FontFeature.tabularFigures(), - ], - height: 1.2, + child: SelectableRegion( + focusNode: FocusNode(), + selectionControls: PlatformUtil.selectionControls, + child: Text( + _logs!, + style: const TextStyle( + fontFeatures: [ + FontFeature.tabularFigures(), + ], + height: 1.2, + ), ), ), ), diff --git a/auth/lib/ui/tools/lock_screen.dart b/auth/lib/ui/tools/lock_screen.dart index c2e725f1eb..b6e2126e1d 100644 --- a/auth/lib/ui/tools/lock_screen.dart +++ b/auth/lib/ui/tools/lock_screen.dart @@ -8,7 +8,7 @@ import 'package:flutter/material.dart'; import 'package:logging/logging.dart'; class LockScreen extends StatefulWidget { - const LockScreen({Key? key}) : super(key: key); + const LockScreen({super.key}); @override State createState() => _LockScreenState(); diff --git a/auth/lib/ui/two_factor_authentication_page.dart b/auth/lib/ui/two_factor_authentication_page.dart index 43b8f967e8..58a4622864 100644 --- a/auth/lib/ui/two_factor_authentication_page.dart +++ b/auth/lib/ui/two_factor_authentication_page.dart @@ -4,13 +4,13 @@ import 'package:ente_auth/services/user_service.dart'; import 'package:ente_auth/ui/lifecycle_event_handler.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:pinput/pin_put/pin_put.dart'; + +import 'package:pinput/pinput.dart'; class TwoFactorAuthenticationPage extends StatefulWidget { final String sessionID; - const TwoFactorAuthenticationPage(this.sessionID, {Key? key}) - : super(key: key); + const TwoFactorAuthenticationPage(this.sessionID, {super.key}); @override State createState() => @@ -86,29 +86,31 @@ class _TwoFactorAuthenticationPageState const Padding(padding: EdgeInsets.all(32)), Padding( padding: const EdgeInsets.fromLTRB(40, 0, 40, 0), - child: PinPut( - fieldsCount: 6, - onSubmit: (String code) { + child: Pinput( + onSubmitted: (String code) { _verifyTwoFactorCode(code); }, + length: 6, + defaultPinTheme: const PinTheme(), + submittedPinTheme: PinTheme( + decoration: pinPutDecoration.copyWith( + borderRadius: BorderRadius.circular(20.0), + ), + ), + focusedPinTheme: PinTheme( + decoration: pinPutDecoration, + ), + followingPinTheme: PinTheme( + decoration: pinPutDecoration.copyWith( + borderRadius: BorderRadius.circular(5.0), + ), + ), onChanged: (String pin) { setState(() { _code = pin; }); }, controller: _pinController, - submittedFieldDecoration: pinPutDecoration.copyWith( - borderRadius: BorderRadius.circular(20.0), - ), - selectedFieldDecoration: pinPutDecoration, - followingFieldDecoration: pinPutDecoration.copyWith( - borderRadius: BorderRadius.circular(5.0), - ), - inputDecoration: const InputDecoration( - focusedBorder: InputBorder.none, - border: InputBorder.none, - counterText: '', - ), autofocus: true, ), ), diff --git a/auth/lib/ui/two_factor_recovery_page.dart b/auth/lib/ui/two_factor_recovery_page.dart index 4eae7ff2dc..5743f0246f 100644 --- a/auth/lib/ui/two_factor_recovery_page.dart +++ b/auth/lib/ui/two_factor_recovery_page.dart @@ -1,5 +1,3 @@ -import 'dart:ui'; - import 'package:ente_auth/l10n/l10n.dart'; import 'package:ente_auth/models/account/two_factor.dart'; import 'package:ente_auth/services/user_service.dart'; @@ -17,8 +15,8 @@ class TwoFactorRecoveryPage extends StatefulWidget { this.sessionID, this.encryptedSecret, this.secretDecryptionNonce, { - Key? key, - }) : super(key: key); + super.key, + }); @override State createState() => _TwoFactorRecoveryPageState(); diff --git a/auth/lib/ui/utils/icon_utils.dart b/auth/lib/ui/utils/icon_utils.dart index 298a143330..7713039527 100644 --- a/auth/lib/ui/utils/icon_utils.dart +++ b/auth/lib/ui/utils/icon_utils.dart @@ -87,7 +87,7 @@ class IconUtils { Color? _getAdaptiveColor(String? hexColor, BuildContext context) { if (hexColor == null) return null; final theme = Theme.of(context).brightness; - final color = Color(int.parse("0xFF" + hexColor)); + final color = Color(int.parse("0xFF$hexColor")); // Color is close to neutral-grey and it's too light or dark for theme if (_isCloseToNeutralGrey(color) && ((theme == Brightness.light && _getColorLuminance(color) > 0.70) || diff --git a/auth/lib/utils/auth_util.dart b/auth/lib/utils/auth_util.dart index 0a63201912..c2d2f5afa0 100644 --- a/auth/lib/utils/auth_util.dart +++ b/auth/lib/utils/auth_util.dart @@ -1,38 +1,45 @@ +import 'dart:io'; + import 'package:ente_auth/l10n/l10n.dart'; import 'package:flutter/cupertino.dart'; +import 'package:flutter_local_authentication/flutter_local_authentication.dart'; import 'package:local_auth/local_auth.dart'; import 'package:local_auth_android/local_auth_android.dart'; -import 'package:local_auth_ios/types/auth_messages_ios.dart'; +import 'package:local_auth_darwin/types/auth_messages_ios.dart'; import 'package:logging/logging.dart'; Future requestAuthentication(BuildContext context, String reason) async { Logger("AuthUtil").info("Requesting authentication"); - await LocalAuthentication().stopAuthentication(); - final l10n = context.l10n; - return await LocalAuthentication().authenticate( - localizedReason: reason, - authMessages: [ - AndroidAuthMessages( - biometricHint: l10n.androidBiometricHint, - biometricNotRecognized: l10n.androidBiometricNotRecognized, - biometricRequiredTitle: l10n.androidBiometricRequiredTitle, - biometricSuccess: l10n.androidBiometricSuccess, - cancelButton: l10n.androidCancelButton, - deviceCredentialsRequiredTitle: - l10n.androidDeviceCredentialsRequiredTitle, - deviceCredentialsSetupDescription: - l10n.androidDeviceCredentialsSetupDescription, - goToSettingsButton: l10n.goToSettings, - goToSettingsDescription: l10n.androidGoToSettingsDescription, - signInTitle: l10n.androidSignInTitle, - ), - IOSAuthMessages( - goToSettingsButton: l10n.goToSettings, - goToSettingsDescription: l10n.goToSettings, - lockOut: l10n.iOSLockOut, - // cancelButton default value is "Ok" - cancelButton: l10n.iOSOkButton, - ), - ], - ); + if (Platform.isMacOS || Platform.isLinux) { + return await FlutterLocalAuthentication().authenticate(); + } else { + await LocalAuthentication().stopAuthentication(); + final l10n = context.l10n; + return await LocalAuthentication().authenticate( + localizedReason: reason, + authMessages: [ + AndroidAuthMessages( + biometricHint: l10n.androidBiometricHint, + biometricNotRecognized: l10n.androidBiometricNotRecognized, + biometricRequiredTitle: l10n.androidBiometricRequiredTitle, + biometricSuccess: l10n.androidBiometricSuccess, + cancelButton: l10n.androidCancelButton, + deviceCredentialsRequiredTitle: + l10n.androidDeviceCredentialsRequiredTitle, + deviceCredentialsSetupDescription: + l10n.androidDeviceCredentialsSetupDescription, + goToSettingsButton: l10n.goToSettings, + goToSettingsDescription: l10n.androidGoToSettingsDescription, + signInTitle: l10n.androidSignInTitle, + ), + IOSAuthMessages( + goToSettingsButton: l10n.goToSettings, + goToSettingsDescription: l10n.goToSettings, + lockOut: l10n.iOSLockOut, + // cancelButton default value is "Ok" + cancelButton: l10n.iOSOkButton, + ), + ], + ); + } } diff --git a/auth/lib/utils/crypto_util.dart b/auth/lib/utils/crypto_util.dart deleted file mode 100644 index 494ef130f9..0000000000 --- a/auth/lib/utils/crypto_util.dart +++ /dev/null @@ -1,509 +0,0 @@ -import 'dart:convert'; -import 'dart:io' as io; -import 'dart:typed_data'; - -import 'package:computer/computer.dart'; -import 'package:ente_auth/core/errors.dart'; -import 'package:ente_auth/models/derived_key_result.dart'; -import 'package:ente_auth/models/encryption_result.dart'; -import 'package:ente_auth/utils/device_info.dart'; -import 'package:flutter_sodium/flutter_sodium.dart'; -import 'package:logging/logging.dart'; - -const int encryptionChunkSize = 4 * 1024 * 1024; -final int decryptionChunkSize = - encryptionChunkSize + Sodium.cryptoSecretstreamXchacha20poly1305Abytes; -const int hashChunkSize = 4 * 1024 * 1024; -const int loginSubKeyLen = 32; -const int loginSubKeyId = 1; -const String loginSubKeyContext = "loginctx"; - -Uint8List cryptoSecretboxEasy(Map args) { - return Sodium.cryptoSecretboxEasy(args["source"], args["nonce"], args["key"]); -} - -Uint8List cryptoSecretboxOpenEasy(Map args) { - return Sodium.cryptoSecretboxOpenEasy( - args["cipher"], - args["nonce"], - args["key"], - ); -} - -Uint8List cryptoPwHash(Map args) { - return Sodium.cryptoPwhash( - Sodium.cryptoSecretboxKeybytes, - args["password"], - args["salt"], - args["opsLimit"], - args["memLimit"], - Sodium.cryptoPwhashAlgArgon2id13, - ); -} - -Uint8List cryptoKdfDeriveFromKey( - Map args, -) { - return Sodium.cryptoKdfDeriveFromKey( - args["subkeyLen"], - args["subkeyId"], - args["context"], - args["key"], - ); -} - -// Returns the hash for a given file, chunking it in batches of hashChunkSize -Future cryptoGenericHash(Map args) async { - final sourceFile = io.File(args["sourceFilePath"]); - final sourceFileLength = await sourceFile.length(); - final inputFile = sourceFile.openSync(mode: io.FileMode.read); - final state = - Sodium.cryptoGenerichashInit(null, Sodium.cryptoGenerichashBytesMax); - var bytesRead = 0; - bool isDone = false; - while (!isDone) { - var chunkSize = hashChunkSize; - if (bytesRead + chunkSize >= sourceFileLength) { - chunkSize = sourceFileLength - bytesRead; - isDone = true; - } - final buffer = await inputFile.read(chunkSize); - bytesRead += chunkSize; - Sodium.cryptoGenerichashUpdate(state, buffer); - } - await inputFile.close(); - return Sodium.cryptoGenerichashFinal(state, Sodium.cryptoGenerichashBytesMax); -} - -EncryptionResult chachaEncryptData(Map args) { - final initPushResult = - Sodium.cryptoSecretstreamXchacha20poly1305InitPush(args["key"]); - final encryptedData = Sodium.cryptoSecretstreamXchacha20poly1305Push( - initPushResult.state, - args["source"], - null, - Sodium.cryptoSecretstreamXchacha20poly1305TagFinal, - ); - return EncryptionResult( - encryptedData: encryptedData, - header: initPushResult.header, - ); -} - -// Encrypts a given file, in chunks of encryptionChunkSize -Future chachaEncryptFile(Map args) async { - final encryptionStartTime = DateTime.now().millisecondsSinceEpoch; - final logger = Logger("ChaChaEncrypt"); - final sourceFile = io.File(args["sourceFilePath"]); - final destinationFile = io.File(args["destinationFilePath"]); - final sourceFileLength = await sourceFile.length(); - logger.info("Encrypting file of size " + sourceFileLength.toString()); - - final inputFile = sourceFile.openSync(mode: io.FileMode.read); - final key = args["key"] ?? Sodium.cryptoSecretstreamXchacha20poly1305Keygen(); - final initPushResult = - Sodium.cryptoSecretstreamXchacha20poly1305InitPush(key); - var bytesRead = 0; - var tag = Sodium.cryptoSecretstreamXchacha20poly1305TagMessage; - while (tag != Sodium.cryptoSecretstreamXchacha20poly1305TagFinal) { - var chunkSize = encryptionChunkSize; - if (bytesRead + chunkSize >= sourceFileLength) { - chunkSize = sourceFileLength - bytesRead; - tag = Sodium.cryptoSecretstreamXchacha20poly1305TagFinal; - } - final buffer = await inputFile.read(chunkSize); - bytesRead += chunkSize; - final encryptedData = Sodium.cryptoSecretstreamXchacha20poly1305Push( - initPushResult.state, - buffer, - null, - tag, - ); - await destinationFile.writeAsBytes(encryptedData, mode: io.FileMode.append); - } - await inputFile.close(); - - logger.info( - "Encryption time: " + - (DateTime.now().millisecondsSinceEpoch - encryptionStartTime) - .toString(), - ); - - return EncryptionResult(key: key, header: initPushResult.header); -} - -Future chachaDecryptFile(Map args) async { - final logger = Logger("ChaChaDecrypt"); - final decryptionStartTime = DateTime.now().millisecondsSinceEpoch; - final sourceFile = io.File(args["sourceFilePath"]); - final destinationFile = io.File(args["destinationFilePath"]); - final sourceFileLength = await sourceFile.length(); - logger.info("Decrypting file of size " + sourceFileLength.toString()); - - final inputFile = sourceFile.openSync(mode: io.FileMode.read); - final pullState = Sodium.cryptoSecretstreamXchacha20poly1305InitPull( - args["header"], - args["key"], - ); - - var bytesRead = 0; - var tag = Sodium.cryptoSecretstreamXchacha20poly1305TagMessage; - while (tag != Sodium.cryptoSecretstreamXchacha20poly1305TagFinal) { - var chunkSize = decryptionChunkSize; - if (bytesRead + chunkSize >= sourceFileLength) { - chunkSize = sourceFileLength - bytesRead; - } - final buffer = await inputFile.read(chunkSize); - bytesRead += chunkSize; - final pullResult = - Sodium.cryptoSecretstreamXchacha20poly1305Pull(pullState, buffer, null); - await destinationFile.writeAsBytes(pullResult.m, mode: io.FileMode.append); - tag = pullResult.tag; - } - inputFile.closeSync(); - - logger.info( - "ChaCha20 Decryption time: " + - (DateTime.now().millisecondsSinceEpoch - decryptionStartTime) - .toString(), - ); -} - -Uint8List chachaDecryptData(Map args) { - final pullState = Sodium.cryptoSecretstreamXchacha20poly1305InitPull( - args["header"], - args["key"], - ); - final pullResult = Sodium.cryptoSecretstreamXchacha20poly1305Pull( - pullState, - args["source"], - null, - ); - return pullResult.m; -} - -class CryptoUtil { - // Note: workers are turned on during app startup. - static final Computer _computer = Computer.shared(); - - static init() { - Sodium.init(); - } - - static Uint8List base642bin( - String b64, { - String? ignore, - int variant = Sodium.base64VariantOriginal, - }) { - return Sodium.base642bin(b64, ignore: ignore, variant: variant); - } - - static String bin2base64( - Uint8List bin, { - bool urlSafe = false, - }) { - return Sodium.bin2base64( - bin, - variant: - urlSafe ? Sodium.base64VariantUrlsafe : Sodium.base64VariantOriginal, - ); - } - - static String bin2hex(Uint8List bin) { - return Sodium.bin2hex(bin); - } - - static Uint8List hex2bin(String hex) { - return Sodium.hex2bin(hex); - } - - // Encrypts the given source, with the given key and a randomly generated - // nonce, using XSalsa20 (w Poly1305 MAC). - // This function runs on the same thread as the caller, so should be used only - // for small amounts of data where thread switching can result in a degraded - // user experience - static EncryptionResult encryptSync(Uint8List source, Uint8List key) { - final nonce = Sodium.randombytesBuf(Sodium.cryptoSecretboxNoncebytes); - - final args = {}; - args["source"] = source; - args["nonce"] = nonce; - args["key"] = key; - final encryptedData = cryptoSecretboxEasy(args); - return EncryptionResult( - key: key, - nonce: nonce, - encryptedData: encryptedData, - ); - } - - // Decrypts the given cipher, with the given key and nonce using XSalsa20 - // (w Poly1305 MAC). - static Future decrypt( - Uint8List cipher, - Uint8List key, - Uint8List nonce, - ) async { - final args = {}; - args["cipher"] = cipher; - args["nonce"] = nonce; - args["key"] = key; - return _computer.compute( - cryptoSecretboxOpenEasy, - param: args, - taskName: "decrypt", - ); - } - - // Decrypts the given cipher, with the given key and nonce using XSalsa20 - // (w Poly1305 MAC). - // This function runs on the same thread as the caller, so should be used only - // for small amounts of data where thread switching can result in a degraded - // user experience - static Uint8List decryptSync( - Uint8List cipher, - Uint8List key, - Uint8List nonce, - ) { - final args = {}; - args["cipher"] = cipher; - args["nonce"] = nonce; - args["key"] = key; - return cryptoSecretboxOpenEasy(args); - } - - // Encrypts the given source, with the given key and a randomly generated - // nonce, using XChaCha20 (w Poly1305 MAC). - // This function runs on the isolate pool held by `_computer`. - // TODO: Remove "ChaCha", an implementation detail from the function name - static Future encryptChaCha( - Uint8List source, - Uint8List key, - ) async { - final args = {}; - args["source"] = source; - args["key"] = key; - return _computer.compute( - chachaEncryptData, - param: args, - taskName: "encryptChaCha", - ); - } - - // Decrypts the given source, with the given key and header using XChaCha20 - // (w Poly1305 MAC). - // TODO: Remove "ChaCha", an implementation detail from the function name - static Future decryptChaCha( - Uint8List source, - Uint8List key, - Uint8List header, - ) async { - final args = {}; - args["source"] = source; - args["key"] = key; - args["header"] = header; - return _computer.compute( - chachaDecryptData, - param: args, - taskName: "decryptChaCha", - ); - } - - // Encrypts the file at sourceFilePath, with the key (if provided) and a - // randomly generated nonce using XChaCha20 (w Poly1305 MAC), and writes it - // to the destinationFilePath. - // If a key is not provided, one is generated and returned. - static Future encryptFile( - String sourceFilePath, - String destinationFilePath, { - Uint8List? key, - }) { - final args = {}; - args["sourceFilePath"] = sourceFilePath; - args["destinationFilePath"] = destinationFilePath; - args["key"] = key; - return _computer.compute( - chachaEncryptFile, - param: args, - taskName: "encryptFile", - ); - } - - // Decrypts the file at sourceFilePath, with the given key and header using - // XChaCha20 (w Poly1305 MAC), and writes it to the destinationFilePath. - static Future decryptFile( - String sourceFilePath, - String destinationFilePath, - Uint8List header, - Uint8List key, - ) { - final args = {}; - args["sourceFilePath"] = sourceFilePath; - args["destinationFilePath"] = destinationFilePath; - args["header"] = header; - args["key"] = key; - return _computer.compute( - chachaDecryptFile, - param: args, - taskName: "decryptFile", - ); - } - - // Generates and returns a 256-bit key. - static Uint8List generateKey() { - return Sodium.cryptoSecretboxKeygen(); - } - - // Generates and returns a random byte buffer of length - // crypto_pwhash_SALTBYTES (16) - static Uint8List getSaltToDeriveKey() { - return Sodium.randombytesBuf(Sodium.cryptoPwhashSaltbytes); - } - - // Generates and returns a secret key and the corresponding public key. - static Future generateKeyPair() async { - return Sodium.cryptoBoxKeypair(); - } - - // Decrypts the input using the given publicKey-secretKey pair - static Uint8List openSealSync( - Uint8List input, - Uint8List publicKey, - Uint8List secretKey, - ) { - return Sodium.cryptoBoxSealOpen(input, publicKey, secretKey); - } - - // Encrypts the input using the given publicKey - static Uint8List sealSync(Uint8List input, Uint8List publicKey) { - return Sodium.cryptoBoxSeal(input, publicKey); - } - - // Derives a key for a given password and salt using Argon2id, v1.3. - // The function first attempts to derive a key with both memLimit and opsLimit - // set to their Sensitive variants. - // If this fails, say on a device with insufficient RAM, we retry by halving - // the memLimit and doubling the opsLimit, while ensuring that we stay within - // the min and max limits for both parameters. - // At all points, we ensure that the product of these two variables (the area - // under the graph that determines the amount of work required) is a constant. - static Future deriveSensitiveKey( - Uint8List password, - Uint8List salt, - ) async { - final logger = Logger("pwhash"); - int memLimit = Sodium.cryptoPwhashMemlimitSensitive; - int opsLimit = Sodium.cryptoPwhashOpslimitSensitive; - if (await isLowSpecDevice()) { - logger.info("low spec device detected"); - // When sensitive memLimit (1 GB) is used, on low spec device the OS might - // kill the app with OOM. To avoid that, start with 256 MB and - // corresponding ops limit (16). - // This ensures that the product of these two variables - // (the area under the graph that determines the amount of work required) - // stays the same - // SODIUM_CRYPTO_PWHASH_MEMLIMIT_SENSITIVE: 1073741824 - // SODIUM_CRYPTO_PWHASH_MEMLIMIT_MODERATE: 268435456 - // SODIUM_CRYPTO_PWHASH_OPSLIMIT_SENSITIVE: 4 - memLimit = Sodium.cryptoPwhashMemlimitModerate; - final factor = Sodium.cryptoPwhashMemlimitSensitive ~/ - Sodium.cryptoPwhashMemlimitModerate; // = 4 - opsLimit = opsLimit * factor; // = 16 - } - Uint8List key; - while (memLimit >= Sodium.cryptoPwhashMemlimitMin && - opsLimit <= Sodium.cryptoPwhashOpslimitMax) { - try { - key = await deriveKey(password, salt, memLimit, opsLimit); - return DerivedKeyResult(key, memLimit, opsLimit); - } catch (e, s) { - logger.warning( - "failed to deriveKey mem: $memLimit, ops: $opsLimit", - e, - s, - ); - } - memLimit = (memLimit / 2).round(); - opsLimit = opsLimit * 2; - } - throw UnsupportedError("Cannot perform this operation on this device"); - } - - // Derives a key for the given password and salt, using Argon2id, v1.3 - // with memory and ops limit hardcoded to their Interactive variants - // NOTE: This is only used while setting passwords for shared links, as an - // extra layer of authentication (atop the access token and collection key). - // More details @ https://ente.io/blog/building-shareable-links/ - static Future deriveInteractiveKey( - Uint8List password, - Uint8List salt, - ) async { - final int memLimit = Sodium.cryptoPwhashMemlimitInteractive; - final int opsLimit = Sodium.cryptoPwhashOpslimitInteractive; - final key = await deriveKey(password, salt, memLimit, opsLimit); - return DerivedKeyResult(key, memLimit, opsLimit); - } - - // Derives a key for a given password, salt, memLimit and opsLimit using - // Argon2id, v1.3. - static Future deriveKey( - Uint8List password, - Uint8List salt, - int memLimit, - int opsLimit, - ) async { - try { - return await _computer.compute( - cryptoPwHash, - param: { - "password": password, - "salt": salt, - "memLimit": memLimit, - "opsLimit": opsLimit, - }, - taskName: "deriveKey", - ); - } catch (e, s) { - final String errMessage = 'failed to deriveKey memLimit: $memLimit and ' - 'opsLimit: $opsLimit'; - Logger("CryptoUtilDeriveKey").warning(errMessage, e, s); - throw KeyDerivationError(); - } - } - - // derives a Login key as subKey from the given key by applying KDF - // (Key Derivation Function) with the `loginSubKeyId` and - // `loginSubKeyLen` and `loginSubKeyContext` as context - static Future deriveLoginKey( - Uint8List key, - ) async { - try { - final Uint8List derivedKey = await _computer.compute( - cryptoKdfDeriveFromKey, - param: { - "key": key, - "subkeyId": loginSubKeyId, - "subkeyLen": loginSubKeyLen, - "context": utf8.encode(loginSubKeyContext), - }, - taskName: "deriveLoginKey", - ); - // return the first 16 bytes of the derived key - return derivedKey.sublist(0, 16); - } catch (e, s) { - Logger("deriveLoginKey").severe("loginKeyDerivation failed", e, s); - throw LoginKeyDerivationError(); - } - } - - // Computes and returns the hash of the source file - static Future getHash(io.File source) { - return _computer.compute( - cryptoGenericHash, - param: { - "sourceFilePath": source.path, - }, - taskName: "fileHash", - ); - } -} diff --git a/auth/lib/utils/data_util.dart b/auth/lib/utils/data_util.dart deleted file mode 100644 index 3dcae58fac..0000000000 --- a/auth/lib/utils/data_util.dart +++ /dev/null @@ -1,26 +0,0 @@ -import 'dart:math'; - -double convertBytesToGBs(final int bytes, {int precision = 2}) { - return double.parse( - (bytes / (1024 * 1024 * 1024)).toStringAsFixed(precision), - ); -} - -final storageUnits = ["bytes", "KB", "MB", "GB"]; - -String convertBytesToReadableFormat(int bytes) { - int storageUnitIndex = 0; - while (bytes >= 1024 && storageUnitIndex < storageUnits.length - 1) { - storageUnitIndex++; - bytes = (bytes / 1024).round(); - } - return bytes.toString() + " " + storageUnits[storageUnitIndex]; -} - -String formatBytes(int bytes, [int decimals = 2]) { - if (bytes == 0) return '0 bytes'; - const k = 1024; - final int dm = decimals < 0 ? 0 : decimals; - final int i = (log(bytes) / log(k)).floor(); - return ((bytes / pow(k, i)).toStringAsFixed(dm)) + ' ' + storageUnits[i]; -} diff --git a/auth/lib/utils/date_time_util.dart b/auth/lib/utils/date_time_util.dart index 5ce25de2db..cd80999231 100644 --- a/auth/lib/utils/date_time_util.dart +++ b/auth/lib/utils/date_time_util.dart @@ -48,25 +48,17 @@ const searchStartYear = 1970; //Jun 2022 String getMonthAndYear(DateTime dateTime) { - return _months[dateTime.month]! + " " + dateTime.year.toString(); + return "${_months[dateTime.month]!} ${dateTime.year}"; } //Thu, 30 Jun String getDayAndMonth(DateTime dateTime) { - return _days[dateTime.weekday]! + - ", " + - dateTime.day.toString() + - " " + - _months[dateTime.month]!; + return "${_days[dateTime.weekday]!}, ${dateTime.day} ${_months[dateTime.month]!}"; } //30 Jun, 2022 String getDateAndMonthAndYear(DateTime dateTime) { - return dateTime.day.toString() + - " " + - _months[dateTime.month]! + - ", " + - dateTime.year.toString(); + return "${dateTime.day} ${_months[dateTime.month]!}, ${dateTime.year}"; } String getDay(DateTime dateTime) { @@ -87,13 +79,11 @@ String getAbbreviationOfYear(DateTime dateTime) { //14:32 String getTime(DateTime dateTime) { - final hours = dateTime.hour > 9 - ? dateTime.hour.toString() - : "0" + dateTime.hour.toString(); - final minutes = dateTime.minute > 9 - ? dateTime.minute.toString() - : "0" + dateTime.minute.toString(); - return hours + ":" + minutes; + final hours = + dateTime.hour > 9 ? dateTime.hour.toString() : "0${dateTime.hour}"; + final minutes = + dateTime.minute > 9 ? dateTime.minute.toString() : "0${dateTime.minute}"; + return "$hours:$minutes"; } //11:22 AM @@ -103,41 +93,23 @@ String getTimeIn12hrFormat(DateTime dateTime) { //Thu, Jun 30, 2022 - 14:32 String getFormattedTime(DateTime dateTime) { - return getDay(dateTime) + - ", " + - getMonth(dateTime) + - " " + - dateTime.day.toString() + - ", " + - dateTime.year.toString() + - " - " + - getTime(dateTime); + return "${getDay(dateTime)}, ${getMonth(dateTime)} ${dateTime.day}, ${dateTime.year} - ${getTime(dateTime)}"; } //30 Jun'22 String getFormattedDate(DateTime dateTime) { - return dateTime.day.toString() + - " " + - getMonth(dateTime) + - "'" + - getAbbreviationOfYear(dateTime); + return "${dateTime.day} ${getMonth(dateTime)}'${getAbbreviationOfYear(dateTime)}"; } String getFullDate(DateTime dateTime) { - return getDay(dateTime) + - ", " + - getMonth(dateTime) + - " " + - dateTime.day.toString() + - " " + - dateTime.year.toString(); + return "${getDay(dateTime)}, ${getMonth(dateTime)} ${dateTime.day} ${dateTime.year}"; } String daysLeft(int futureTime) { final int daysLeft = ((futureTime - DateTime.now().microsecondsSinceEpoch) / Duration.microsecondsPerDay) .ceil(); - return '$daysLeft day' + (daysLeft <= 1 ? "" : "s"); + return '$daysLeft day${daysLeft <= 1 ? "" : "s"}'; } String formatDuration(Duration position) { @@ -168,7 +140,7 @@ String formatDuration(Duration position) { : '0$seconds'; final formattedTime = - '${hoursString == '00' ? '' : hoursString + ':'}$minutesString:$secondsString'; + '${hoursString == '00' ? '' : '$hoursString:'}$minutesString:$secondsString'; return formattedTime; } @@ -223,7 +195,7 @@ String getDayTitle(int timestamp) { } } if (date.year != DateTime.now().year) { - title += " " + date.year.toString(); + title += " ${date.year}"; } return title; } @@ -233,14 +205,11 @@ String secondsToHHMMSS(int value) { h = value ~/ 3600; m = ((value - h * 3600)) ~/ 60; s = value - (h * 3600) - (m * 60); - final String hourLeft = - h.toString().length < 2 ? "0" + h.toString() : h.toString(); + final String hourLeft = h.toString().length < 2 ? "0$h" : h.toString(); - final String minuteLeft = - m.toString().length < 2 ? "0" + m.toString() : m.toString(); + final String minuteLeft = m.toString().length < 2 ? "0$m" : m.toString(); - final String secondsLeft = - s.toString().length < 2 ? "0" + s.toString() : s.toString(); + final String secondsLeft = s.toString().length < 2 ? "0$s" : s.toString(); final String result = "$hourLeft:$minuteLeft:$secondsLeft"; diff --git a/auth/lib/utils/device_info.dart b/auth/lib/utils/device_info.dart deleted file mode 100644 index 5832ec6de7..0000000000 --- a/auth/lib/utils/device_info.dart +++ /dev/null @@ -1,44 +0,0 @@ -import 'dart:io'; - -import 'package:device_info_plus/device_info_plus.dart'; -import 'package:flutter/foundation.dart'; -import 'package:logging/logging.dart'; - -late DeviceInfoPlugin deviceInfoPlugin = DeviceInfoPlugin(); - -// https://gist.github.com/adamawolf/3048717 -late Set iOSLowEndMachineCodes = { - "iPhone5,1", //iPhone 5 (GSM) - "iPhone5,2", //iPhone 5 (GSM+CDMA) - "iPhone5,3", //iPhone 5C (GSM) - "iPhone5,4", //iPhone 5C (Global) - "iPhone6,1", //iPhone 5S (GSM) - "iPhone6,2", //iPhone 5S (Global) - "iPhone7,1", //iPhone 6 Plus - "iPhone7,2", //iPhone 6 - "iPhone8,1", // iPhone 6s - "iPhone8,2", // iPhone 6s Plus - "iPhone8,4", // iPhone SE (GSM) - "iPhone9,1", // iPhone 7 - "iPhone9,2", // iPhone 7 Plus - "iPhone9,3", // iPhone 7 - "iPhone9,4", // iPhone 7 Plus - "iPhone10,1", // iPhone 8 - "iPhone10,2", // iPhone 8 Plus - "iPhone10,3", // iPhone X Global - "iPhone10,4", // iPhone 8 - "iPhone10,5", // iPhone 8 -}; - -Future isLowSpecDevice() async { - try { - if (Platform.isIOS) { - final IosDeviceInfo iosInfo = await deviceInfoPlugin.iosInfo; - debugPrint("ios utc name ${iosInfo.utsname.machine}"); - return iOSLowEndMachineCodes.contains(iosInfo.utsname.machine); - } - } catch (e) { - Logger("device_info").severe("deviceSpec check failed", e); - } - return false; -} diff --git a/auth/lib/utils/dialog_util.dart b/auth/lib/utils/dialog_util.dart index 9cf7273fda..d24608b783 100644 --- a/auth/lib/utils/dialog_util.dart +++ b/auth/lib/utils/dialog_util.dart @@ -47,11 +47,11 @@ Future showErrorDialogForException({ String apiErrorPrefix = "It looks like something went wrong.", }) async { String errorMessage = context.l10n.tempErrorContactSupportIfPersists; - if (exception is DioError && + if (exception is DioException && exception.response != null && exception.response!.data["code"] != null) { errorMessage = - "$apiErrorPrefix\n\nReason: " + exception.response!.data["code"]; + "$apiErrorPrefix\n\nReason: ${exception.response!.data["code"]}"; } return showDialogWidget( context: context, diff --git a/auth/lib/utils/directory_utils.dart b/auth/lib/utils/directory_utils.dart new file mode 100644 index 0000000000..b9da6e6130 --- /dev/null +++ b/auth/lib/utils/directory_utils.dart @@ -0,0 +1,12 @@ +import 'package:path/path.dart' as p; +import 'package:path_provider/path_provider.dart'; + +class DirectoryUtils { + static Future getDatabasePath(String databaseName) async => p.joinAll( + [ + (await getApplicationDocumentsDirectory()).path, + "ente", + ".$databaseName", + ], + ); +} diff --git a/auth/lib/utils/email_util.dart b/auth/lib/utils/email_util.dart index d276962b1e..582449edbb 100644 --- a/auth/lib/utils/email_util.dart +++ b/auth/lib/utils/email_util.dart @@ -4,22 +4,21 @@ import 'package:archive/archive_io.dart'; import 'package:email_validator/email_validator.dart'; import 'package:ente_auth/core/configuration.dart'; import 'package:ente_auth/core/logging/super_logging.dart'; -import 'package:ente_auth/ente_theme_data.dart'; import 'package:ente_auth/l10n/l10n.dart'; import 'package:ente_auth/ui/components/buttons/button_widget.dart'; import 'package:ente_auth/ui/components/dialog_widget.dart'; import 'package:ente_auth/ui/components/models/button_type.dart'; import 'package:ente_auth/ui/tools/debug/log_file_viewer.dart'; -// import 'package:ente_auth/ui/tools/debug/log_file_viewer.dart'; import 'package:ente_auth/utils/dialog_util.dart'; +import 'package:ente_auth/utils/platform_util.dart'; +import 'package:ente_auth/utils/share_utils.dart'; import 'package:ente_auth/utils/toast_util.dart'; -import 'package:file_saver/file_saver.dart'; +import "package:file_saver/file_saver.dart"; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_email_sender/flutter_email_sender.dart'; -import 'package:intl/intl.dart'; +import "package:intl/intl.dart"; import 'package:logging/logging.dart'; -// import 'package:open_mail_app/open_mail_app.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:path_provider/path_provider.dart'; import 'package:share_plus/share_plus.dart'; @@ -27,7 +26,10 @@ import 'package:url_launcher/url_launcher.dart'; final Logger _logger = Logger('email_util'); -bool isValidEmail(String email) { +bool isValidEmail(String? email) { + if (email == null) { + return false; + } return EmailValidator.validate(email); } @@ -40,92 +42,73 @@ Future sendLogs( String? body, }) async { final l10n = context.l10n; - final List actions = [ - TextButton( - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Icon( - Icons.feed_outlined, - color: Theme.of(context).iconTheme.color?.withOpacity(0.85), - ), - const Padding(padding: EdgeInsets.all(4)), - Text( - l10n.viewLogsAction, - style: TextStyle( - color: Theme.of(context) - .colorScheme - .defaultTextColor - .withOpacity(0.85), + await showDialogWidget( + context: context, + title: title, + icon: Icons.bug_report_outlined, + body: l10n.sendLogsDescription, + buttons: [ + ButtonWidget( + isInAlert: true, + buttonType: ButtonType.neutral, + labelText: l10n.reportABug, + buttonAction: ButtonAction.first, + shouldSurfaceExecutionStates: false, + onTap: () async { + await _sendLogs(context, toEmail, subject, body); + if (postShare != null) { + postShare(); + } + }, + ), + //isInAlert is false here as we don't want to the dialog to dismiss + //on pressing this button + ButtonWidget( + buttonType: ButtonType.secondary, + labelText: l10n.viewLogsAction, + buttonAction: ButtonAction.second, + onTap: () async { + await showDialog( + context: context, + builder: (BuildContext context) { + return LogFileViewer(SuperLogging.logFile!); + }, + barrierColor: Colors.black87, + barrierDismissible: false, + ); + }, + ), + ButtonWidget( + isInAlert: true, + buttonType: ButtonType.secondary, + labelText: l10n.exportLogs, + buttonAction: ButtonAction.third, + onTap: () async { + Future.delayed( + const Duration(milliseconds: 200), + () => shareDialog( + context, + title, + saveAction: () async { + final zipFilePath = await getZippedLogsFile(context); + await exportLogs(context, zipFilePath); + }, + sendAction: () async { + final zipFilePath = await getZippedLogsFile(context); + await exportLogs(context, zipFilePath, true); + }, ), - ), - ], + ); + }, ), - onPressed: () async { - // ignore: unawaited_futures - showDialog( - context: context, - builder: (BuildContext context) { - return LogFileViewer(SuperLogging.logFile!); - }, - barrierColor: Colors.black87, - barrierDismissible: false, - ); - }, - ), - TextButton( - child: Text( - title, - style: TextStyle( - color: Theme.of(context).colorScheme.alternativeColor, - ), - ), - onPressed: () async { - Navigator.of(context, rootNavigator: true).pop('dialog'); - await _sendLogs(context, toEmail, subject, body); - if (postShare != null) { - postShare(); - } - }, - ), - ]; - final List content = []; - content.addAll( - [ - Text( - l10n.sendLogsDescription, - style: const TextStyle( - height: 1.5, - fontSize: 16, - ), - ), - const Padding(padding: EdgeInsets.all(12)), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: actions, + ButtonWidget( + isInAlert: true, + buttonType: ButtonType.secondary, + labelText: l10n.cancel, + buttonAction: ButtonAction.cancel, ), ], ); - final confirmation = AlertDialog( - title: Text( - title, - style: const TextStyle( - fontSize: 18, - ), - ), - content: SingleChildScrollView( - child: ListBody( - children: content, - ), - ), - ); - // ignore: unawaited_futures - showDialog( - context: context, - builder: (_) { - return confirmation; - }, - ); } Future _sendLogs( @@ -146,6 +129,7 @@ Future _sendLogs( await FlutterEmailSender.send(email); } catch (e, s) { _logger.severe('email sender failed', e, s); + Navigator.of(context, rootNavigator: true).pop(); await shareLogs(context, toEmail, zipFilePath); } } @@ -155,10 +139,10 @@ Future getZippedLogsFile(BuildContext context) async { final dialog = createProgressDialog(context, l10n.preparingLogsTitle); await dialog.show(); final logsPath = (await getApplicationSupportDirectory()).path; - final logsDirectory = Directory(logsPath + "/logs"); + final logsDirectory = Directory("$logsPath/logs"); final tempPath = (await getTemporaryDirectory()).path; final zipFilePath = - tempPath + "/logs-${Configuration.instance.getUserID() ?? 0}.zip"; + "$tempPath/logs-${Configuration.instance.getUserID() ?? 0}.zip"; final encoder = ZipFileEncoder(); encoder.create(zipFilePath); await encoder.addDirectory(logsDirectory); @@ -172,14 +156,15 @@ Future shareLogs( String toEmail, String zipFilePath, ) async { + final l10n = context.l10n; final result = await showDialogWidget( context: context, - title: context.l10n.emailYourLogs, - body: context.l10n.pleaseSendTheLogsTo(toEmail), + title: l10n.emailYourLogs, + body: l10n.pleaseSendTheLogsTo(toEmail), buttons: [ ButtonWidget( buttonType: ButtonType.neutral, - labelText: context.l10n.copyEmailAddress, + labelText: l10n.copyEmailAddress, isInAlert: true, buttonAction: ButtonAction.first, onTap: () async { @@ -189,35 +174,55 @@ Future shareLogs( ), ButtonWidget( buttonType: ButtonType.neutral, - labelText: context.l10n.exportLogs, + labelText: l10n.exportLogs, isInAlert: true, buttonAction: ButtonAction.second, ), ButtonWidget( buttonType: ButtonType.secondary, - labelText: context.l10n.cancel, + labelText: l10n.cancel, isInAlert: true, buttonAction: ButtonAction.cancel, ), ], ); if (result?.action != null && result!.action == ButtonAction.second) { - await exportLogs(context, zipFilePath); + Future.delayed( + const Duration(milliseconds: 200), + () => shareDialog( + context, + context.l10n.exportLogs, + saveAction: () async { + final zipFilePath = await getZippedLogsFile(context); + await exportLogs(context, zipFilePath); + }, + sendAction: () async { + final zipFilePath = await getZippedLogsFile(context); + await exportLogs(context, zipFilePath, true); + }, + ), + ); } } -Future exportLogs(BuildContext context, String zipFilePath) async { +Future exportLogs( + BuildContext context, + String zipFilePath, [ + bool isSharing = false, +]) async { final Size size = MediaQuery.of(context).size; - if (Platform.isAndroid) { - DateTime now = DateTime.now().toUtc(); - String shortMonthName = DateFormat('MMM').format(now); // Short month name - String logFileName = + if (!isSharing) { + final DateTime now = DateTime.now().toUtc(); + final String shortMonthName = DateFormat('MMM').format(now); // Short month + final String logFileName = 'ente-logs-${now.year}-$shortMonthName-${now.day}-${now.hour}-${now.minute}'; - await FileSaver.instance.saveAs( - name: logFileName, - filePath: zipFilePath, - mimeType: MimeType.zip, - ext: 'zip', + + final bytes = await File(zipFilePath).readAsBytes(); + await PlatformUtil.shareFile( + logFileName, + 'zip', + bytes, + MimeType.zip, ); } else { await Share.shareXFiles( @@ -235,8 +240,8 @@ Future sendEmail( }) async { try { final String clientDebugInfo = await _clientInfo(); - final String _subject = subject ?? '[Support]'; - final String _body = (body ?? '') + clientDebugInfo; + final String subject0 = subject ?? '[Support]'; + final String body0 = (body ?? '') + clientDebugInfo; // final EmailContent email = EmailContent( // to: [ // to, @@ -250,7 +255,7 @@ Future sendEmail( final Uri params = Uri( scheme: 'mailto', path: to, - query: 'subject=$_subject&body=$_body', + query: 'subject=$subject0&body=$body0', ); if (await canLaunchUrl(params)) { await launchUrl(params); @@ -280,27 +285,15 @@ Future _clientInfo() async { void _showNoMailAppsDialog(BuildContext context, String toEmail) { final l10n = context.l10n; - showDialog( - context: context, - builder: (context) { - return AlertDialog( - title: Text(l10n.emailUsMessage(toEmail)), - actions: [ - TextButton( - child: Text(l10n.copyEmailAction), - onPressed: () async { - await Clipboard.setData(ClipboardData(text: toEmail)); - showShortToast(context, l10n.copied); - }, - ), - TextButton( - child: Text(l10n.ok), - onPressed: () { - Navigator.pop(context); - }, - ), - ], - ); + showChoiceDialog( + context, + icon: Icons.email_outlined, + title: l10n.emailUsMessage(toEmail), + firstButtonLabel: l10n.copyEmailAddress, + secondButtonLabel: l10n.ok, + firstButtonOnTap: () async { + await Clipboard.setData(ClipboardData(text: toEmail)); + showShortToast(context, l10n.copied); }, ); } diff --git a/auth/lib/utils/navigation_util.dart b/auth/lib/utils/navigation_util.dart index 38a3dbe53c..58aa0fb852 100644 --- a/auth/lib/utils/navigation_util.dart +++ b/auth/lib/utils/navigation_util.dart @@ -109,9 +109,9 @@ class SwipeableRouteBuilder extends PageRoute { class TransparentRoute extends PageRoute { TransparentRoute({ required this.builder, - RouteSettings? settings, + super.settings, }) : assert(builder != null), - super(settings: settings, fullscreenDialog: false); + super(fullscreenDialog: false); final WidgetBuilder? builder; diff --git a/auth/lib/utils/package_info_util.dart b/auth/lib/utils/package_info_util.dart new file mode 100644 index 0000000000..0bb5c24585 --- /dev/null +++ b/auth/lib/utils/package_info_util.dart @@ -0,0 +1,20 @@ +import 'package:ente_auth/utils/platform_util.dart'; +import 'package:package_info_plus/package_info_plus.dart'; + +class PackageInfoUtil { + Future getPackageInfo() async { + return await PackageInfo.fromPlatform(); + } + + String getVersion(PackageInfo info) { + return info.version; + } + + String getPackageName(PackageInfo info) { + if (PlatformUtil.isMobile()) { + return info.packageName; + } else { + return 'io.ente.auth'; + } + } +} diff --git a/auth/lib/utils/platform_util.dart b/auth/lib/utils/platform_util.dart new file mode 100644 index 0000000000..5e02702579 --- /dev/null +++ b/auth/lib/utils/platform_util.dart @@ -0,0 +1,93 @@ +import 'dart:io'; + +import 'package:desktop_webview_window/desktop_webview_window.dart'; +import 'package:ente_auth/ui/common/web_page.dart'; +import 'package:file_saver/file_saver.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:url_launcher/url_launcher_string.dart'; +import 'package:window_manager/window_manager.dart'; + +class PlatformUtil { + static bool isDesktop() { + return !kIsWeb && + (Platform.isWindows || Platform.isLinux || Platform.isMacOS); + } + + static bool isMobile() { + return !kIsWeb && (Platform.isAndroid || Platform.isIOS); + } + + static bool isWeb() { + return kIsWeb; + } + + static TextSelectionControls get selectionControls => Platform.isAndroid + ? materialTextSelectionControls + : Platform.isIOS + ? cupertinoTextSelectionControls + : desktopTextSelectionControls; + + static openWebView(BuildContext context, String title, String url) async { + if (PlatformUtil.isDesktop()) { + if (!await WebviewWindow.isWebviewAvailable()) { + await launchUrlString(url); + return; + } + + final webview = await WebviewWindow.create( + configuration: CreateConfiguration( + title: title, + ), + ); + webview.launch(url); + return; + } + await Navigator.of(context).push( + MaterialPageRoute( + builder: (BuildContext context) { + return WebPage( + title, + url, + ); + }, + ), + ); + } + + static Future shareFile( + String fileName, + String extension, + Uint8List bytes, + MimeType type, + ) async { + try { + if (Platform.isAndroid || Platform.isIOS) { + await FileSaver.instance.saveAs( + name: fileName, + ext: extension, + bytes: bytes, + mimeType: type, + ); + } else { + await FileSaver.instance.saveFile( + name: fileName, + ext: extension, + bytes: bytes, + mimeType: type, + ); + } + } catch (_) {} + } + + // Needed to fix issue with local_auth on Windows + // https://github.com/flutter/flutter/issues/122322 + static Future refocusWindows() async { + if (!Platform.isWindows) return; + await windowManager.blur(); + await windowManager.focus(); + await windowManager.setAlwaysOnTop(true); + await windowManager.setAlwaysOnTop(false); + } +} diff --git a/auth/lib/utils/share_utils.dart b/auth/lib/utils/share_utils.dart new file mode 100644 index 0000000000..5bb8e08480 --- /dev/null +++ b/auth/lib/utils/share_utils.dart @@ -0,0 +1,51 @@ +import 'dart:io'; + +import 'package:ente_auth/l10n/l10n.dart'; +import 'package:ente_auth/ui/components/buttons/button_widget.dart'; +import 'package:ente_auth/ui/components/dialog_widget.dart'; +import 'package:ente_auth/ui/components/models/button_type.dart'; +import 'package:flutter/material.dart'; + +Future shareDialog( + BuildContext context, + String title, { + required Function saveAction, + required Function sendAction, +}) async { + final l10n = context.l10n; + await showDialogWidget( + context: context, + title: title, + body: Platform.isLinux || Platform.isWindows + ? l10n.saveOnlyDescription + : l10n.saveOrSendDescription, + buttons: [ + ButtonWidget( + isInAlert: true, + buttonType: ButtonType.neutral, + labelText: l10n.save, + buttonAction: ButtonAction.first, + shouldSurfaceExecutionStates: false, + onTap: () async { + await saveAction(); + }, + ), + if (!Platform.isWindows && !Platform.isLinux) + ButtonWidget( + isInAlert: true, + buttonType: ButtonType.secondary, + labelText: l10n.send, + buttonAction: ButtonAction.second, + onTap: () async { + await sendAction(); + }, + ), + ButtonWidget( + isInAlert: true, + buttonType: ButtonType.secondary, + labelText: l10n.cancel, + buttonAction: ButtonAction.cancel, + ), + ], + ); +} diff --git a/auth/lib/utils/toast_util.dart b/auth/lib/utils/toast_util.dart index c275160ebd..942274bc85 100644 --- a/auth/lib/utils/toast_util.dart +++ b/auth/lib/utils/toast_util.dart @@ -1,5 +1,6 @@ import 'package:ente_auth/ente_theme_data.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:fluttertoast/fluttertoast.dart'; void showToast( @@ -8,16 +9,42 @@ void showToast( toastLength = Toast.LENGTH_LONG, iOSDismissOnTap = true, }) async { - await Fluttertoast.cancel(); - await Fluttertoast.showToast( - msg: message, - toastLength: toastLength, - gravity: ToastGravity.BOTTOM, - timeInSecForIosWeb: 1, - backgroundColor: Theme.of(context).colorScheme.toastBackgroundColor, - textColor: Theme.of(context).colorScheme.toastTextColor, - fontSize: 16.0, - ); + try { + await Fluttertoast.cancel(); + await Fluttertoast.showToast( + msg: message, + toastLength: toastLength, + gravity: ToastGravity.BOTTOM, + timeInSecForIosWeb: 1, + backgroundColor: Theme.of(context).colorScheme.toastBackgroundColor, + textColor: Theme.of(context).colorScheme.toastTextColor, + fontSize: 16.0, + ); + } on MissingPluginException catch (_) { + Widget toast = Container( + padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 12.0), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(25.0), + color: Theme.of(context).colorScheme.toastBackgroundColor, + ), + child: Text( + message, + style: TextStyle( + color: Theme.of(context).colorScheme.toastTextColor, + fontSize: 16.0, + ), + ), + ); + + final fToast = FToast(); + fToast.init(context); + + fToast.showToast( + child: toast, + gravity: ToastGravity.BOTTOM, + toastDuration: const Duration(seconds: 2), + ); + } } void showShortToast(context, String message) { diff --git a/auth/lib/utils/window_protocol_handler.dart b/auth/lib/utils/window_protocol_handler.dart new file mode 100644 index 0000000000..a1436ef01f --- /dev/null +++ b/auth/lib/utils/window_protocol_handler.dart @@ -0,0 +1,54 @@ +import 'dart:io'; + +import 'package:ffi/ffi.dart'; +import 'package:win32/win32.dart'; + +const _hive = HKEY_CURRENT_USER; + +class WindowsProtocolHandler { + void register(String scheme, {String? executable, List? arguments}) { + final prefix = _regPrefix(scheme); + final capitalized = scheme[0].toUpperCase() + scheme.substring(1); + final cmd = executable ?? Platform.resolvedExecutable; + + _regCreateStringKey(_hive, prefix, '', 'URL:$capitalized'); + _regCreateStringKey(_hive, prefix, 'URL Protocol', ''); + _regCreateStringKey(_hive, '$prefix\\shell\\open\\command', '', cmd); + } + + void unregister(String scheme) { + final txtKey = TEXT(_regPrefix(scheme)); + try { + RegDeleteTree(HKEY_CURRENT_USER, txtKey); + } finally { + free(txtKey); + } + } + + String _regPrefix(String scheme) => 'SOFTWARE\\Classes\\$scheme'; + + int _regCreateStringKey(int hKey, String key, String valueName, String data) { + final txtKey = TEXT(key); + final txtValue = TEXT(valueName); + final txtData = TEXT(data); + try { + return RegSetKeyValue( + hKey, + txtKey, + txtValue, + REG_SZ, + txtData, + txtData.length * 2 + 2, + ); + } finally { + free(txtKey); + free(txtValue); + free(txtData); + } + } + + // String _sanitize(String value) { + // value = value.replaceAll(r'%s', '%1').replaceAll(r'"', '\\"'); + // return '"$value"'; + // } +} diff --git a/auth/linux/CMakeLists.txt b/auth/linux/CMakeLists.txt index 68e736ee36..ca3bd0a60e 100644 --- a/auth/linux/CMakeLists.txt +++ b/auth/linux/CMakeLists.txt @@ -4,7 +4,7 @@ project(runner LANGUAGES CXX) # The name of the executable created for the application. Change this to change # the on-disk name of your application. -set(BINARY_NAME "authenticator") +set(BINARY_NAME "ente_auth") # The unique GTK application identifier for this application. See: # https://wiki.gnome.org/HowDoI/ChooseApplicationID set(APPLICATION_ID "io.ente.auth") diff --git a/auth/linux/flutter/generated_plugin_registrant.cc b/auth/linux/flutter/generated_plugin_registrant.cc index 53de79447e..bcc3b982d1 100644 --- a/auth/linux/flutter/generated_plugin_registrant.cc +++ b/auth/linux/flutter/generated_plugin_registrant.cc @@ -6,22 +6,58 @@ #include "generated_plugin_registrant.h" +#include #include +#include #include +#include +#include #include +#include +#include +#include +#include #include +#include void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) desktop_webview_window_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "DesktopWebviewWindowPlugin"); + desktop_webview_window_plugin_register_with_registrar(desktop_webview_window_registrar); g_autoptr(FlPluginRegistrar) file_saver_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "FileSaverPlugin"); file_saver_plugin_register_with_registrar(file_saver_registrar); + g_autoptr(FlPluginRegistrar) flutter_local_authentication_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterLocalAuthenticationPlugin"); + flutter_local_authentication_plugin_register_with_registrar(flutter_local_authentication_registrar); g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin"); flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar); + g_autoptr(FlPluginRegistrar) gtk_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "GtkPlugin"); + gtk_plugin_register_with_registrar(gtk_registrar); + g_autoptr(FlPluginRegistrar) screen_retriever_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverPlugin"); + screen_retriever_plugin_register_with_registrar(screen_retriever_registrar); g_autoptr(FlPluginRegistrar) sentry_flutter_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "SentryFlutterPlugin"); sentry_flutter_plugin_register_with_registrar(sentry_flutter_registrar); + g_autoptr(FlPluginRegistrar) smart_auth_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "SmartAuthPlugin"); + smart_auth_plugin_register_with_registrar(smart_auth_registrar); + g_autoptr(FlPluginRegistrar) sodium_libs_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "SodiumLibsPlugin"); + sodium_libs_plugin_register_with_registrar(sodium_libs_registrar); + g_autoptr(FlPluginRegistrar) sqlite3_flutter_libs_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "Sqlite3FlutterLibsPlugin"); + sqlite3_flutter_libs_plugin_register_with_registrar(sqlite3_flutter_libs_registrar); + g_autoptr(FlPluginRegistrar) tray_manager_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "TrayManagerPlugin"); + tray_manager_plugin_register_with_registrar(tray_manager_registrar); g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); + g_autoptr(FlPluginRegistrar) window_manager_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "WindowManagerPlugin"); + window_manager_plugin_register_with_registrar(window_manager_registrar); } diff --git a/auth/linux/flutter/generated_plugins.cmake b/auth/linux/flutter/generated_plugins.cmake index 4b981d07ab..c6aab9a95e 100644 --- a/auth/linux/flutter/generated_plugins.cmake +++ b/auth/linux/flutter/generated_plugins.cmake @@ -3,10 +3,19 @@ # list(APPEND FLUTTER_PLUGIN_LIST + desktop_webview_window file_saver + flutter_local_authentication flutter_secure_storage_linux + gtk + screen_retriever sentry_flutter + smart_auth + sodium_libs + sqlite3_flutter_libs + tray_manager url_launcher_linux + window_manager ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/auth/linux/my_application.cc b/auth/linux/my_application.cc index 95b3a369c4..8a553fec35 100644 --- a/auth/linux/my_application.cc +++ b/auth/linux/my_application.cc @@ -7,17 +7,27 @@ #include "flutter/generated_plugin_registrant.h" -struct _MyApplication { +struct _MyApplication +{ GtkApplication parent_instance; - char** dart_entrypoint_arguments; + char **dart_entrypoint_arguments; }; G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) // Implements GApplication::activate. -static void my_application_activate(GApplication* application) { - MyApplication* self = MY_APPLICATION(application); - GtkWindow* window = +static void my_application_activate(GApplication *application) +{ + MyApplication *self = MY_APPLICATION(application); + + GList *windows = gtk_application_get_windows(GTK_APPLICATION(application)); + if (windows) + { + gtk_window_present(GTK_WINDOW(windows->data)); + return; + } + + GtkWindow *window = GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); // Use a header bar when running in GNOME as this is the common style used @@ -29,31 +39,36 @@ static void my_application_activate(GApplication* application) { // if future cases occur). gboolean use_header_bar = TRUE; #ifdef GDK_WINDOWING_X11 - GdkScreen* screen = gtk_window_get_screen(window); - if (GDK_IS_X11_SCREEN(screen)) { - const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); - if (g_strcmp0(wm_name, "GNOME Shell") != 0) { + GdkScreen *screen = gtk_window_get_screen(window); + if (GDK_IS_X11_SCREEN(screen)) + { + const gchar *wm_name = gdk_x11_screen_get_window_manager_name(screen); + if (g_strcmp0(wm_name, "GNOME Shell") != 0) + { use_header_bar = FALSE; } } #endif - if (use_header_bar) { - GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); + if (use_header_bar) + { + GtkHeaderBar *header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); gtk_widget_show(GTK_WIDGET(header_bar)); - gtk_header_bar_set_title(header_bar, "authenticator"); + gtk_header_bar_set_title(header_bar, "Ente Auth"); gtk_header_bar_set_show_close_button(header_bar, TRUE); gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); - } else { - gtk_window_set_title(window, "authenticator"); + } + else + { + gtk_window_set_title(window, "Ente Auth"); } gtk_window_set_default_size(window, 1280, 720); - gtk_widget_show(GTK_WIDGET(window)); + gtk_widget_realize(GTK_WIDGET(window)); g_autoptr(FlDartProject) project = fl_dart_project_new(); fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); - FlView* view = fl_view_new(project); + FlView *view = fl_view_new(project); gtk_widget_show(GTK_WIDGET(view)); gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); @@ -63,42 +78,47 @@ static void my_application_activate(GApplication* application) { } // Implements GApplication::local_command_line. -static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) { - MyApplication* self = MY_APPLICATION(application); +static gboolean my_application_local_command_line(GApplication *application, gchar ***arguments, int *exit_status) +{ + MyApplication *self = MY_APPLICATION(application); // Strip out the first argument as it is the binary name. self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); g_autoptr(GError) error = nullptr; - if (!g_application_register(application, nullptr, &error)) { - g_warning("Failed to register: %s", error->message); - *exit_status = 1; - return TRUE; + if (!g_application_register(application, nullptr, &error)) + { + g_warning("Failed to register: %s", error->message); + *exit_status = 1; + return TRUE; } g_application_activate(application); *exit_status = 0; - return TRUE; + return FALSE; } // Implements GObject::dispose. -static void my_application_dispose(GObject* object) { - MyApplication* self = MY_APPLICATION(object); +static void my_application_dispose(GObject *object) +{ + MyApplication *self = MY_APPLICATION(object); g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); G_OBJECT_CLASS(my_application_parent_class)->dispose(object); } -static void my_application_class_init(MyApplicationClass* klass) { +static void my_application_class_init(MyApplicationClass *klass) +{ G_APPLICATION_CLASS(klass)->activate = my_application_activate; G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line; G_OBJECT_CLASS(klass)->dispose = my_application_dispose; } -static void my_application_init(MyApplication* self) {} +static void my_application_init(MyApplication *self) {} -MyApplication* my_application_new() { +MyApplication *my_application_new() +{ return MY_APPLICATION(g_object_new(my_application_get_type(), "application-id", APPLICATION_ID, - "flags", G_APPLICATION_NON_UNIQUE, + "flags", G_APPLICATION_HANDLES_COMMAND_LINE | G_APPLICATION_HANDLES_OPEN, nullptr)); } diff --git a/auth/linux/packaging/appimage/make_config.yaml b/auth/linux/packaging/appimage/make_config.yaml new file mode 100644 index 0000000000..4f4e1a4cd7 --- /dev/null +++ b/auth/linux/packaging/appimage/make_config.yaml @@ -0,0 +1,27 @@ +display_name: Auth +license: GPLv3 + +icon: assets/icons/auth-icon.png + +keywords: + - Authentication + - 2FA + +generic_name: Ente Auth + +categories: + - Utility + +startup_notify: false + +# You can specify the shared libraries that you want to bundle with your app +# +# flutter_distributor automatically detects the shared libraries that your app +# depends on, but you can also specify them manually here. +# +# The following example shows how to bundle the libcurl library with your app. +# +# include: +# - libcurl.so.4 +include: + - libffi.so.7 diff --git a/auth/linux/packaging/deb/make_config.yaml b/auth/linux/packaging/deb/make_config.yaml new file mode 100644 index 0000000000..93f54e4720 --- /dev/null +++ b/auth/linux/packaging/deb/make_config.yaml @@ -0,0 +1,34 @@ +display_name: Auth +package_name: auth +maintainer: + name: Ente.io Developers + email: human@ente.io +priority: optional +section: x11 +essential: false +license: GPLv3 +icon: assets/icons/auth-icon.png +installed_size: 36000 + +dependencies: + - libwebkit2gtk-4.0-37 + - libsqlite3-0 + - libsodium23 + - libsecret-1-0 + - libappindicator3-1 | libayatana-appindicator3-1 + - gir1.2-appindicator3-0.1 | gir1.2-ayatanaappindicator3-0.1 + - libayatana-ido3-0.4-0 + +keywords: + - Authentication + - 2FA + +generic_name: Ente Authentication + +categories: + - Utility + +startup_notify: false + +supported_mime_type: + - x-scheme-handler/ente diff --git a/auth/linux/packaging/rpm/make_config.yaml b/auth/linux/packaging/rpm/make_config.yaml new file mode 100644 index 0000000000..2b1a6a7fe4 --- /dev/null +++ b/auth/linux/packaging/rpm/make_config.yaml @@ -0,0 +1,31 @@ +icon: assets/icons/auth-icon.png +summary: 2FA app with free end-to-end encrypted backup and sync +group: Application/Utility +vendor: Ente.io +packager: Ente.io Developers +packagerEmail: human@ente.io +license: GPLv3 +url: https://github.com/ente-io/ente + +display_name: Auth + +dependencies: + - libsqlite3x + - webkit2gtk-4.0 + - libsodium + - libsecret + - libappindicator + +keywords: + - Authentication + - 2FA + +generic_name: Ente Authentication + +categories: + - Utility + +startup_notify: false + +supported_mime_type: + - x-scheme-handler/ente diff --git a/auth/macos/Flutter/GeneratedPluginRegistrant.swift b/auth/macos/Flutter/GeneratedPluginRegistrant.swift index 6b18cc3813..68c59c25a7 100644 --- a/auth/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/auth/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,30 +5,50 @@ import FlutterMacOS import Foundation -import connectivity_macos +import app_links +import connectivity_plus +import desktop_webview_window import device_info_plus import file_saver +import flutter_inappwebview_macos +import flutter_local_authentication import flutter_local_notifications import flutter_secure_storage_macos import package_info_plus import path_provider_foundation +import screen_retriever import sentry_flutter import share_plus import shared_preferences_foundation +import smart_auth +import sodium_libs import sqflite +import sqlite3_flutter_libs +import tray_manager import url_launcher_macos +import window_manager func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin")) ConnectivityPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlugin")) + DesktopWebviewWindowPlugin.register(with: registry.registrar(forPlugin: "DesktopWebviewWindowPlugin")) DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) FileSaverPlugin.register(with: registry.registrar(forPlugin: "FileSaverPlugin")) + InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin")) + FlutterLocalAuthenticationPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalAuthenticationPlugin")) FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin")) FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) - FLTPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FLTPackageInfoPlusPlugin")) + FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) + ScreenRetrieverPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverPlugin")) SentryFlutterPlugin.register(with: registry.registrar(forPlugin: "SentryFlutterPlugin")) SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) + SmartAuthPlugin.register(with: registry.registrar(forPlugin: "SmartAuthPlugin")) + SodiumLibsPlugin.register(with: registry.registrar(forPlugin: "SodiumLibsPlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) + Sqlite3FlutterLibsPlugin.register(with: registry.registrar(forPlugin: "Sqlite3FlutterLibsPlugin")) + TrayManagerPlugin.register(with: registry.registrar(forPlugin: "TrayManagerPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) + WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin")) } diff --git a/auth/macos/Podfile b/auth/macos/Podfile index 049abe2954..0c308d4f56 100644 --- a/auth/macos/Podfile +++ b/auth/macos/Podfile @@ -36,5 +36,8 @@ end post_install do |installer| installer.pods_project.targets.each do |target| flutter_additional_macos_build_settings(target) - end + target.build_configurations.each do |build_configuration| + build_configuration.build_settings['export_xcargs'] = '-allowProvisioningUpdates' + end +end end diff --git a/auth/macos/Podfile.lock b/auth/macos/Podfile.lock index 678bce8daa..a5b6eb77c9 100644 --- a/auth/macos/Podfile.lock +++ b/auth/macos/Podfile.lock @@ -1,102 +1,186 @@ PODS: - - connectivity_macos (0.0.1): + - app_links (1.0.0): + - FlutterMacOS + - connectivity_plus (0.0.1): + - FlutterMacOS + - ReachabilitySwift + - desktop_webview_window (0.0.1): - FlutterMacOS - - Reachability - device_info_plus (0.0.1): - FlutterMacOS + - file_saver (0.0.1): + - FlutterMacOS + - flutter_inappwebview_macos (0.0.1): + - FlutterMacOS + - OrderedSet (~> 5.0) + - flutter_local_authentication (1.2.0): + - FlutterMacOS - flutter_local_notifications (0.0.1): - FlutterMacOS - flutter_secure_storage_macos (6.1.1): - FlutterMacOS - FlutterMacOS (1.0.0) - - FMDB (2.7.5): - - FMDB/standard (= 2.7.5) - - FMDB/standard (2.7.5) - - package_info_plus_macos (0.0.1): + - OrderedSet (5.0.0) + - package_info_plus (0.0.1): - FlutterMacOS - path_provider_foundation (0.0.1): - Flutter - FlutterMacOS - - Reachability (3.2) - - Sentry/HybridSDK (7.31.5) + - ReachabilitySwift (5.0.0) + - screen_retriever (0.0.1): + - FlutterMacOS + - Sentry/HybridSDK (8.21.0): + - SentryPrivate (= 8.21.0) - sentry_flutter (0.0.1): - Flutter - FlutterMacOS - - Sentry/HybridSDK (= 7.31.5) - - share_plus_macos (0.0.1): + - Sentry/HybridSDK (= 8.21.0) + - SentryPrivate (8.21.0) + - share_plus (0.0.1): - FlutterMacOS - shared_preferences_foundation (0.0.1): - Flutter - FlutterMacOS - - sqflite (0.0.2): + - smart_auth (0.0.1): + - FlutterMacOS + - sodium_libs (2.2.1): + - FlutterMacOS + - sqflite (0.0.3): + - Flutter + - FlutterMacOS + - sqlite3 (3.45.1): + - sqlite3/common (= 3.45.1) + - sqlite3/common (3.45.1) + - sqlite3/fts5 (3.45.1): + - sqlite3/common + - sqlite3/perf-threadsafe (3.45.1): + - sqlite3/common + - sqlite3/rtree (3.45.1): + - sqlite3/common + - sqlite3_flutter_libs (0.0.1): + - FlutterMacOS + - sqlite3 (~> 3.45.1) + - sqlite3/fts5 + - sqlite3/perf-threadsafe + - sqlite3/rtree + - tray_manager (0.0.1): - FlutterMacOS - - FMDB (>= 2.7.5) - url_launcher_macos (0.0.1): - FlutterMacOS + - window_manager (0.2.0): + - FlutterMacOS DEPENDENCIES: - - connectivity_macos (from `Flutter/ephemeral/.symlinks/plugins/connectivity_macos/macos`) + - app_links (from `Flutter/ephemeral/.symlinks/plugins/app_links/macos`) + - connectivity_plus (from `Flutter/ephemeral/.symlinks/plugins/connectivity_plus/macos`) + - desktop_webview_window (from `Flutter/ephemeral/.symlinks/plugins/desktop_webview_window/macos`) - device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`) + - file_saver (from `Flutter/ephemeral/.symlinks/plugins/file_saver/macos`) + - flutter_inappwebview_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_inappwebview_macos/macos`) + - flutter_local_authentication (from `Flutter/ephemeral/.symlinks/plugins/flutter_local_authentication/macos`) - flutter_local_notifications (from `Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos`) - flutter_secure_storage_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos`) - FlutterMacOS (from `Flutter/ephemeral`) - - package_info_plus_macos (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus_macos/macos`) - - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/macos`) + - package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`) + - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`) + - screen_retriever (from `Flutter/ephemeral/.symlinks/plugins/screen_retriever/macos`) - sentry_flutter (from `Flutter/ephemeral/.symlinks/plugins/sentry_flutter/macos`) - - share_plus_macos (from `Flutter/ephemeral/.symlinks/plugins/share_plus_macos/macos`) - - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/macos`) - - sqflite (from `Flutter/ephemeral/.symlinks/plugins/sqflite/macos`) + - share_plus (from `Flutter/ephemeral/.symlinks/plugins/share_plus/macos`) + - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`) + - smart_auth (from `Flutter/ephemeral/.symlinks/plugins/smart_auth/macos`) + - sodium_libs (from `Flutter/ephemeral/.symlinks/plugins/sodium_libs/macos`) + - sqflite (from `Flutter/ephemeral/.symlinks/plugins/sqflite/darwin`) + - sqlite3_flutter_libs (from `Flutter/ephemeral/.symlinks/plugins/sqlite3_flutter_libs/macos`) + - tray_manager (from `Flutter/ephemeral/.symlinks/plugins/tray_manager/macos`) - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) + - window_manager (from `Flutter/ephemeral/.symlinks/plugins/window_manager/macos`) SPEC REPOS: trunk: - - FMDB - - Reachability + - OrderedSet + - ReachabilitySwift - Sentry + - SentryPrivate + - sqlite3 EXTERNAL SOURCES: - connectivity_macos: - :path: Flutter/ephemeral/.symlinks/plugins/connectivity_macos/macos + app_links: + :path: Flutter/ephemeral/.symlinks/plugins/app_links/macos + connectivity_plus: + :path: Flutter/ephemeral/.symlinks/plugins/connectivity_plus/macos + desktop_webview_window: + :path: Flutter/ephemeral/.symlinks/plugins/desktop_webview_window/macos device_info_plus: :path: Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos + file_saver: + :path: Flutter/ephemeral/.symlinks/plugins/file_saver/macos + flutter_inappwebview_macos: + :path: Flutter/ephemeral/.symlinks/plugins/flutter_inappwebview_macos/macos + flutter_local_authentication: + :path: Flutter/ephemeral/.symlinks/plugins/flutter_local_authentication/macos flutter_local_notifications: :path: Flutter/ephemeral/.symlinks/plugins/flutter_local_notifications/macos flutter_secure_storage_macos: :path: Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos FlutterMacOS: :path: Flutter/ephemeral - package_info_plus_macos: - :path: Flutter/ephemeral/.symlinks/plugins/package_info_plus_macos/macos + package_info_plus: + :path: Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos path_provider_foundation: - :path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/macos + :path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin + screen_retriever: + :path: Flutter/ephemeral/.symlinks/plugins/screen_retriever/macos sentry_flutter: :path: Flutter/ephemeral/.symlinks/plugins/sentry_flutter/macos - share_plus_macos: - :path: Flutter/ephemeral/.symlinks/plugins/share_plus_macos/macos + share_plus: + :path: Flutter/ephemeral/.symlinks/plugins/share_plus/macos shared_preferences_foundation: - :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/macos + :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin + smart_auth: + :path: Flutter/ephemeral/.symlinks/plugins/smart_auth/macos + sodium_libs: + :path: Flutter/ephemeral/.symlinks/plugins/sodium_libs/macos sqflite: - :path: Flutter/ephemeral/.symlinks/plugins/sqflite/macos + :path: Flutter/ephemeral/.symlinks/plugins/sqflite/darwin + sqlite3_flutter_libs: + :path: Flutter/ephemeral/.symlinks/plugins/sqlite3_flutter_libs/macos + tray_manager: + :path: Flutter/ephemeral/.symlinks/plugins/tray_manager/macos url_launcher_macos: :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos + window_manager: + :path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos SPEC CHECKSUMS: - connectivity_macos: 5dae6ee11d320fac7c05f0d08bd08fc32b5514d9 + app_links: 10e0a0ab602ffaf34d142cd4862f29d34b303b2a + connectivity_plus: 18d3c32514c886e046de60e9c13895109866c747 + desktop_webview_window: d4365e71bcd4e1aa0c14cf0377aa24db0c16a7e2 device_info_plus: 5401765fde0b8d062a2f8eb65510fb17e77cf07f + file_saver: 44e6fbf666677faf097302460e214e977fdd977b + flutter_inappwebview_macos: 9600c9df9fdb346aaa8933812009f8d94304203d + flutter_local_authentication: 85674893931e1c9cfa7c9e4f5973cb8c56b018b0 flutter_local_notifications: 3805ca215b2fb7f397d78b66db91f6a747af52e4 flutter_secure_storage_macos: d56e2d218c1130b262bef8b4a7d64f88d7f9c9ea FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 - FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a - package_info_plus_macos: f010621b07802a241d96d01876d6705f15e77c1c - path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943 - Reachability: 33e18b67625424e47b6cde6d202dce689ad7af96 - Sentry: 4c9babff9034785067c896fd580b1f7de44da020 - sentry_flutter: 1346a880b24c0240807b53b10cf50ddad40f504e - share_plus_macos: 853ee48e7dce06b633998ca0735d482dd671ade4 - shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126 - sqflite: a5789cceda41d54d23f31d6de539d65bb14100ea + OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c + package_info_plus: 02d7a575e80f194102bef286361c6c326e4c29ce + path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c + ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825 + screen_retriever: 59634572a57080243dd1bf715e55b6c54f241a38 + Sentry: ebc12276bd17613a114ab359074096b6b3725203 + sentry_flutter: dff1df05dc39c83d04f9330b36360fc374574c5e + SentryPrivate: d651efb234cf385ec9a1cdd3eff94b5e78a0e0fe + share_plus: 76dd39142738f7a68dd57b05093b5e8193f220f7 + shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695 + smart_auth: b38e3ab4bfe089eacb1e233aca1a2340f96c28e9 + sodium_libs: d39bd76697736cb11ce4a0be73b9b4bc64466d6f + sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec + sqlite3: 73b7fc691fdc43277614250e04d183740cb15078 + sqlite3_flutter_libs: 06a05802529659a272beac4ee1350bfec294f386 + tray_manager: 9064e219c56d75c476e46b9a21182087930baf90 url_launcher_macos: d2691c7dd33ed713bf3544850a623080ec693d95 + window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8 -PODFILE CHECKSUM: 353c8bcc5d5b0994e508d035b5431cfe18c1dea7 +PODFILE CHECKSUM: f401c31c8f7c5571f6f565c78915d54338812dab -COCOAPODS: 1.12.1 +COCOAPODS: 1.14.3 diff --git a/auth/macos/Runner.xcodeproj/project.pbxproj b/auth/macos/Runner.xcodeproj/project.pbxproj index cbd8b2c8a3..492f710ada 100644 --- a/auth/macos/Runner.xcodeproj/project.pbxproj +++ b/auth/macos/Runner.xcodeproj/project.pbxproj @@ -55,7 +55,7 @@ /* Begin PBXFileReference section */ 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; - 33CC10ED2044A3C60003C045 /* authenticator.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = authenticator.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10ED2044A3C60003C045 /* Ente Auth.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Ente Auth.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; @@ -94,7 +94,6 @@ 4F2F733D93DB4D2D82767271 /* Pods-Runner.release.xcconfig */, B347CC163E4E13C897729F91 /* Pods-Runner.profile.xcconfig */, ); - name = Pods; path = Pods; sourceTree = ""; }; @@ -123,7 +122,7 @@ 33CC10EE2044A3C60003C045 /* Products */ = { isa = PBXGroup; children = ( - 33CC10ED2044A3C60003C045 /* authenticator.app */, + 33CC10ED2044A3C60003C045 /* Ente Auth.app */, ); name = Products; sourceTree = ""; @@ -193,7 +192,7 @@ ); name = Runner; productName = Runner; - productReference = 33CC10ED2044A3C60003C045 /* authenticator.app */; + productReference = 33CC10ED2044A3C60003C045 /* Ente Auth.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ @@ -203,13 +202,12 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0920; - LastUpgradeCheck = 1430; + LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { 33CC10EC2044A3C60003C045 = { CreatedOnToolsVersion = 9.2; LastSwiftMigration = 1100; - ProvisioningStyle = Automatic; SystemCapabilities = { com.apple.Sandbox = { enabled = 1; @@ -420,13 +418,18 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_IDENTITY = "-"; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; INFOPLIST_FILE = Runner/Info.plist; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); + MACOSX_DEPLOYMENT_TARGET = 10.14; + PRODUCT_BUNDLE_IDENTIFIER = io.ente.auth.mac; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; }; @@ -546,13 +549,18 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_IDENTITY = "-"; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; INFOPLIST_FILE = Runner/Info.plist; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); + MACOSX_DEPLOYMENT_TARGET = 10.14; + PRODUCT_BUNDLE_IDENTIFIER = io.ente.auth.mac; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; @@ -566,13 +574,21 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; + CODE_SIGN_IDENTITY = "-"; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; + CODE_SIGN_INJECT_BASE_ENTITLEMENTS = NO; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; + ENABLE_HARDENED_RUNTIME = YES; INFOPLIST_FILE = Runner/Info.plist; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); + MACOSX_DEPLOYMENT_TARGET = 10.14; + OTHER_CODE_SIGN_FLAGS = "--timestamp"; + PRODUCT_BUNDLE_IDENTIFIER = io.ente.auth.mac; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; }; diff --git a/auth/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/auth/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 38ba92a69c..7b82ae2ae0 100644 --- a/auth/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/auth/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ @@ -31,7 +31,7 @@ @@ -54,7 +54,7 @@ @@ -71,7 +71,7 @@ diff --git a/auth/macos/Runner/AppDelegate.swift b/auth/macos/Runner/AppDelegate.swift index d53ef64377..218f93e023 100644 --- a/auth/macos/Runner/AppDelegate.swift +++ b/auth/macos/Runner/AppDelegate.swift @@ -4,6 +4,6 @@ import FlutterMacOS @NSApplicationMain class AppDelegate: FlutterAppDelegate { override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { - return true + return false } } diff --git a/auth/macos/Runner/Assets.xcassets/AppIcon.appiconset/1024-mac.png b/auth/macos/Runner/Assets.xcassets/AppIcon.appiconset/1024-mac.png new file mode 100644 index 0000000000..bbf24e4364 Binary files /dev/null and b/auth/macos/Runner/Assets.xcassets/AppIcon.appiconset/1024-mac.png differ diff --git a/auth/macos/Runner/Assets.xcassets/AppIcon.appiconset/128-mac.png b/auth/macos/Runner/Assets.xcassets/AppIcon.appiconset/128-mac.png new file mode 100644 index 0000000000..2a210095a5 Binary files /dev/null and b/auth/macos/Runner/Assets.xcassets/AppIcon.appiconset/128-mac.png differ diff --git a/auth/macos/Runner/Assets.xcassets/AppIcon.appiconset/16-mac.png b/auth/macos/Runner/Assets.xcassets/AppIcon.appiconset/16-mac.png new file mode 100644 index 0000000000..fb83d3abe9 Binary files /dev/null and b/auth/macos/Runner/Assets.xcassets/AppIcon.appiconset/16-mac.png differ diff --git a/auth/macos/Runner/Assets.xcassets/AppIcon.appiconset/256-mac.png b/auth/macos/Runner/Assets.xcassets/AppIcon.appiconset/256-mac.png new file mode 100644 index 0000000000..f64b470b01 Binary files /dev/null and b/auth/macos/Runner/Assets.xcassets/AppIcon.appiconset/256-mac.png differ diff --git a/auth/macos/Runner/Assets.xcassets/AppIcon.appiconset/32-mac.png b/auth/macos/Runner/Assets.xcassets/AppIcon.appiconset/32-mac.png new file mode 100644 index 0000000000..c72e503af5 Binary files /dev/null and b/auth/macos/Runner/Assets.xcassets/AppIcon.appiconset/32-mac.png differ diff --git a/auth/macos/Runner/Assets.xcassets/AppIcon.appiconset/512-mac.png b/auth/macos/Runner/Assets.xcassets/AppIcon.appiconset/512-mac.png new file mode 100644 index 0000000000..07f8c930f9 Binary files /dev/null and b/auth/macos/Runner/Assets.xcassets/AppIcon.appiconset/512-mac.png differ diff --git a/auth/macos/Runner/Assets.xcassets/AppIcon.appiconset/64-mac.png b/auth/macos/Runner/Assets.xcassets/AppIcon.appiconset/64-mac.png new file mode 100644 index 0000000000..d7c149e3d1 Binary files /dev/null and b/auth/macos/Runner/Assets.xcassets/AppIcon.appiconset/64-mac.png differ diff --git a/auth/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/auth/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json index a2ec33f19f..eba1335b9c 100644 --- a/auth/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/auth/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,68 +1 @@ -{ - "images" : [ - { - "size" : "16x16", - "idiom" : "mac", - "filename" : "app_icon_16.png", - "scale" : "1x" - }, - { - "size" : "16x16", - "idiom" : "mac", - "filename" : "app_icon_32.png", - "scale" : "2x" - }, - { - "size" : "32x32", - "idiom" : "mac", - "filename" : "app_icon_32.png", - "scale" : "1x" - }, - { - "size" : "32x32", - "idiom" : "mac", - "filename" : "app_icon_64.png", - "scale" : "2x" - }, - { - "size" : "128x128", - "idiom" : "mac", - "filename" : "app_icon_128.png", - "scale" : "1x" - }, - { - "size" : "128x128", - "idiom" : "mac", - "filename" : "app_icon_256.png", - "scale" : "2x" - }, - { - "size" : "256x256", - "idiom" : "mac", - "filename" : "app_icon_256.png", - "scale" : "1x" - }, - { - "size" : "256x256", - "idiom" : "mac", - "filename" : "app_icon_512.png", - "scale" : "2x" - }, - { - "size" : "512x512", - "idiom" : "mac", - "filename" : "app_icon_512.png", - "scale" : "1x" - }, - { - "size" : "512x512", - "idiom" : "mac", - "filename" : "app_icon_1024.png", - "scale" : "2x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} +{"images":[{"size":"1024x1024","filename":"1024-mac.png","expected-size":"1024","idiom":"ios-marketing","folder":"Assets.xcassets/AppIcon.appiconset/","scale":"1x"},{"size":"128x128","expected-size":"128","filename":"128-mac.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"256x256","expected-size":"256","filename":"256-mac.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"128x128","expected-size":"256","filename":"256-mac.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"},{"size":"256x256","expected-size":"512","filename":"512-mac.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"},{"size":"32x32","expected-size":"32","filename":"32-mac.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"512x512","expected-size":"512","filename":"512-mac.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"16x16","expected-size":"16","filename":"16-mac.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"16x16","expected-size":"32","filename":"32-mac.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"},{"size":"32x32","expected-size":"64","filename":"64-mac.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"},{"size":"512x512","expected-size":"1024","filename":"1024-mac.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"}]} \ No newline at end of file diff --git a/auth/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/auth/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png deleted file mode 100644 index 3c4935a7ca..0000000000 Binary files a/auth/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png and /dev/null differ diff --git a/auth/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/auth/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png deleted file mode 100644 index ed4cc16421..0000000000 Binary files a/auth/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png and /dev/null differ diff --git a/auth/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/auth/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png deleted file mode 100644 index 483be61389..0000000000 Binary files a/auth/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png and /dev/null differ diff --git a/auth/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/auth/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png deleted file mode 100644 index bcbf36df2f..0000000000 Binary files a/auth/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png and /dev/null differ diff --git a/auth/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/auth/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png deleted file mode 100644 index 9c0a652864..0000000000 Binary files a/auth/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png and /dev/null differ diff --git a/auth/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/auth/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png deleted file mode 100644 index e71a726136..0000000000 Binary files a/auth/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png and /dev/null differ diff --git a/auth/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/auth/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png deleted file mode 100644 index 8a31fe2dd3..0000000000 Binary files a/auth/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png and /dev/null differ diff --git a/auth/macos/Runner/Configs/AppInfo.xcconfig b/auth/macos/Runner/Configs/AppInfo.xcconfig index 7c1cda27ad..cb548680bb 100644 --- a/auth/macos/Runner/Configs/AppInfo.xcconfig +++ b/auth/macos/Runner/Configs/AppInfo.xcconfig @@ -5,10 +5,12 @@ // 'flutter create' template. // The application's name. By default this is also the title of the Flutter window. -PRODUCT_NAME = auth +PRODUCT_NAME = Ente Auth // The application's bundle identifier PRODUCT_BUNDLE_IDENTIFIER = io.ente.auth // The copyright displayed in application information PRODUCT_COPYRIGHT = Copyright © 2022 io.ente.auth. All rights reserved. + +DEVELOPMENT_TEAM = 6Z68YJY9Q2 \ No newline at end of file diff --git a/auth/macos/Runner/DebugProfile.entitlements b/auth/macos/Runner/DebugProfile.entitlements index c946719a1a..bc070d381f 100644 --- a/auth/macos/Runner/DebugProfile.entitlements +++ b/auth/macos/Runner/DebugProfile.entitlements @@ -10,5 +10,9 @@ com.apple.security.network.client + keychain-access-groups + + com.apple.security.files.downloads.read-write + diff --git a/auth/macos/Runner/Info.plist b/auth/macos/Runner/Info.plist index 4789daa6a4..418d695c9f 100644 --- a/auth/macos/Runner/Info.plist +++ b/auth/macos/Runner/Info.plist @@ -28,5 +28,20 @@ MainMenu NSPrincipalClass NSApplication + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLName + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleURLSchemes + + otpauth + + + + LSApplicationCategoryType + public.app-category.security diff --git a/auth/macos/Runner/MainFlutterWindow.swift b/auth/macos/Runner/MainFlutterWindow.swift index 2722837ec9..a823d046a6 100644 --- a/auth/macos/Runner/MainFlutterWindow.swift +++ b/auth/macos/Runner/MainFlutterWindow.swift @@ -1,5 +1,6 @@ import Cocoa import FlutterMacOS +import window_manager class MainFlutterWindow: NSWindow { override func awakeFromNib() { @@ -12,4 +13,9 @@ class MainFlutterWindow: NSWindow { super.awakeFromNib() } + + override public func order(_ place: NSWindow.OrderingMode, relativeTo otherWin: Int) { + super.order(place, relativeTo: otherWin) + hiddenWindowAtLaunch() + } } diff --git a/auth/macos/Runner/Release.entitlements b/auth/macos/Runner/Release.entitlements index 48271acc95..e04b5e06c7 100644 --- a/auth/macos/Runner/Release.entitlements +++ b/auth/macos/Runner/Release.entitlements @@ -4,7 +4,11 @@ com.apple.security.app-sandbox + com.apple.security.files.downloads.read-write + com.apple.security.network.client - + + keychain-access-groups + diff --git a/auth/macos/build/.last_build_id b/auth/macos/build/.last_build_id new file mode 100644 index 0000000000..637ab03820 --- /dev/null +++ b/auth/macos/build/.last_build_id @@ -0,0 +1 @@ +f51f5c3bcecb0339dc02189e9dd2c2c8 \ No newline at end of file diff --git a/auth/macos/packaging/dmg/make_config.yaml b/auth/macos/packaging/dmg/make_config.yaml new file mode 100644 index 0000000000..30628bef37 --- /dev/null +++ b/auth/macos/packaging/dmg/make_config.yaml @@ -0,0 +1,11 @@ +title: Auth +icon: ../../../assets/icons/auth-icon.png +contents: + - x: 448 + y: 344 + type: link + path: "/Applications" + - x: 192 + y: 344 + type: file + path: Ente Auth.app diff --git a/auth/pubspec.lock b/auth/pubspec.lock index 4514e503ca..35dd963563 100644 --- a/auth/pubspec.lock +++ b/auth/pubspec.lock @@ -5,34 +5,50 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: ae92f5d747aee634b87f89d9946000c2de774be1d6ac3e58268224348cd0101a + sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7" url: "https://pub.dev" source: hosted - version: "61.0.0" + version: "67.0.0" adaptive_theme: dependency: "direct main" description: name: adaptive_theme - sha256: "3568bb526d4823c7bb35f9ce3604af15e04cc0e9cc4f257da3604fe6b48d74ae" + sha256: f4ee609b464e5efc68131d9d15ba9aa1de4e3b5ede64be17781c6e19a52d637d url: "https://pub.dev" source: hosted - version: "3.2.1" + version: "3.6.0" analyzer: dependency: transitive description: name: analyzer - sha256: ea3d8652bda62982addfd92fdc2d0214e5f82e43325104990d4f4c4a2a313562 + sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d" url: "https://pub.dev" source: hosted - version: "5.13.0" + version: "6.4.1" + ansicolor: + dependency: transitive + description: + name: ansicolor + sha256: "8bf17a8ff6ea17499e40a2d2542c2f481cd7615760c6d34065cb22bfd22e6880" + url: "https://pub.dev" + source: hosted + version: "2.0.2" + app_links: + dependency: "direct main" + description: + name: app_links + sha256: "3ced568a5d9e309e99af71285666f1f3117bddd0bd5b3317979dccc1a40cada4" + url: "https://pub.dev" + source: hosted + version: "3.5.1" archive: dependency: "direct main" description: name: archive - sha256: "0c8368c9b3f0abbc193b9d6133649a614204b528982bebc7026372d61677ce3a" + sha256: "22600aa1e926be775fa5fe7e6894e7fb3df9efda8891c73f70fb3262399a432d" url: "https://pub.dev" source: hosted - version: "3.3.7" + version: "3.4.10" args: dependency: transitive description: @@ -69,18 +85,10 @@ packages: dependency: "direct main" description: name: bloc - sha256: "3820f15f502372d979121de1f6b97bfcf1630ebff8fe1d52fb2b0bfa49be5b49" + sha256: "106842ad6569f0b60297619e9e0b1885c2fb9bf84812935490e6c5275777804e" url: "https://pub.dev" source: hosted - version: "8.1.2" - bloc_test: - dependency: "direct dev" - description: - name: bloc_test - sha256: "43d5b2f3d09ba768d6b611151bdf20ca141ffb46e795eb9550a58c9c2f4eae3f" - url: "https://pub.dev" - source: hosted - version: "9.1.3" + version: "8.1.4" boolean_selector: dependency: transitive description: @@ -93,10 +101,10 @@ packages: dependency: transitive description: name: build - sha256: "3fbda25365741f8251b39f3917fb3c8e286a96fd068a5a242e11c2012d495777" + sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.4.1" build_config: dependency: transitive description: @@ -109,34 +117,34 @@ packages: dependency: transitive description: name: build_daemon - sha256: "757153e5d9cd88253cb13f28c2fb55a537dc31fefd98137549895b5beb7c6169" + sha256: "0343061a33da9c5810b2d6cee51945127d8f4c060b7fbdd9d54917f0a3feaaa1" url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "4.0.1" build_resolvers: dependency: transitive description: name: build_resolvers - sha256: "6c4dd11d05d056e76320b828a1db0fc01ccd376922526f8e9d6c796a5adbac20" + sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a" url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.4.2" build_runner: dependency: "direct dev" description: name: build_runner - sha256: b0a8a7b8a76c493e85f1b84bffa0588859a06197863dba8c9036b15581fd9727 + sha256: "3ac61a79bfb6f6cc11f693591063a7f19a7af628dc52f141743edac5c16e8c22" url: "https://pub.dev" source: hosted - version: "2.3.3" + version: "2.4.9" build_runner_core: dependency: transitive description: name: build_runner_core - sha256: "0671ad4162ed510b70d0eb4ad6354c249f8429cab4ae7a4cec86bbc2886eb76e" + sha256: "4ae8ffe5ac758da294ecf1802f2aff01558d8b1b00616aa7538ea9a8a5d50799" url: "https://pub.dev" source: hosted - version: "7.2.7+1" + version: "7.3.0" built_collection: dependency: transitive description: @@ -149,10 +157,10 @@ packages: dependency: transitive description: name: built_value - sha256: "598a2a682e2a7a90f08ba39c0aaa9374c5112340f0a2e275f61b59389543d166" + sha256: fedde275e0a6b798c3296963c5cd224e3e1b55d0e478d5b7e65e6b540f363a0e url: "https://pub.dev" source: hosted - version: "8.6.1" + version: "8.9.1" characters: dependency: transitive description: @@ -169,6 +177,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.3" + cli_util: + dependency: transitive + description: + name: cli_util + sha256: c05b7406fdabc7a49a3929d4af76bcaccbbffcbcdcf185b082e1ae07da323d19 + url: "https://pub.dev" + source: hosted + version: "0.4.1" clipboard: dependency: "direct main" description: @@ -189,27 +205,18 @@ packages: dependency: transitive description: name: code_builder - sha256: "4ad01d6e56db961d29661561effde45e519939fdaeb46c351275b182eac70189" + sha256: f692079e25e7869c14132d39f223f8eec9830eb76131925143b2129c4bb01b37 url: "https://pub.dev" source: hosted - version: "4.5.0" + version: "4.10.0" collection: dependency: "direct main" description: name: collection - sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687 + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a url: "https://pub.dev" source: hosted - version: "1.17.2" - computer: - dependency: "direct main" - description: - path: "." - ref: HEAD - resolved-ref: "82e365fed8a1a76f6eea0220de98389eed7b0445" - url: "https://github.com/ente-io/computer.git" - source: git - version: "3.2.1" + version: "1.18.0" confetti: dependency: "direct main" description: @@ -218,62 +225,38 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.0" - connectivity: + connectivity_plus: dependency: "direct main" description: - name: connectivity - sha256: a8e91263cf3e25fb5cc95e19dfde4999e32a648ac3b9e8a558a28165731678f8 + name: connectivity_plus + sha256: "224a77051d52a11fbad53dd57827594d3bd24f945af28bd70bab376d68d437f0" url: "https://pub.dev" source: hosted - version: "3.0.6" - connectivity_for_web: + version: "5.0.2" + connectivity_plus_platform_interface: dependency: transitive description: - name: connectivity_for_web - sha256: "01a390c1d5adc2ed1fa1f52d120c07fe9fd01166a93f965a832fd6cfc0ea6482" + name: connectivity_plus_platform_interface + sha256: cf1d1c28f4416f8c654d7dc3cd638ec586076255d407cef3ddbdaf178272a71a url: "https://pub.dev" source: hosted - version: "0.4.0+1" - connectivity_macos: - dependency: transitive - description: - name: connectivity_macos - sha256: "51ae08d5162eca9669b9d8951ed83ce19c5355a81149f94e4dee2740beb93628" - url: "https://pub.dev" - source: hosted - version: "0.2.1+2" - connectivity_platform_interface: - dependency: transitive - description: - name: connectivity_platform_interface - sha256: "2d82e942df9d49f29a24bb07fb5ce085d4a53e47818c62364d2b6deb9e0d7a8e" - url: "https://pub.dev" - source: hosted - version: "2.0.1" + version: "1.2.4" convert: - dependency: transitive + dependency: "direct main" description: name: convert sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" url: "https://pub.dev" source: hosted version: "3.1.1" - coverage: - dependency: transitive - description: - name: coverage - sha256: "595a29b55ce82d53398e1bcc2cba525d7bd7c59faeb2d2540e9d42c390cfeeeb" - url: "https://pub.dev" - source: hosted - version: "1.6.4" cross_file: dependency: transitive description: name: cross_file - sha256: "0b0036e8cccbfbe0555fd83c1d31a6f30b77a96b598b35a5d36dd41f718695e9" + sha256: "55d7b444feb71301ef6b8838dbc1ae02e63dd48c8773f3810ff53bb1e2945b32" url: "https://pub.dev" source: hosted - version: "0.3.3+4" + version: "0.3.4+1" crypto: dependency: transitive description: @@ -290,38 +273,39 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" - cupertino_icons: - dependency: "direct main" - description: - name: cupertino_icons - sha256: e35129dc44c9118cee2a5603506d823bab99c68393879edb440e0090d07586be - url: "https://pub.dev" - source: hosted - version: "1.0.5" dart_style: dependency: transitive description: name: dart_style - sha256: "1efa911ca7086affd35f463ca2fc1799584fb6aa89883cf0af8e3664d6a02d55" + sha256: "99e066ce75c89d6b29903d788a7bb9369cf754f7b24bf70bf4b6d6d6b26853b9" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.3.6" dbus: dependency: transitive description: name: dbus - sha256: "6f07cba3f7b3448d42d015bfd3d53fe12e5b36da2423f23838efc1d5fb31a263" + sha256: "365c771ac3b0e58845f39ec6deebc76e3276aa9922b0cc60840712094d9047ac" url: "https://pub.dev" source: hosted - version: "0.7.8" + version: "0.7.10" + desktop_webview_window: + dependency: "direct main" + description: + path: "packages/desktop_webview_window" + ref: HEAD + resolved-ref: "8cbbf9cd6efcfee5e0f420a36f7f8e7e64b667a1" + url: "https://github.com/MixinNetwork/flutter-plugins" + source: git + version: "0.2.4" device_info_plus: dependency: "direct main" description: name: device_info_plus - sha256: f52ab3b76b36ede4d135aab80194df8925b553686f0fa12226b4e2d658e45903 + sha256: "77f757b789ff68e4eaf9c56d1752309bd9f7ad557cb105b938a7f8eb89e59110" url: "https://pub.dev" source: hosted - version: "8.2.2" + version: "9.1.2" device_info_plus_platform_interface: dependency: transitive description: @@ -330,30 +314,22 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.0" - diff_match_patch: - dependency: transitive - description: - name: diff_match_patch - sha256: "2efc9e6e8f449d0abe15be240e2c2a3bcd977c8d126cfd70598aee60af35c0a4" - url: "https://pub.dev" - source: hosted - version: "0.4.1" dio: dependency: "direct main" description: name: dio - sha256: "7d328c4d898a61efc3cd93655a0955858e29a0aa647f0f9e02d59b3bb275e2e8" + sha256: "0978e9a3e45305a80a7210dbeaf79d6ee8bee33f70c8e542dc654c952070217f" url: "https://pub.dev" source: hosted - version: "4.0.6" + version: "5.4.2+1" dotted_border: dependency: "direct main" description: name: dotted_border - sha256: "07a5c5e8d4e6e992279e190e0352be8faa5b8f96d81c77a78b2d42f060279840" + sha256: "108837e11848ca776c53b30bc870086f84b62ed6e01c503ed976e8f8c7df9c04" url: "https://pub.dev" source: hosted - version: "2.0.0+3" + version: "2.1.0" email_validator: dependency: "direct main" description: @@ -362,6 +338,23 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.17" + ente_crypto_dart: + dependency: "direct main" + description: + path: "." + ref: HEAD + resolved-ref: e2e66ffd03f23bef5e0bb138b5f01b32d8e9b7bb + url: "https://github.com/ente-io/ente_crypto_dart.git" + source: git + version: "1.0.0" + equatable: + dependency: transitive + description: + name: equatable + sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2 + url: "https://pub.dev" + source: hosted + version: "2.0.5" event_bus: dependency: "direct main" description: @@ -395,39 +388,39 @@ packages: source: hosted version: "1.3.1" ffi: - dependency: transitive + dependency: "direct main" description: name: ffi - sha256: ed5337a5660c506388a9f012be0288fb38b49020ce2b45fe1f8b8323fe429f99 + sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21" url: "https://pub.dev" source: hosted - version: "2.0.2" + version: "2.1.2" file: dependency: transitive description: name: file - sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" + sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" url: "https://pub.dev" source: hosted - version: "6.1.4" + version: "7.0.0" file_picker: dependency: "direct main" description: name: file_picker - sha256: "9d6e95ec73abbd31ec54d0e0df8a961017e165aba1395e462e5b31ea0c165daf" + sha256: "1bbf65dd997458a08b531042ec3794112a6c39c07c37ff22113d2e7e4f81d4e4" url: "https://pub.dev" source: hosted - version: "5.3.1" + version: "6.2.1" file_saver: dependency: "direct main" description: name: file_saver - sha256: "591d25e750e3a4b654f7b0293abc2ed857242f82ca7334051b2a8ceeb369dac8" + sha256: bdebc720e17b3e01aba59da69b6d47020a7e5ba7d5c75bd9194f9618d5f16ef4 url: "https://pub.dev" source: hosted - version: "0.2.8" + version: "0.2.12" fixnum: - dependency: transitive + dependency: "direct main" description: name: fixnum sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" @@ -451,10 +444,18 @@ packages: dependency: "direct main" description: name: flutter_bloc - sha256: e74efb89ee6945bcbce74a5b3a5a3376b088e5f21f55c263fc38cbdc6237faae + sha256: f0ecf6e6eb955193ca60af2d5ca39565a86b8a142452c5b24d96fb477428f4d2 url: "https://pub.dev" source: hosted - version: "8.1.3" + version: "8.1.5" + flutter_context_menu: + dependency: "direct main" + description: + name: flutter_context_menu + sha256: "9f220a8fa0290c68e38000d6d62a0dc4555d490c15a5bd856a6e6d255d81b8dc" + url: "https://pub.dev" + source: hosted + version: "0.1.3" flutter_displaymode: dependency: "direct main" description: @@ -467,50 +468,107 @@ packages: dependency: "direct main" description: name: flutter_email_sender - sha256: "9e253c69617f43d4cb5de672e93a7a19c12a21fb6a75e66c6ce7626336c4c1bc" + sha256: "5001e9158f91a8799140fb30a11ad89cd587244f30b4f848d87085985c49b60f" url: "https://pub.dev" source: hosted - version: "5.2.0" + version: "6.0.2" flutter_inappwebview: dependency: "direct main" description: name: flutter_inappwebview - sha256: f73505c792cf083d5566e1a94002311be497d984b5607f25be36d685cf6361cf + sha256: "3e9a443a18ecef966fb930c3a76ca5ab6a7aafc0c7b5e14a4a850cf107b09959" url: "https://pub.dev" source: hosted - version: "5.7.2+3" + version: "6.0.0" + flutter_inappwebview_android: + dependency: transitive + description: + name: flutter_inappwebview_android + sha256: d247f6ed417f1f8c364612fa05a2ecba7f775c8d0c044c1d3b9ee33a6515c421 + url: "https://pub.dev" + source: hosted + version: "1.0.13" + flutter_inappwebview_internal_annotations: + dependency: transitive + description: + name: flutter_inappwebview_internal_annotations + sha256: "5f80fd30e208ddded7dbbcd0d569e7995f9f63d45ea3f548d8dd4c0b473fb4c8" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + flutter_inappwebview_ios: + dependency: transitive + description: + name: flutter_inappwebview_ios + sha256: f363577208b97b10b319cd0c428555cd8493e88b468019a8c5635a0e4312bd0f + url: "https://pub.dev" + source: hosted + version: "1.0.13" + flutter_inappwebview_macos: + dependency: transitive + description: + name: flutter_inappwebview_macos + sha256: b55b9e506c549ce88e26580351d2c71d54f4825901666bd6cfa4be9415bb2636 + url: "https://pub.dev" + source: hosted + version: "1.0.11" + flutter_inappwebview_platform_interface: + dependency: transitive + description: + name: flutter_inappwebview_platform_interface + sha256: "545fd4c25a07d2775f7d5af05a979b2cac4fbf79393b0a7f5d33ba39ba4f6187" + url: "https://pub.dev" + source: hosted + version: "1.0.10" + flutter_inappwebview_web: + dependency: transitive + description: + name: flutter_inappwebview_web + sha256: d8c680abfb6fec71609a700199635d38a744df0febd5544c5a020bd73de8ee07 + url: "https://pub.dev" + source: hosted + version: "1.0.8" flutter_launcher_icons: dependency: "direct main" description: name: flutter_launcher_icons - sha256: "559c600f056e7c704bd843723c21e01b5fba47e8824bd02422165bcc02a5de1d" + sha256: "526faf84284b86a4cb36d20a5e45147747b7563d921373d4ee0559c54fcdbcea" url: "https://pub.dev" source: hosted - version: "0.9.3" + version: "0.13.1" + flutter_local_authentication: + dependency: "direct main" + description: + path: "." + ref: "1ac346a04592a05fd75acccf2e01fa3c7e955d96" + resolved-ref: "1ac346a04592a05fd75acccf2e01fa3c7e955d96" + url: "https://github.com/eaceto/flutter_local_authentication" + source: git + version: "1.2.0" flutter_local_notifications: dependency: "direct main" description: name: flutter_local_notifications - sha256: f222919a34545931e47b06000836b5101baeffb0e6eb5a4691d2d42851740dd9 + sha256: "55b9b229307a10974b26296ff29f2e132256ba4bd74266939118eaefa941cb00" url: "https://pub.dev" source: hosted - version: "12.0.4" + version: "16.3.3" flutter_local_notifications_linux: dependency: transitive description: name: flutter_local_notifications_linux - sha256: "3c6d6db334f609a92be0c0915f40871ec56f5d2adf01e77ae364162c587c0ca8" + sha256: "33f741ef47b5f63cc7f78fe75eeeac7e19f171ff3c3df054d84c1e38bedb6a03" url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "4.0.0+1" flutter_local_notifications_platform_interface: dependency: transitive description: name: flutter_local_notifications_platform_interface - sha256: "5ec1feac5f7f7d9266759488bc5f76416152baba9aa1b26fe572246caa00d1ab" + sha256: "7cf643d6d5022f3baed0be777b0662cce5919c0a7b86e700299f22dc4ae660ef" url: "https://pub.dev" source: hosted - version: "6.0.0" + version: "7.0.0+1" flutter_localizations: dependency: "direct main" description: flutter @@ -520,99 +578,99 @@ packages: dependency: "direct main" description: name: flutter_native_splash - sha256: "6777a3abb974021a39b5fdd2d46a03ca390e03903b6351f21d10e7ecc969f12d" + sha256: edf39bcf4d74aca1eb2c1e43c3e445fd9f494013df7f0da752fefe72020eedc0 url: "https://pub.dev" source: hosted - version: "2.2.16" + version: "2.4.0" flutter_plugin_android_lifecycle: dependency: transitive description: name: flutter_plugin_android_lifecycle - sha256: "950e77c2bbe1692bc0874fc7fb491b96a4dc340457f4ea1641443d0a6c1ea360" + sha256: b068ffc46f82a55844acfa4fdbb61fad72fa2aef0905548419d97f0f95c456da url: "https://pub.dev" source: hosted - version: "2.0.15" + version: "2.0.17" flutter_secure_storage: dependency: "direct main" description: name: flutter_secure_storage - sha256: "98352186ee7ad3639ccc77ad7924b773ff6883076ab952437d20f18a61f0a7c5" + sha256: ffdbb60130e4665d2af814a0267c481bcf522c41ae2e43caf69fa0146876d685 url: "https://pub.dev" source: hosted - version: "8.0.0" + version: "9.0.0" flutter_secure_storage_linux: - dependency: transitive + dependency: "direct overridden" description: - name: flutter_secure_storage_linux - sha256: "0912ae29a572230ad52d8a4697e5518d7f0f429052fd51df7e5a7952c7efe2a3" - url: "https://pub.dev" - source: hosted - version: "1.1.3" + path: flutter_secure_storage_linux + ref: patch-1 + resolved-ref: da8ab43bc51c8c3249a261c33b27aa6f018f819b + url: "https://github.com/prateekmedia/flutter_secure_storage.git" + source: git + version: "1.2.0" flutter_secure_storage_macos: dependency: transitive description: name: flutter_secure_storage_macos - sha256: "083add01847fc1c80a07a08e1ed6927e9acd9618a35e330239d4422cd2a58c50" + sha256: bd33935b4b628abd0b86c8ca20655c5b36275c3a3f5194769a7b3f37c905369c url: "https://pub.dev" source: hosted - version: "3.0.0" + version: "3.0.1" flutter_secure_storage_platform_interface: dependency: transitive description: name: flutter_secure_storage_platform_interface - sha256: b3773190e385a3c8a382007893d678ae95462b3c2279e987b55d140d3b0cb81b + sha256: "0d4d3a5dd4db28c96ae414d7ba3b8422fd735a8255642774803b2532c9a61d7e" url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.0.2" flutter_secure_storage_web: dependency: transitive description: name: flutter_secure_storage_web - sha256: "42938e70d4b872e856e678c423cc0e9065d7d294f45bc41fc1981a4eb4beaffe" + sha256: "30f84f102df9dcdaa2241866a958c2ec976902ebdaa8883fbfe525f1f2f3cf20" url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" flutter_secure_storage_windows: dependency: transitive description: name: flutter_secure_storage_windows - sha256: fc2910ec9b28d60598216c29ea763b3a96c401f0ce1d13cdf69ccb0e5c93c3ee + sha256: "5809c66f9dd3b4b93b0a6e2e8561539405322ee767ac2f64d084e2ab5429d108" url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "3.0.0" flutter_slidable: dependency: "direct main" description: name: flutter_slidable - sha256: "6c68e1fad129b4b807b2218ef4cf7f7f6f61c5ec8861c990dc2278d9d03cb09f" + sha256: "673403d2eeef1f9e8483bd6d8d92aae73b1d8bd71f382bc3930f699c731bc27c" url: "https://pub.dev" source: hosted - version: "2.0.0" - flutter_sodium: - dependency: "direct main" - description: - path: "." - ref: HEAD - resolved-ref: "267435eaf07af60b94406adf14bedf21e08a6b4f" - url: "https://github.com/ente-io/flutter_sodium.git" - source: git - version: "0.2.0" + version: "3.1.0" flutter_speed_dial: dependency: "direct main" description: name: flutter_speed_dial - sha256: "41d7ad0bc224248637b3a5e0b9083e912a75445bdb450cf82b8ed06d7af7c61d" + sha256: "698a037274a66dbae8697c265440e6acb6ab6cae9ac5f95c749e7944d8f28d41" url: "https://pub.dev" source: hosted - version: "6.2.0" + version: "7.0.0" + flutter_staggered_grid_view: + dependency: "direct main" + description: + name: flutter_staggered_grid_view + sha256: "19e7abb550c96fbfeb546b23f3ff356ee7c59a019a651f8f102a4ba9b7349395" + url: "https://pub.dev" + source: hosted + version: "0.7.0" flutter_svg: dependency: "direct main" description: name: flutter_svg - sha256: f991fdb1533c3caeee0cdc14b04f50f0c3916f0dbcbc05237ccbe4e3c6b93f3f + sha256: "7b4ca6cf3304575fe9c8ec64813c8d02ee41d2afe60bcfe0678bcb5375d596a2" url: "https://pub.dev" source: hosted - version: "2.0.5" + version: "2.0.10+1" flutter_test: dependency: "direct dev" description: flutter @@ -627,18 +685,26 @@ packages: dependency: "direct main" description: name: fluttertoast - sha256: "474f7d506230897a3cd28c965ec21c5328ae5605fc9c400cd330e9e9d6ac175c" + sha256: dfdde255317af381bfc1c486ed968d5a43a2ded9c931e87cbecd88767d6a71c1 url: "https://pub.dev" source: hosted - version: "8.2.2" + version: "8.2.4" + freezed_annotation: + dependency: transitive + description: + name: freezed_annotation + sha256: c3fd9336eb55a38cc1bbd79ab17573113a8deccd0ecbbf926cca3c62803b5c2d + url: "https://pub.dev" + source: hosted + version: "2.4.1" frontend_server_client: dependency: transitive description: name: frontend_server_client - sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 url: "https://pub.dev" source: hosted - version: "3.2.0" + version: "4.0.0" glob: dependency: transitive description: @@ -663,6 +729,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.1" + gtk: + dependency: transitive + description: + name: gtk + sha256: e8ce9ca4b1df106e4d72dad201d345ea1a036cc12c360f1a7d5a758f78ffa42c + url: "https://pub.dev" + source: hosted + version: "2.1.0" hex: dependency: transitive description: @@ -683,10 +757,10 @@ packages: dependency: "direct main" description: name: http - sha256: "759d1a329847dd0f39226c688d3e06a6b8679668e350e2891a6474f8b4bb8525" + sha256: "761a297c042deedc1ffbb156d6e2af13886bb305c2a343a4d972504cd67dd938" url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.2.1" http_multi_server: dependency: transitive description: @@ -707,10 +781,10 @@ packages: dependency: transitive description: name: image - sha256: "8e9d133755c3e84c73288363e6343157c383a0c6c56fc51afcc5d4d7180306d6" + sha256: "4c68bfd5ae83e700b5204c1e74451e7bf3cf750e6843c6e158289cf56bda018e" url: "https://pub.dev" source: hosted - version: "3.3.0" + version: "4.1.7" intl: dependency: "direct main" description: @@ -747,58 +821,82 @@ packages: dependency: "direct dev" description: name: json_serializable - sha256: "43793352f90efa5d8b251893a63d767b2f7c833120e3cc02adad55eefec04dc7" + sha256: aa1f5a8912615733e0fdc7a02af03308933c93235bdc8d50d0b0c8a8ccb0b969 url: "https://pub.dev" source: hosted - version: "6.6.2" + version: "6.7.1" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" + url: "https://pub.dev" + source: hosted + version: "10.0.0" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 + url: "https://pub.dev" + source: hosted + version: "2.0.1" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 + url: "https://pub.dev" + source: hosted + version: "2.0.1" lints: dependency: "direct dev" description: name: lints - sha256: a2c3d198cb5ea2e179926622d433331d8b58374ab8f29cdda6e863bd62fd369c + sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290 url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "3.0.0" local_auth: dependency: "direct main" description: name: local_auth - sha256: "7e6c63082e399b61e4af71266b012e767a5d4525dd6e9ba41e174fd42d76e115" + sha256: "280421b416b32de31405b0a25c3bd42dfcef2538dfbb20c03019e02a5ed55ed0" url: "https://pub.dev" source: hosted - version: "2.1.7" + version: "2.2.0" local_auth_android: dependency: "direct main" description: name: local_auth_android - sha256: "523dd636ce061ddb296cbc3db410cb8f21efb7d8798f7b9532c8038ce2f8bad5" + sha256: "3bcd732dda7c75fcb7ddaef12e131230f53dcc8c00790d0d6efb3aa0fbbeda57" url: "https://pub.dev" source: hosted - version: "1.0.31" - local_auth_ios: + version: "1.0.37" + local_auth_darwin: dependency: "direct main" description: - name: local_auth_ios - sha256: edc2977c5145492f3451db9507a2f2f284ee4f408950b3e16670838726761940 + name: local_auth_darwin + sha256: "33381a15b0de2279523eca694089393bb146baebdce72a404555d03174ebc1e9" url: "https://pub.dev" source: hosted - version: "1.1.3" + version: "1.2.2" local_auth_platform_interface: dependency: transitive description: name: local_auth_platform_interface - sha256: "9e160d59ef0743e35f1b50f4fb84dc64f55676b1b8071e319ef35e7f3bc13367" + sha256: "1b842ff177a7068442eae093b64abe3592f816afd2a533c0ebcdbe40f9d2075a" url: "https://pub.dev" source: hosted - version: "1.0.7" + version: "1.0.10" local_auth_windows: dependency: transitive description: name: local_auth_windows - sha256: "19323b75ab781d5362dbb15dcb7e0916d2431c7a6dbdda016ec9708689877f73" + sha256: "505ba3367ca781efb1c50d3132e44a2446bccc4163427bc203b9b4d8994d97ea" url: "https://pub.dev" source: hosted - version: "1.0.8" + version: "1.0.10" logging: dependency: "direct main" description: @@ -811,50 +909,58 @@ packages: dependency: transitive description: name: matcher - sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb url: "https://pub.dev" source: hosted - version: "0.12.16" + version: "0.12.16+1" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" + sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" url: "https://pub.dev" source: hosted - version: "0.5.0" + version: "0.8.0" + menu_base: + dependency: transitive + description: + name: menu_base + sha256: "820368014a171bd1241030278e6c2617354f492f5c703d7b7d4570a6b8b84405" + url: "https://pub.dev" + source: hosted + version: "0.1.1" meta: dependency: transitive description: name: meta - sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" + sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 url: "https://pub.dev" source: hosted - version: "1.9.1" + version: "1.11.0" mime: dependency: transitive description: name: mime - sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e + sha256: "2e123074287cc9fd6c09de8336dae606d1ddb88d9ac47358826db698c176a1f2" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.0.5" mocktail: dependency: "direct dev" description: name: mocktail - sha256: "80a996cd9a69284b3dc521ce185ffe9150cde69767c2d3a0720147d93c0cef53" + sha256: c4b5007d91ca4f67256e720cb1b6d704e79a510183a12fa551021f652577dce6 url: "https://pub.dev" source: hosted - version: "0.3.0" + version: "1.0.3" modal_bottom_sheet: dependency: "direct main" description: name: modal_bottom_sheet - sha256: "3bba63c62d35c931bce7f8ae23a47f9a05836d8cb3c11122ada64e0b2f3d718f" + sha256: eac66ef8cb0461bf069a38c5eb0fa728cee525a531a8304bd3f7b2185407c67e url: "https://pub.dev" source: hosted - version: "3.0.0-pre" + version: "3.0.0" move_to_background: dependency: "direct main" description: @@ -871,14 +977,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" - node_preamble: + nm: dependency: transitive description: - name: node_preamble - sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" + name: nm + sha256: "2c9aae4127bdc8993206464fcc063611e0e36e72018696cd9631023a31b24254" url: "https://pub.dev" source: hosted - version: "2.0.2" + version: "0.5.0" otp: dependency: "direct main" description: @@ -899,10 +1005,10 @@ packages: dependency: "direct main" description: name: package_info_plus - sha256: "6ff267fcd9d48cb61c8df74a82680e8b82e940231bb5f68356672fde0397334a" + sha256: "7e76fad405b3e4016cd39d08f455a4eb5199723cf594cd1b8916d47140d93017" url: "https://pub.dev" source: hosted - version: "4.1.0" + version: "4.2.0" package_info_plus_platform_interface: dependency: transitive description: @@ -920,13 +1026,13 @@ packages: source: hosted version: "0.2.0" path: - dependency: transitive + dependency: "direct main" description: name: path - sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" url: "https://pub.dev" source: hosted - version: "1.8.3" + version: "1.9.0" path_drawing: dependency: transitive description: @@ -947,90 +1053,90 @@ packages: dependency: "direct main" description: name: path_provider - sha256: "3087813781ab814e4157b172f1a11c46be20179fcc9bea043e0fba36bc0acaa2" + sha256: b27217933eeeba8ff24845c34003b003b2b22151de3c908d0e679e8fe1aa078b url: "https://pub.dev" source: hosted - version: "2.0.15" + version: "2.1.2" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: "2cec049d282c7f13c594b4a73976b0b4f2d7a1838a6dd5aaf7bd9719196bee86" + sha256: "477184d672607c0a3bf68fbbf601805f92ef79c82b64b4d6eb318cbca4c48668" url: "https://pub.dev" source: hosted - version: "2.0.27" + version: "2.2.2" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: "916731ccbdce44d545414dd9961f26ba5fbaa74bcbb55237d8e65a623a8c7297" + sha256: "5a7999be66e000916500be4f15a3633ebceb8302719b47b9cc49ce924125350f" url: "https://pub.dev" source: hosted - version: "2.2.4" + version: "2.3.2" path_provider_linux: dependency: transitive description: name: path_provider_linux - sha256: ffbb8cc9ed2c9ec0e4b7a541e56fd79b138e8f47d2fb86815f15358a349b3b57 + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 url: "https://pub.dev" source: hosted - version: "2.1.11" + version: "2.2.1" path_provider_platform_interface: dependency: transitive description: name: path_provider_platform_interface - sha256: "57585299a729335f1298b43245842678cb9f43a6310351b18fb577d6e33165ec" + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" url: "https://pub.dev" source: hosted - version: "2.0.6" + version: "2.1.2" path_provider_windows: dependency: transitive description: name: path_provider_windows - sha256: "1cb68ba4cd3a795033de62ba1b7b4564dace301f952de6bfb3cd91b202b6ee96" + sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" url: "https://pub.dev" source: hosted - version: "2.1.7" + version: "2.2.1" petitparser: dependency: transitive description: name: petitparser - sha256: "49392a45ced973e8d94a85fdb21293fbb40ba805fc49f2965101ae748a3683b4" + sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27 url: "https://pub.dev" source: hosted - version: "5.1.0" + version: "6.0.2" pinput: dependency: "direct main" description: name: pinput - sha256: "27eb69042f75755bdb6544f6e79a50a6ed09d6e97e2d75c8421744df1e392949" + sha256: a92b55ecf9c25d1b9e100af45905385d5bc34fc9b6b04177a9e82cb88fe4d805 url: "https://pub.dev" source: hosted - version: "1.2.2" + version: "3.0.1" platform: dependency: transitive description: name: platform - sha256: "4a451831508d7d6ca779f7ac6e212b4023dd5a7d08a27a63da33756410e32b76" + sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.1.4" plugin_platform_interface: dependency: transitive description: name: plugin_platform_interface - sha256: "6a2128648c854906c53fa8e33986fc0247a1116122f9534dd20e3ab9e16a32bc" + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.1.8" pointycastle: dependency: "direct main" description: name: pointycastle - sha256: "7c1e5f0d23c9016c5bbd8b1473d0d3fb3fc851b876046039509e18e0c7485f2c" + sha256: "43ac87de6e10afabc85c445745a7b799e04de84cebaa4fd7bf55a5e1e9604d29" url: "https://pub.dev" source: hosted - version: "3.7.3" + version: "3.7.4" pool: dependency: transitive description: @@ -1047,30 +1153,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.0.6" - process: - dependency: transitive - description: - name: process - sha256: "53fd8db9cec1d37b0574e12f07520d582019cb6c44abf5479a01505099a34a09" - url: "https://pub.dev" - source: hosted - version: "4.2.4" protobuf: dependency: "direct main" description: name: protobuf - sha256: "4034a02b7e231e7e60bff30a8ac13a7347abfdac0798595fae0b90a3f0afe759" + sha256: "68645b24e0716782e58948f8467fd42a880f255096a821f9e7d0ec625b00c84d" url: "https://pub.dev" source: hosted - version: "3.0.0" + version: "3.1.0" provider: dependency: transitive description: name: provider - sha256: cdbe7530b12ecd9eb455bdaa2fcb8d4dad22e80b8afb4798b41479d5ce26847f + sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c url: "https://pub.dev" source: hosted - version: "6.0.5" + version: "6.1.2" pub_semver: dependency: transitive description: @@ -1091,10 +1189,10 @@ packages: dependency: transitive description: name: qr - sha256: "5c4208b4dc0d55c3184d10d83ee0ded6212dc2b5e2ba17c5a0c0aab279128d21" + sha256: "64957a3930367bf97cc211a5af99551d630f2f4625e38af10edd6b19131b64b3" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "3.0.1" qr_code_scanner: dependency: "direct main" description: @@ -1107,98 +1205,106 @@ packages: dependency: "direct main" description: name: qr_flutter - sha256: c5c121c54cb6dd837b9b9d57eb7bc7ec6df4aee741032060c8833a678c80b87e + sha256: "5095f0fc6e3f71d08adef8feccc8cea4f12eec18a2e31c2e8d82cb6019f4b097" url: "https://pub.dev" source: hosted - version: "4.0.0" + version: "4.1.0" + screen_retriever: + dependency: transitive + description: + name: screen_retriever + sha256: "6ee02c8a1158e6dae7ca430da79436e3b1c9563c8cf02f524af997c201ac2b90" + url: "https://pub.dev" + source: hosted + version: "0.1.9" sentry: dependency: "direct main" description: name: sentry - sha256: "39c23342fc96105da449914f7774139a17a0ca8a4e70d9ad5200171f7e47d6ba" + sha256: a460aa48568d47140dd0557410b624d344ffb8c05555107ac65035c1097cf1ad url: "https://pub.dev" source: hosted - version: "7.9.0" + version: "7.18.0" sentry_flutter: dependency: "direct main" description: name: sentry_flutter - sha256: ff68ab31918690da004a42e20204242a3ad9ad57da7e2712da8487060ac9767f + sha256: "3d0d1d4e0e407d276ae8128d123263ccbc37e988bae906765efd6f37d544f4c6" url: "https://pub.dev" source: hosted - version: "7.9.0" + version: "7.18.0" share_plus: dependency: "direct main" description: name: share_plus - sha256: f74fc3f1cbd99f39760182e176802f693fa0ec9625c045561cfad54681ea93dd + sha256: "3ef39599b00059db0990ca2e30fca0a29d8b37aae924d60063f8e0184cf20900" url: "https://pub.dev" source: hosted - version: "7.2.1" + version: "7.2.2" share_plus_platform_interface: dependency: transitive description: name: share_plus_platform_interface - sha256: df08bc3a07d01f5ea47b45d03ffcba1fa9cd5370fb44b3f38c70e42cced0f956 + sha256: "251eb156a8b5fa9ce033747d73535bf53911071f8d3b6f4f0b578505ce0d4496" url: "https://pub.dev" source: hosted - version: "3.3.1" + version: "3.4.0" shared_preferences: dependency: "direct main" description: name: shared_preferences - sha256: "0344316c947ffeb3a529eac929e1978fcd37c26be4e8468628bac399365a3ca1" + sha256: "81429e4481e1ccfb51ede496e916348668fd0921627779233bd24cc3ff6abd02" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.2.2" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: fe8401ec5b6dcd739a0fe9588802069e608c3fdbfd3c3c93e546cf2f90438076 + sha256: "8568a389334b6e83415b6aae55378e158fbc2314e074983362d20c562780fb06" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.2.1" shared_preferences_foundation: dependency: transitive description: name: shared_preferences_foundation - sha256: f39696b83e844923b642ce9dd4bd31736c17e697f6731a5adf445b1274cf3cd4 + sha256: "7708d83064f38060c7b39db12aefe449cb8cdc031d6062280087bc4cdb988f5c" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.3.5" shared_preferences_linux: dependency: transitive description: name: shared_preferences_linux - sha256: "71d6806d1449b0a9d4e85e0c7a917771e672a3d5dc61149cc9fac871115018e1" + sha256: "9f2cbcf46d4270ea8be39fa156d86379077c8a5228d9dfdb1164ae0bb93f1faa" url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.3.2" shared_preferences_platform_interface: dependency: transitive description: name: shared_preferences_platform_interface - sha256: "23b052f17a25b90ff2b61aad4cc962154da76fb62848a9ce088efe30d7c50ab1" + sha256: "22e2ecac9419b4246d7c22bfbbda589e3acf5c0351137d87dd2939d984d37c3b" url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.3.2" shared_preferences_web: dependency: transitive description: name: shared_preferences_web - sha256: "7347b194fb0bbeb4058e6a4e87ee70350b6b2b90f8ac5f8bd5b3a01548f6d33a" + sha256: "9aee1089b36bd2aafe06582b7d7817fd317ef05fc30e6ba14bff247d0933042a" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.3.0" shared_preferences_windows: dependency: transitive description: name: shared_preferences_windows - sha256: f95e6a43162bce43c9c3405f3eb6f39e5b5d11f65fab19196cf8225e2777624d + sha256: "841ad54f3c8381c480d0c9b508b89a34036f512482c407e6df7a9c4aa2ef8f59" url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.3.2" shelf: dependency: transitive description: @@ -1207,22 +1313,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.1" - shelf_packages_handler: - dependency: transitive - description: - name: shelf_packages_handler - sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e" - url: "https://pub.dev" - source: hosted - version: "3.0.2" - shelf_static: - dependency: transitive - description: - name: shelf_static - sha256: a41d3f53c4adf0f57480578c1d61d90342cd617de7fc8077b1304643c2d85c1e - url: "https://pub.dev" - source: hosted - version: "1.1.2" shelf_web_socket: dependency: transitive description: @@ -1231,19 +1321,51 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.4" + shortid: + dependency: transitive + description: + name: shortid + sha256: d0b40e3dbb50497dad107e19c54ca7de0d1a274eb9b4404991e443dadb9ebedb + url: "https://pub.dev" + source: hosted + version: "0.1.2" sky_engine: dependency: transitive description: flutter source: sdk version: "0.0.99" + smart_auth: + dependency: transitive + description: + name: smart_auth + sha256: a25229b38c02f733d0a4e98d941b42bed91a976cb589e934895e60ccfa674cf6 + url: "https://pub.dev" + source: hosted + version: "1.1.1" + sodium: + dependency: transitive + description: + name: sodium + sha256: d9830a388e37c82891888e64cfd4c6764fa3ac716bed80ac6eab89ee42c3cd76 + url: "https://pub.dev" + source: hosted + version: "2.3.1+1" + sodium_libs: + dependency: transitive + description: + name: sodium_libs + sha256: f7f6719b7ab3e8512ce7a5ecd7bc8d865482431cdd5a07a46b55b13c152b54e1 + url: "https://pub.dev" + source: hosted + version: "2.2.1+1" source_gen: dependency: transitive description: name: source_gen - sha256: "373f96cf5a8744bc9816c1ff41cf5391bbdbe3d7a96fe98c622b6738a8a7bd33" + sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832" url: "https://pub.dev" source: hosted - version: "1.3.2" + version: "1.5.0" source_helper: dependency: transitive description: @@ -1252,22 +1374,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.4" - source_map_stack_trace: - dependency: transitive - description: - name: source_map_stack_trace - sha256: "84cf769ad83aa6bb61e0aa5a18e53aea683395f196a6f39c4c881fb90ed4f7ae" - url: "https://pub.dev" - source: hosted - version: "2.1.1" - source_maps: - dependency: transitive - description: - name: source_maps - sha256: "708b3f6b97248e5781f493b765c3337db11c5d2c81c3094f10904bfa8004c703" - url: "https://pub.dev" - source: hosted - version: "0.10.12" source_span: dependency: transitive description: @@ -1276,30 +1382,63 @@ packages: url: "https://pub.dev" source: hosted version: "1.10.0" + sprintf: + dependency: transitive + description: + name: sprintf + sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" + url: "https://pub.dev" + source: hosted + version: "7.0.0" sqflite: dependency: "direct main" description: - name: sqflite - sha256: b4d6710e1200e96845747e37338ea8a819a12b51689a3bcf31eff0003b37a0b9 - url: "https://pub.dev" - source: hosted - version: "2.2.8+4" + path: sqflite + ref: HEAD + resolved-ref: "075b3e2f81e691a19a500e7ff6db2953de7f83a9" + url: "https://github.com/tekartik/sqflite" + source: git + version: "2.3.2" sqflite_common: dependency: transitive description: name: sqflite_common - sha256: "8f7603f3f8f126740bc55c4ca2d1027aab4b74a1267a3e31ce51fe40e3b65b8f" + sha256: "3da423ce7baf868be70e2c0976c28a1bb2f73644268b7ffa7d2e08eab71f16a4" url: "https://pub.dev" source: hosted - version: "2.4.5+1" + version: "2.5.4" + sqflite_common_ffi: + dependency: "direct main" + description: + name: sqflite_common_ffi + sha256: "4d6137c29e930d6e4a8ff373989dd9de7bac12e3bc87bce950f6e844e8ad3bb5" + url: "https://pub.dev" + source: hosted + version: "2.3.3" + sqlite3: + dependency: "direct main" + description: + name: sqlite3 + sha256: "072128763f1547e3e9b4735ce846bfd226d68019ccda54db4cd427b12dfdedc9" + url: "https://pub.dev" + source: hosted + version: "2.4.0" + sqlite3_flutter_libs: + dependency: "direct main" + description: + name: sqlite3_flutter_libs + sha256: d6c31c8511c441d1f12f20b607343df1afe4eddf24a1cf85021677c8eea26060 + url: "https://pub.dev" + source: hosted + version: "0.5.20" stack_trace: dependency: transitive description: name: stack_trace - sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.11.1" step_progress_indicator: dependency: "direct main" description: @@ -1312,10 +1451,10 @@ packages: dependency: transitive description: name: stream_channel - sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" stream_transform: dependency: transitive description: @@ -1336,18 +1475,18 @@ packages: dependency: "direct main" description: name: styled_text - sha256: f72928d1ebe8cb149e3b34a689cb1ddca696b808187cf40ac3a0bd183dff379c + sha256: fd624172cf629751b4f171dd0ecf9acf02a06df3f8a81bb56c0caa4f1df706c3 url: "https://pub.dev" source: hosted - version: "7.0.0" + version: "8.1.0" synchronized: dependency: transitive description: name: synchronized - sha256: "5fcbd27688af6082f5abd611af56ee575342c30e87541d0245f7ff99faa02c60" + sha256: "539ef412b170d65ecdafd780f924e5be3f60032a1128df156adad6c5b373d558" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.1.0+1" term_glyph: dependency: transitive description: @@ -1356,30 +1495,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.1" - test: - dependency: transitive - description: - name: test - sha256: "13b41f318e2a5751c3169137103b60c584297353d4b1761b66029bae6411fe46" - url: "https://pub.dev" - source: hosted - version: "1.24.3" test_api: dependency: transitive description: name: test_api - sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8" + sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" url: "https://pub.dev" source: hosted - version: "0.6.0" - test_core: - dependency: transitive - description: - name: test_core - sha256: "99806e9e6d95c7b059b7a0fc08f07fc53fabe54a829497f0d9676299f1e8637e" - url: "https://pub.dev" - source: hosted - version: "0.5.3" + version: "0.6.1" timezone: dependency: transitive description: @@ -1396,6 +1519,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.1" + tray_manager: + dependency: "direct main" + description: + name: tray_manager + sha256: "4ab709d70a4374af172f8c39e018db33a4271265549c6fc9d269a65e5f4b0225" + url: "https://pub.dev" + source: hosted + version: "0.2.1" tuple: dependency: "direct main" description: @@ -1412,134 +1543,118 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.2" - uni_links: - dependency: "direct main" - description: - name: uni_links - sha256: "051098acfc9e26a9fde03b487bef5d3d228ca8f67693480c6f33fd4fbb8e2b6e" - url: "https://pub.dev" - source: hosted - version: "0.5.1" - uni_links_platform_interface: - dependency: transitive - description: - name: uni_links_platform_interface - sha256: "929cf1a71b59e3b7c2d8a2605a9cf7e0b125b13bc858e55083d88c62722d4507" - url: "https://pub.dev" - source: hosted - version: "1.0.0" - uni_links_web: - dependency: transitive - description: - name: uni_links_web - sha256: "7539db908e25f67de2438e33cc1020b30ab94e66720b5677ba6763b25f6394df" - url: "https://pub.dev" - source: hosted - version: "0.1.0" universal_io: dependency: transitive description: name: universal_io - sha256: "06866290206d196064fd61df4c7aea1ffe9a4e7c4ccaa8fcded42dd41948005d" + sha256: "1722b2dcc462b4b2f3ee7d188dad008b6eb4c40bbd03a3de451d82c78bba9aad" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.2.2" + universal_platform: + dependency: transitive + description: + name: universal_platform + sha256: d315be0f6641898b280ffa34e2ddb14f3d12b1a37882557869646e0cc363d0cc + url: "https://pub.dev" + source: hosted + version: "1.0.0+1" url_launcher: dependency: "direct main" description: name: url_launcher - sha256: eb1e00ab44303d50dd487aab67ebc575456c146c6af44422f9c13889984c00f3 + sha256: "0ecc004c62fd3ed36a2ffcbe0dd9700aee63bd7532d0b642a488b1ec310f492e" url: "https://pub.dev" source: hosted - version: "6.1.11" + version: "6.2.5" url_launcher_android: dependency: transitive description: name: url_launcher_android - sha256: "15f5acbf0dce90146a0f5a2c4a002b1814a6303c4c5c075aa2623b2d16156f03" + sha256: d4ed0711849dd8e33eb2dd69c25db0d0d3fdc37e0a62e629fe32f57a22db2745 url: "https://pub.dev" source: hosted - version: "6.0.36" + version: "6.3.0" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: "9af7ea73259886b92199f9e42c116072f05ff9bea2dcb339ab935dfc957392c2" + sha256: "9149d493b075ed740901f3ee844a38a00b33116c7c5c10d7fb27df8987fb51d5" url: "https://pub.dev" source: hosted - version: "6.1.4" + version: "6.2.5" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - sha256: "207f4ddda99b95b4d4868320a352d374b0b7e05eefad95a4a26f57da413443f5" + sha256: ab360eb661f8879369acac07b6bb3ff09d9471155357da8443fd5d3cf7363811 url: "https://pub.dev" source: hosted - version: "3.0.5" + version: "3.1.1" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - sha256: "1c4fdc0bfea61a70792ce97157e5cc17260f61abbe4f39354513f39ec6fd73b1" + sha256: b7244901ea3cf489c5335bdacda07264a6e960b1c1b1a9f91e4bc371d9e68234 url: "https://pub.dev" source: hosted - version: "3.0.6" + version: "3.1.0" url_launcher_platform_interface: dependency: transitive description: name: url_launcher_platform_interface - sha256: bfdfa402f1f3298637d71ca8ecfe840b4696698213d5346e9d12d4ab647ee2ea + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.3.2" url_launcher_web: dependency: transitive description: name: url_launcher_web - sha256: cc26720eefe98c1b71d85f9dc7ef0cada5132617046369d9dc296b3ecaa5cbb4 + sha256: "3692a459204a33e04bc94f5fb91158faf4f2c8903281ddd82915adecdb1a901d" url: "https://pub.dev" source: hosted - version: "2.0.18" + version: "2.3.0" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - sha256: "7967065dd2b5fccc18c653b97958fdf839c5478c28e767c61ee879f4e7882422" + sha256: ecf9725510600aa2bb6d7ddabe16357691b6d2805f66216a97d1b881e21beff7 url: "https://pub.dev" source: hosted - version: "3.0.7" + version: "3.1.1" uuid: dependency: "direct main" description: name: uuid - sha256: "648e103079f7c64a36dc7d39369cabb358d377078a051d6ae2ad3aa539519313" + sha256: cd210a09f7c18cbe5a02511718e0334de6559871052c90a90c0cca46a4aa81c8 url: "https://pub.dev" source: hosted - version: "3.0.7" + version: "4.3.3" vector_graphics: dependency: transitive description: name: vector_graphics - sha256: ea8d3fc7b2e0f35de38a7465063ecfcf03d8217f7962aa2a6717132cb5d43a79 + sha256: "32c3c684e02f9bc0afb0ae0aa653337a2fe022e8ab064bcd7ffda27a74e288e3" url: "https://pub.dev" source: hosted - version: "1.1.5" + version: "1.1.11+1" vector_graphics_codec: dependency: transitive description: name: vector_graphics_codec - sha256: a5eaa5d19e123ad4f61c3718ca1ed921c4e6254238d9145f82aa214955d9aced + sha256: c86987475f162fadff579e7320c7ddda04cd2fdeffbe1129227a85d9ac9e03da url: "https://pub.dev" source: hosted - version: "1.1.5" + version: "1.1.11+1" vector_graphics_compiler: dependency: transitive description: name: vector_graphics_compiler - sha256: "15edc42f7eaa478ce854eaf1fbb9062a899c0e4e56e775dd73b7f4709c97c4ca" + sha256: "12faff3f73b1741a36ca7e31b292ddeb629af819ca9efe9953b70bd63fc8cd81" url: "https://pub.dev" source: hosted - version: "1.1.5" + version: "1.1.11+1" vector_math: dependency: transitive description: @@ -1552,74 +1667,82 @@ packages: dependency: transitive description: name: vm_service - sha256: c538be99af830f478718b51630ec1b6bee5e74e52c8a802d328d9e71d35d2583 + sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 url: "https://pub.dev" source: hosted - version: "11.10.0" + version: "13.0.0" watcher: dependency: transitive description: name: watcher - sha256: "6a7f46926b01ce81bfc339da6a7f20afbe7733eff9846f6d6a5466aa4c6667c0" + sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" url: "https://pub.dev" source: hosted - version: "1.0.2" + version: "1.1.0" web: dependency: transitive description: name: web - sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10 + sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27" url: "https://pub.dev" source: hosted - version: "0.1.4-beta" + version: "0.5.1" web_socket_channel: dependency: transitive description: name: web_socket_channel - sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b + sha256: "1d8e795e2a8b3730c41b8a98a2dff2e0fb57ae6f0764a1c46ec5915387d257b2" url: "https://pub.dev" source: hosted - version: "2.4.0" - webkit_inspection_protocol: - dependency: transitive - description: - name: webkit_inspection_protocol - sha256: "67d3a8b6c79e1987d19d848b0892e582dbb0c66c57cc1fef58a177dd2aa2823d" - url: "https://pub.dev" - source: hosted - version: "1.2.0" + version: "2.4.4" win32: - dependency: transitive + dependency: "direct main" description: name: win32 - sha256: "5a751eddf9db89b3e5f9d50c20ab8612296e4e8db69009788d6c8b060a84191c" + sha256: "8cb58b45c47dcb42ab3651533626161d6b67a2921917d8d429791f76972b3480" url: "https://pub.dev" source: hosted - version: "4.1.4" + version: "5.3.0" + win32_registry: + dependency: transitive + description: + name: win32_registry + sha256: "41fd8a189940d8696b1b810efb9abcf60827b6cbfab90b0c43e8439e3a39d85a" + url: "https://pub.dev" + source: hosted + version: "1.1.2" + window_manager: + dependency: "direct main" + description: + name: window_manager + sha256: b3c895bdf936c77b83c5254bec2e6b3f066710c1f89c38b20b8acc382b525494 + url: "https://pub.dev" + source: hosted + version: "0.3.8" xdg_directories: dependency: transitive description: name: xdg_directories - sha256: bd512f03919aac5f1313eb8249f223bacf4927031bf60b02601f81f687689e86 + sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d url: "https://pub.dev" source: hosted - version: "0.2.0+3" + version: "1.0.4" xml: dependency: transitive description: name: xml - sha256: "979ee37d622dec6365e2efa4d906c37470995871fe9ae080d967e192d88286b5" + sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 url: "https://pub.dev" source: hosted - version: "6.2.2" + version: "6.5.0" xmlstream: dependency: transitive description: name: xmlstream - sha256: "2d10c69a9d5fc46f71798b80ee6db15bc0d5bf560fdbdd264776cbeee0c83631" + sha256: cfc14e3f256997897df9481ae630d94c2d85ada5187ebeb868bb1aabc2c977b4 url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.1.1" yaml: dependency: transitive description: @@ -1629,5 +1752,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.1.0-185.0.dev <4.0.0" - flutter: ">=3.10.0" + dart: ">=3.3.0 <4.0.0" + flutter: ">=3.19.0" diff --git a/auth/pubspec.yaml b/auth/pubspec.yaml index 5e78084078..7f8e20bc80 100644 --- a/auth/pubspec.yaml +++ b/auth/pubspec.yaml @@ -1,6 +1,6 @@ name: ente_auth description: ente two-factor authenticator -version: 2.0.42+242 +version: 2.0.50+250 publish_to: none environment: @@ -8,87 +8,112 @@ environment: dependencies: adaptive_theme: ^3.1.0 # done + app_links: ^3.5.0 archive: ^3.3.7 base32: ^2.1.3 bip39: ^1.0.6 #done - bloc: ^8.0.3 #done + bloc: ^8.1.2 clipboard: ^0.1.3 collection: # dart - computer: - git: "https://github.com/ente-io/computer.git" confetti: ^0.7.0 - connectivity: ^3.0.3 - cupertino_icons: ^1.0.0 - device_info_plus: ^8.0.0 - dio: ^4.0.6 + connectivity_plus: ^5.0.2 + convert: ^3.1.1 + desktop_webview_window: + git: + url: https://github.com/MixinNetwork/flutter-plugins + path: packages/desktop_webview_window + device_info_plus: ^9.1.1 + dio: ^5.4.0 dotted_border: ^2.0.0+2 email_validator: ^2.0.1 + ente_crypto_dart: + git: + url: https://github.com/ente-io/ente_crypto_dart.git event_bus: ^2.0.0 expandable: ^5.0.1 expansion_tile_card: ^3.0.0 - file_picker: ^5.2.4 + ffi: ^2.1.0 + file_picker: ^6.1.1 # https://github.com/incrediblezayed/file_saver/issues/86 - file_saver: 0.2.8 + file_saver: ^0.2.11 + fixnum: ^1.1.0 fk_user_agent: ^2.1.0 flutter: sdk: flutter flutter_bloc: ^8.0.1 + flutter_context_menu: ^0.1.3 flutter_displaymode: ^0.6.0 - flutter_email_sender: ^5.1.0 - flutter_inappwebview: ^5.7.1 - flutter_launcher_icons: ^0.9.3 - flutter_local_notifications: ^12.0.3 + flutter_email_sender: ^6.0.2 + flutter_inappwebview: ^6.0.0 + flutter_launcher_icons: ^0.13.1 + flutter_local_authentication: + git: + url: https://github.com/eaceto/flutter_local_authentication + ref: 1ac346a04592a05fd75acccf2e01fa3c7e955d96 + flutter_local_notifications: ^16.3.1+1 flutter_localizations: sdk: flutter flutter_native_splash: ^2.2.13 - flutter_secure_storage: ^8.0.0 - flutter_slidable: ^2.0.0 - flutter_sodium: - git: - url: https://github.com/ente-io/flutter_sodium.git - flutter_speed_dial: ^6.2.0 + flutter_secure_storage: ^9.0.0 + flutter_slidable: ^3.0.1 + flutter_speed_dial: ^7.0.0 + flutter_staggered_grid_view: ^0.7.0 flutter_svg: ^2.0.5 fluttertoast: ^8.1.1 google_nav_bar: ^5.0.5 #supported http: ^1.1.0 intl: ^0.18.0 json_annotation: ^4.5.0 - local_auth: ^2.1.7 - local_auth_android: ^1.0.31 - local_auth_ios: ^1.1.3 + local_auth: ^2.2.0 + local_auth_android: ^1.0.37 + local_auth_darwin: ^1.2.2 logging: ^1.0.1 modal_bottom_sheet: ^3.0.0-pre move_to_background: ^1.0.2 otp: ^3.1.1 package_info_plus: ^4.1.0 password_strength: ^0.2.0 + path: ^1.8.3 path_provider: ^2.0.11 - pinput: ^1.2.2 + pinput: ^3.0.1 pointycastle: ^3.7.3 privacy_screen: ^0.0.6 protobuf: ^3.0.0 qr_code_scanner: ^1.0.1 - qr_flutter: 4.0.0 + qr_flutter: ^4.1.0 sentry: ^7.9.0 sentry_flutter: ^7.9.0 share_plus: ^7.2.1 shared_preferences: ^2.0.5 - sqflite: ^2.1.0 + sqflite: + git: + url: https://github.com/tekartik/sqflite + path: sqflite + sqflite_common_ffi: ^2.3.0+4 + sqlite3: ^2.1.0 + sqlite3_flutter_libs: ^0.5.19+1 step_progress_indicator: ^1.0.2 - styled_text: ^7.0.0 + styled_text: ^8.1.0 + tray_manager: ^0.2.1 tuple: ^2.0.0 - uni_links: ^0.5.1 url_launcher: ^6.1.5 - uuid: ^3.0.4 + uuid: ^4.2.2 + win32: ^5.1.1 + window_manager: ^0.3.8 +dependency_overrides: + flutter_secure_storage_linux: + git: + url: https://github.com/prateekmedia/flutter_secure_storage.git + ref: patch-1 + path: flutter_secure_storage_linux dev_dependencies: - bloc_test: ^9.0.3 build_runner: ^2.1.11 flutter_test: sdk: flutter json_serializable: ^6.2.0 - lints: ^1.0.1 - mocktail: ^0.3.0 + lints: ^3.0.0 + mocktail: ^1.0.3 # The following section is specific to Flutter. flutter: @@ -98,6 +123,7 @@ flutter: # https://docs:flutter:dev/development/ui/assets-and-images: assets: - assets/ + - assets/icons/ - assets/simple-icons/icons/ - assets/simple-icons/_data/ - assets/custom-icons/icons/ @@ -117,10 +143,10 @@ flutter: flutter_icons: android: "launcher_icon" - adaptive_icon_foreground: "assets/icon-light-adaptive-fg.png" + adaptive_icon_foreground: "assets/generation-icons/icon-light-adaptive-fg.png" adaptive_icon_background: "#ffffff" ios: true - image_path: "assets/icon-light.png" + image_path: "assets/generation-icons/icon-light.png" remove_alpha_ios: true flutter_native_splash: diff --git a/auth/web/index.html b/auth/web/index.html index 998d81d851..ef953df53b 100644 --- a/auth/web/index.html +++ b/auth/web/index.html @@ -21,13 +21,13 @@ - + - My App + Auth diff --git a/auth/web/manifest.json b/auth/web/manifest.json index 2323599530..eaf5b0fab9 100644 --- a/auth/web/manifest.json +++ b/auth/web/manifest.json @@ -1,6 +1,6 @@ { - "name": "My App", - "short_name": "My App", + "name": "Auth", + "short_name": "Auth", "start_url": ".", "display": "standalone", "background_color": "#0175C2", diff --git a/auth/windows/CMakeLists.txt b/auth/windows/CMakeLists.txt index d481d1864b..46c3159727 100644 --- a/auth/windows/CMakeLists.txt +++ b/auth/windows/CMakeLists.txt @@ -1,13 +1,16 @@ +# Project-level configuration. cmake_minimum_required(VERSION 3.14) -project(ente_auth LANGUAGES CXX) +project(auth LANGUAGES CXX) -set(BINARY_NAME "ente_auth") +# The name of the executable created for the application. Change this to change +# the on-disk name of your application. +set(BINARY_NAME "auth") -cmake_policy(SET CMP0063 NEW) +# Explicitly opt in to modern CMake behaviors to avoid warnings with recent +# versions of CMake. +cmake_policy(VERSION 3.14...3.25) -set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") - -# Configure build options. +# Define build configuration option. get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) if(IS_MULTICONFIG) set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" @@ -20,7 +23,7 @@ else() "Debug" "Profile" "Release") endif() endif() - +# Define settings for the Profile build mode. set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") @@ -29,7 +32,11 @@ set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") # Use Unicode for all projects. add_definitions(-DUNICODE -D_UNICODE) -# Compilation ui.settings that should be applied to most targets. +# Compilation settings that should be applied to most targets. +# +# Be cautious about adding new options here, as plugins use this function by +# default. In most cases, you should add new options to specific targets instead +# of modifying this function. function(APPLY_STANDARD_SETTINGS TARGET) target_compile_features(${TARGET} PUBLIC cxx_std_17) target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") @@ -38,14 +45,14 @@ function(APPLY_STANDARD_SETTINGS TARGET) target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") endfunction() -set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") - # Flutter library and tool build rules. +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") add_subdirectory(${FLUTTER_MANAGED_DIR}) -# Application build +# Application build; see runner/CMakeLists.txt. add_subdirectory("runner") + # Generated plugin build rules, which manage building the plugins and adding # them to the application. include(flutter/generated_plugins.cmake) @@ -80,6 +87,12 @@ if(PLUGIN_BUNDLED_LIBRARIES) COMPONENT Runtime) endif() +# Copy the native assets provided by the build.dart from all packages. +set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/") +install(DIRECTORY "${NATIVE_ASSETS_DIR}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + # Fully re-copy the assets directory on each build to avoid having stale files # from a previous install. set(FLUTTER_ASSET_DIR_NAME "flutter_assets") diff --git a/auth/windows/flutter/CMakeLists.txt b/auth/windows/flutter/CMakeLists.txt index b2e4bd8d65..903f4899d6 100644 --- a/auth/windows/flutter/CMakeLists.txt +++ b/auth/windows/flutter/CMakeLists.txt @@ -1,3 +1,4 @@ +# This file controls Flutter-level build steps. It should not be edited. cmake_minimum_required(VERSION 3.14) set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") @@ -9,6 +10,11 @@ include(${EPHEMERAL_DIR}/generated_config.cmake) # https://github.com/flutter/flutter/issues/57146. set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") +# Set fallback configurations for older versions of the flutter tool. +if (NOT DEFINED FLUTTER_TARGET_PLATFORM) + set(FLUTTER_TARGET_PLATFORM "windows-x64") +endif() + # === Flutter Library === set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") @@ -91,7 +97,7 @@ add_custom_command( COMMAND ${CMAKE_COMMAND} -E env ${FLUTTER_TOOL_ENVIRONMENT} "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" - windows-x64 $ + ${FLUTTER_TARGET_PLATFORM} $ VERBATIM ) add_custom_target(flutter_assemble DEPENDS diff --git a/auth/windows/flutter/generated_plugin_registrant.cc b/auth/windows/flutter/generated_plugin_registrant.cc index 20f612aa85..4184d54853 100644 --- a/auth/windows/flutter/generated_plugin_registrant.cc +++ b/auth/windows/flutter/generated_plugin_registrant.cc @@ -6,24 +6,54 @@ #include "generated_plugin_registrant.h" +#include +#include +#include #include +#include #include #include +#include #include #include +#include +#include +#include +#include #include +#include void RegisterPlugins(flutter::PluginRegistry* registry) { + AppLinksPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("AppLinksPluginCApi")); + ConnectivityPlusWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin")); + DesktopWebviewWindowPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("DesktopWebviewWindowPlugin")); FileSaverPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("FileSaverPlugin")); + FlutterLocalAuthenticationPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FlutterLocalAuthenticationPluginCApi")); FlutterSecureStorageWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin")); LocalAuthPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("LocalAuthPlugin")); + ScreenRetrieverPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("ScreenRetrieverPlugin")); SentryFlutterPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("SentryFlutterPlugin")); SharePlusWindowsPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi")); + SmartAuthPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("SmartAuthPlugin")); + SodiumLibsPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("SodiumLibsPluginCApi")); + Sqlite3FlutterLibsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("Sqlite3FlutterLibsPlugin")); + TrayManagerPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("TrayManagerPlugin")); UrlLauncherWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("UrlLauncherWindows")); + WindowManagerPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("WindowManagerPlugin")); } diff --git a/auth/windows/flutter/generated_plugins.cmake b/auth/windows/flutter/generated_plugins.cmake index 41d3e5061d..a2c679c887 100644 --- a/auth/windows/flutter/generated_plugins.cmake +++ b/auth/windows/flutter/generated_plugins.cmake @@ -3,12 +3,22 @@ # list(APPEND FLUTTER_PLUGIN_LIST + app_links + connectivity_plus + desktop_webview_window file_saver + flutter_local_authentication flutter_secure_storage_windows local_auth_windows + screen_retriever sentry_flutter share_plus + smart_auth + sodium_libs + sqlite3_flutter_libs + tray_manager url_launcher_windows + window_manager ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/auth/windows/packaging/exe/inno_setup.iss b/auth/windows/packaging/exe/inno_setup.iss new file mode 100644 index 0000000000..5906ecbd0e --- /dev/null +++ b/auth/windows/packaging/exe/inno_setup.iss @@ -0,0 +1,64 @@ +[Setup] +AppId={{APP_ID}} +AppVersion={{APP_VERSION}} +AppName={{DISPLAY_NAME}} +AppPublisher={{PUBLISHER_NAME}} +AppPublisherURL={{PUBLISHER_URL}} +AppSupportURL={{PUBLISHER_URL}} +AppUpdatesURL={{PUBLISHER_URL}} +DefaultDirName={{INSTALL_DIR_NAME}} +DisableProgramGroupPage=yes +OutputDir=. +OutputBaseFilename={{OUTPUT_BASE_FILENAME}} +Compression=lzma +SolidCompression=yes +SetupIconFile={{SETUP_ICON_FILE}} +WizardStyle=modern +;PrivilegesRequired={{PRIVILEGES_REQUIRED}} +PrivilegesRequiredOverridesAllowed=dialog +ArchitecturesAllowed=x64 +ArchitecturesInstallIn64BitMode=x64 +UninstallDisplayIcon={app}\auth.exe + +[Languages] +{% for locale in LOCALES %} +{% if locale == 'en' %}Name: "english"; MessagesFile: "compiler:Default.isl"{% endif %} +{% if locale == 'hy' %}Name: "armenian"; MessagesFile: "compiler:Languages\\Armenian.isl"{% endif %} +{% if locale == 'bg' %}Name: "bulgarian"; MessagesFile: "compiler:Languages\\Bulgarian.isl"{% endif %} +{% if locale == 'ca' %}Name: "catalan"; MessagesFile: "compiler:Languages\\Catalan.isl"{% endif %} +{% if locale == 'zh' %}Name: "chinesesimplified"; MessagesFile: "compiler:Languages\\ChineseSimplified.isl"{% endif %} +{% if locale == 'co' %}Name: "corsican"; MessagesFile: "compiler:Languages\\Corsican.isl"{% endif %} +{% if locale == 'cs' %}Name: "czech"; MessagesFile: "compiler:Languages\\Czech.isl"{% endif %} +{% if locale == 'da' %}Name: "danish"; MessagesFile: "compiler:Languages\\Danish.isl"{% endif %} +{% if locale == 'nl' %}Name: "dutch"; MessagesFile: "compiler:Languages\\Dutch.isl"{% endif %} +{% if locale == 'fi' %}Name: "finnish"; MessagesFile: "compiler:Languages\\Finnish.isl"{% endif %} +{% if locale == 'fr' %}Name: "french"; MessagesFile: "compiler:Languages\\French.isl"{% endif %} +{% if locale == 'de' %}Name: "german"; MessagesFile: "compiler:Languages\\German.isl"{% endif %} +{% if locale == 'he' %}Name: "hebrew"; MessagesFile: "compiler:Languages\\Hebrew.isl"{% endif %} +{% if locale == 'is' %}Name: "icelandic"; MessagesFile: "compiler:Languages\\Icelandic.isl"{% endif %} +{% if locale == 'it' %}Name: "italian"; MessagesFile: "compiler:Languages\\Italian.isl"{% endif %} +{% if locale == 'ja' %}Name: "japanese"; MessagesFile: "compiler:Languages\\Japanese.isl"{% endif %} +{% if locale == 'no' %}Name: "norwegian"; MessagesFile: "compiler:Languages\\Norwegian.isl"{% endif %} +{% if locale == 'pl' %}Name: "polish"; MessagesFile: "compiler:Languages\\Polish.isl"{% endif %} +{% if locale == 'pt' %}Name: "portuguese"; MessagesFile: "compiler:Languages\\Portuguese.isl"{% endif %} +{% if locale == 'ru' %}Name: "russian"; MessagesFile: "compiler:Languages\\Russian.isl"{% endif %} +{% if locale == 'sk' %}Name: "slovak"; MessagesFile: "compiler:Languages\\Slovak.isl"{% endif %} +{% if locale == 'sl' %}Name: "slovenian"; MessagesFile: "compiler:Languages\\Slovenian.isl"{% endif %} +{% if locale == 'es' %}Name: "spanish"; MessagesFile: "compiler:Languages\\Spanish.isl"{% endif %} +{% if locale == 'tr' %}Name: "turkish"; MessagesFile: "compiler:Languages\\Turkish.isl"{% endif %} +{% if locale == 'uk' %}Name: "ukrainian"; MessagesFile: "compiler:Languages\\Ukrainian.isl"{% endif %} +{% endfor %} + +[Tasks] +Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: {% if CREATE_DESKTOP_ICON != true %}unchecked{% else %}checkedonce{% endif %} +Name: "launchAtStartup"; Description: "{cm:AutoStartProgram,{{DISPLAY_NAME}}}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: {% if LAUNCH_AT_STARTUP != true %}unchecked{% else %}checkedonce{% endif %} +[Files] +Source: "{{SOURCE_DIR}}\\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs +; NOTE: Don't use "Flags: ignoreversion" on any shared system files + +[Icons] +Name: "{autoprograms}\\{{DISPLAY_NAME}}"; Filename: "{app}\\{{EXECUTABLE_NAME}}" +Name: "{autodesktop}\\{{DISPLAY_NAME}}"; Filename: "{app}\\{{EXECUTABLE_NAME}}"; Tasks: desktopicon +Name: "{userstartup}\\{{DISPLAY_NAME}}"; Filename: "{app}\\{{EXECUTABLE_NAME}}"; WorkingDir: "{app}"; Tasks: launchAtStartup +[Run] +Filename: "{app}\\{{EXECUTABLE_NAME}}"; Description: "{cm:LaunchProgram,{{DISPLAY_NAME}}}"; Flags: {% if PRIVILEGES_REQUIRED == 'admin' %}runascurrentuser{% endif %} nowait postinstall skipifsilent diff --git a/auth/windows/packaging/exe/make_config.yaml b/auth/windows/packaging/exe/make_config.yaml new file mode 100644 index 0000000000..378d2d4d8c --- /dev/null +++ b/auth/windows/packaging/exe/make_config.yaml @@ -0,0 +1,10 @@ +app_id: 9E5F0C93-96A3-4DA9-AE52-1AA6339851FC +publisher: ente.io +publisher_url: https://github.com/ente-io/ente +display_name: Ente Auth +create_desktop_icon: false +install_dir_name: "{autopf}\\Ente Auth" +setup_icon_file: ../../assets/icons/auth-icon.ico +script_template: inno_setup.iss +locales: + - en diff --git a/auth/windows/runner/CMakeLists.txt b/auth/windows/runner/CMakeLists.txt index de2d8916b7..394917c053 100644 --- a/auth/windows/runner/CMakeLists.txt +++ b/auth/windows/runner/CMakeLists.txt @@ -1,6 +1,11 @@ cmake_minimum_required(VERSION 3.14) project(runner LANGUAGES CXX) +# Define the application target. To change its name, change BINARY_NAME in the +# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer +# work. +# +# Any new source files that you add to the application should be added here. add_executable(${BINARY_NAME} WIN32 "flutter_window.cpp" "main.cpp" @@ -10,8 +15,26 @@ add_executable(${BINARY_NAME} WIN32 "Runner.rc" "runner.exe.manifest" ) + +# Apply the standard set of build settings. This can be removed for applications +# that need different build settings. apply_standard_settings(${BINARY_NAME}) + +# Add preprocessor definitions for the build version. +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") +target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") + +# Disable Windows macros that collide with C++ standard library functions. target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") + +# Add dependency libraries and include directories. Add any application-specific +# dependencies here. target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) +target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") + +# Run the Flutter tool portions of the build. This must not be removed. add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/auth/windows/runner/Runner.rc b/auth/windows/runner/Runner.rc index 6c586f2709..398fdf68b9 100644 --- a/auth/windows/runner/Runner.rc +++ b/auth/windows/runner/Runner.rc @@ -60,14 +60,14 @@ IDI_APP_ICON ICON "resources\\app_icon.ico" // Version // -#ifdef FLUTTER_BUILD_NUMBER -#define VERSION_AS_NUMBER FLUTTER_BUILD_NUMBER +#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) +#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD #else -#define VERSION_AS_NUMBER 1,0,0 +#define VERSION_AS_NUMBER 1,0,0,0 #endif -#ifdef FLUTTER_BUILD_NAME -#define VERSION_AS_STRING #FLUTTER_BUILD_NAME +#if defined(FLUTTER_VERSION) +#define VERSION_AS_STRING FLUTTER_VERSION #else #define VERSION_AS_STRING "1.0.0" #endif @@ -90,12 +90,12 @@ BEGIN BLOCK "040904e4" BEGIN VALUE "CompanyName", "Ente Technologies, Inc." "\0" - VALUE "FileDescription", "ente Authenticator" "\0" + VALUE "FileDescription", "Ente Auth" "\0" VALUE "FileVersion", VERSION_AS_STRING "\0" - VALUE "InternalName", "ente Authenticator" "\0" - VALUE "LegalCopyright", "Copyright (C) 2022 Ente Technologies, Inc.. All rights reserved." "\0" - VALUE "OriginalFilename", "ente_auth.exe" "\0" - VALUE "ProductName", "ente Authenticator" "\0" + VALUE "InternalName", "Ente Auth" "\0" + VALUE "LegalCopyright", "Copyright (C) 2024 Ente Technologies, Inc.. All rights reserved." "\0" + VALUE "OriginalFilename", "auth.exe" "\0" + VALUE "ProductName", "Ente Auth" "\0" VALUE "ProductVersion", VERSION_AS_STRING "\0" END END diff --git a/auth/windows/runner/flutter_window.cpp b/auth/windows/runner/flutter_window.cpp index b43b9095ea..1672f9cbf7 100644 --- a/auth/windows/runner/flutter_window.cpp +++ b/auth/windows/runner/flutter_window.cpp @@ -26,6 +26,17 @@ bool FlutterWindow::OnCreate() { } RegisterPlugins(flutter_controller_->engine()); SetChildContent(flutter_controller_->view()->GetNativeWindow()); + + flutter_controller_->engine()->SetNextFrameCallback([&]() { + // [window_manager] + // this->Show(); + }); + + // Flutter can complete the first frame before the "show window" callback is + // registered. The following call ensures a frame is pending to ensure the + // window is shown. It is a no-op if the first frame hasn't completed yet. + flutter_controller_->ForceRedraw(); + return true; } diff --git a/auth/windows/runner/main.cpp b/auth/windows/runner/main.cpp index df82ae0a50..11751936ca 100644 --- a/auth/windows/runner/main.cpp +++ b/auth/windows/runner/main.cpp @@ -5,11 +5,58 @@ #include "flutter_window.h" #include "utils.h" +// [app_links] +#include "app_links/app_links_plugin_c_api.h" +bool SendAppLinkToInstance(const std::wstring &title) +{ + // Find our exact window + HWND hwnd = ::FindWindow(L"FLUTTER_RUNNER_WIN32_WINDOW", title.c_str()); + + if (hwnd) + { + // Dispatch new link to current window + SendAppLink(hwnd); + + // (Optional) Restore our window to front in same state + WINDOWPLACEMENT place = {sizeof(WINDOWPLACEMENT)}; + GetWindowPlacement(hwnd, &place); + + switch (place.showCmd) + { + case SW_SHOWMAXIMIZED: + ShowWindow(hwnd, SW_SHOWMAXIMIZED); + break; + case SW_SHOWMINIMIZED: + ShowWindow(hwnd, SW_RESTORE); + break; + default: + ShowWindow(hwnd, SW_NORMAL); + break; + } + + SetWindowPos(0, HWND_TOP, 0, 0, 0, 0, SWP_SHOWWINDOW | SWP_NOSIZE | SWP_NOMOVE); + SetForegroundWindow(hwnd); + // END Restore + + // Window has been found, don't create another one. + return true; + } + + return false; +} + int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, - _In_ wchar_t *command_line, _In_ int show_command) { + _In_ wchar_t *command_line, _In_ int show_command) +{ + // [app_links] + if (SendAppLinkToInstance(L"Ente Auth")) + { + return EXIT_SUCCESS; + } // Attach to console when present (e.g., 'flutter run') or create a // new console when running with a debugger. - if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { + if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) + { CreateAndAttachConsole(); } @@ -27,13 +74,15 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, FlutterWindow window(project); Win32Window::Point origin(10, 10); Win32Window::Size size(1280, 720); - if (!window.CreateAndShow(L"My App", origin, size)) { + if (!window.Create(L"Ente Auth", origin, size)) + { return EXIT_FAILURE; } window.SetQuitOnClose(true); ::MSG msg; - while (::GetMessage(&msg, nullptr, 0, 0)) { + while (::GetMessage(&msg, nullptr, 0, 0)) + { ::TranslateMessage(&msg); ::DispatchMessage(&msg); } diff --git a/auth/windows/runner/resource.h b/auth/windows/runner/resource.h index d7b448fc54..66a65d1e4a 100644 --- a/auth/windows/runner/resource.h +++ b/auth/windows/runner/resource.h @@ -1,4 +1,4 @@ -// +//{{NO_DEPENDENCIES}} // Microsoft Visual C++ generated include file. // Used by Runner.rc // diff --git a/auth/windows/runner/resources/app_icon.ico b/auth/windows/runner/resources/app_icon.ico index c04e20caf6..38bb22bcf8 100644 Binary files a/auth/windows/runner/resources/app_icon.ico and b/auth/windows/runner/resources/app_icon.ico differ diff --git a/auth/windows/runner/runner.exe.manifest b/auth/windows/runner/runner.exe.manifest index c977c4a425..a42ea7687c 100644 --- a/auth/windows/runner/runner.exe.manifest +++ b/auth/windows/runner/runner.exe.manifest @@ -7,7 +7,7 @@ - + diff --git a/auth/windows/runner/utils.cpp b/auth/windows/runner/utils.cpp index d19bdbbcc3..b2b08734db 100644 --- a/auth/windows/runner/utils.cpp +++ b/auth/windows/runner/utils.cpp @@ -47,16 +47,17 @@ std::string Utf8FromUtf16(const wchar_t* utf16_string) { } int target_length = ::WideCharToMultiByte( CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, - -1, nullptr, 0, nullptr, nullptr); - if (target_length == 0) { - return std::string(); - } + -1, nullptr, 0, nullptr, nullptr) + -1; // remove the trailing null character + int input_length = (int)wcslen(utf16_string); std::string utf8_string; + if (target_length <= 0 || target_length > utf8_string.max_size()) { + return utf8_string; + } utf8_string.resize(target_length); int converted_length = ::WideCharToMultiByte( CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, - -1, utf8_string.data(), - target_length, nullptr, nullptr); + input_length, utf8_string.data(), target_length, nullptr, nullptr); if (converted_length == 0) { return std::string(); } diff --git a/auth/windows/runner/win32_window.cpp b/auth/windows/runner/win32_window.cpp index c10f08dc7d..d3e9d1c60a 100644 --- a/auth/windows/runner/win32_window.cpp +++ b/auth/windows/runner/win32_window.cpp @@ -1,50 +1,76 @@ #include "win32_window.h" +#include #include #include "resource.h" -namespace { +namespace +{ -constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; +/// Window attribute that enables dark mode window decorations. +/// +/// Redefined in case the developer's machine has a Windows SDK older than +/// version 10.0.22000.0. +/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute +#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE +#define DWMWA_USE_IMMERSIVE_DARK_MODE 20 +#endif -// The number of Win32Window objects that currently exist. -static int g_active_window_count = 0; + constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; -using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); + /// Registry key for app theme preference. + /// + /// A value of 0 indicates apps should use dark mode. A non-zero or missing + /// value indicates apps should use light mode. + constexpr const wchar_t kGetPreferredBrightnessRegKey[] = + L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"; + constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme"; -// Scale helper to convert logical scaler values to physical using passed in -// scale factor -int Scale(int source, double scale_factor) { - return static_cast(source * scale_factor); -} + // The number of Win32Window objects that currently exist. + static int g_active_window_count = 0; -// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. -// This API is only needed for PerMonitor V1 awareness mode. -void EnableFullDpiSupportIfAvailable(HWND hwnd) { - HMODULE user32_module = LoadLibraryA("User32.dll"); - if (!user32_module) { - return; + using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); + + // Scale helper to convert logical scaler values to physical using passed in + // scale factor + int Scale(int source, double scale_factor) + { + return static_cast(source * scale_factor); } - auto enable_non_client_dpi_scaling = - reinterpret_cast( - GetProcAddress(user32_module, "EnableNonClientDpiScaling")); - if (enable_non_client_dpi_scaling != nullptr) { - enable_non_client_dpi_scaling(hwnd); + + // Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. + // This API is only needed for PerMonitor V1 awareness mode. + void EnableFullDpiSupportIfAvailable(HWND hwnd) + { + HMODULE user32_module = LoadLibraryA("User32.dll"); + if (!user32_module) + { + return; + } + auto enable_non_client_dpi_scaling = + reinterpret_cast( + GetProcAddress(user32_module, "EnableNonClientDpiScaling")); + if (enable_non_client_dpi_scaling != nullptr) + { + enable_non_client_dpi_scaling(hwnd); + } FreeLibrary(user32_module); } -} -} // namespace +} // namespace // Manages the Win32Window's window class registration. -class WindowClassRegistrar { - public: +class WindowClassRegistrar +{ +public: ~WindowClassRegistrar() = default; - // Returns the singleton registar instance. - static WindowClassRegistrar* GetInstance() { - if (!instance_) { + // Returns the singleton registrar instance. + static WindowClassRegistrar *GetInstance() + { + if (!instance_) + { instance_ = new WindowClassRegistrar(); } return instance_; @@ -52,24 +78,26 @@ class WindowClassRegistrar { // Returns the name of the window class, registering the class if it hasn't // previously been registered. - const wchar_t* GetWindowClass(); + const wchar_t *GetWindowClass(); // Unregisters the window class. Should only be called if there are no // instances of the window. void UnregisterWindowClass(); - private: +private: WindowClassRegistrar() = default; - static WindowClassRegistrar* instance_; + static WindowClassRegistrar *instance_; bool class_registered_ = false; }; -WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; +WindowClassRegistrar *WindowClassRegistrar::instance_ = nullptr; -const wchar_t* WindowClassRegistrar::GetWindowClass() { - if (!class_registered_) { +const wchar_t *WindowClassRegistrar::GetWindowClass() +{ + if (!class_registered_) + { WNDCLASS window_class{}; window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); window_class.lpszClassName = kWindowClassName; @@ -88,26 +116,30 @@ const wchar_t* WindowClassRegistrar::GetWindowClass() { return kWindowClassName; } -void WindowClassRegistrar::UnregisterWindowClass() { +void WindowClassRegistrar::UnregisterWindowClass() +{ UnregisterClass(kWindowClassName, nullptr); class_registered_ = false; } -Win32Window::Win32Window() { +Win32Window::Win32Window() +{ ++g_active_window_count; } -Win32Window::~Win32Window() { +Win32Window::~Win32Window() +{ --g_active_window_count; Destroy(); } -bool Win32Window::CreateAndShow(const std::wstring& title, - const Point& origin, - const Size& size) { +bool Win32Window::Create(const std::wstring &title, + const Point &origin, + const Size &size) +{ Destroy(); - const wchar_t* window_class = + const wchar_t *window_class = WindowClassRegistrar::GetInstance()->GetWindowClass(); const POINT target_point = {static_cast(origin.x), @@ -117,32 +149,45 @@ bool Win32Window::CreateAndShow(const std::wstring& title, double scale_factor = dpi / 96.0; HWND window = CreateWindow( - window_class, title.c_str(), WS_OVERLAPPEDWINDOW | WS_VISIBLE, + window_class, title.c_str(), + WS_OVERLAPPEDWINDOW, // [window_manager] do not add WS_VISIBLE since the window will be shown later Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), Scale(size.width, scale_factor), Scale(size.height, scale_factor), nullptr, nullptr, GetModuleHandle(nullptr), this); - if (!window) { + if (!window) + { return false; } + UpdateTheme(window); + return OnCreate(); } +bool Win32Window::Show() +{ + return ShowWindow(window_handle_, SW_SHOWNORMAL); +} + // static LRESULT CALLBACK Win32Window::WndProc(HWND const window, UINT const message, WPARAM const wparam, - LPARAM const lparam) noexcept { - if (message == WM_NCCREATE) { - auto window_struct = reinterpret_cast(lparam); + LPARAM const lparam) noexcept +{ + if (message == WM_NCCREATE) + { + auto window_struct = reinterpret_cast(lparam); SetWindowLongPtr(window, GWLP_USERDATA, reinterpret_cast(window_struct->lpCreateParams)); - auto that = static_cast(window_struct->lpCreateParams); + auto that = static_cast(window_struct->lpCreateParams); EnableFullDpiSupportIfAvailable(window); that->window_handle_ = window; - } else if (Win32Window* that = GetThisFromHandle(window)) { + } + else if (Win32Window *that = GetThisFromHandle(window)) + { return that->MessageHandler(window, message, wparam, lparam); } @@ -153,64 +198,80 @@ LRESULT Win32Window::MessageHandler(HWND hwnd, UINT const message, WPARAM const wparam, - LPARAM const lparam) noexcept { - switch (message) { - case WM_DESTROY: - window_handle_ = nullptr; - Destroy(); - if (quit_on_close_) { - PostQuitMessage(0); - } - return 0; - - case WM_DPICHANGED: { - auto newRectSize = reinterpret_cast(lparam); - LONG newWidth = newRectSize->right - newRectSize->left; - LONG newHeight = newRectSize->bottom - newRectSize->top; - - SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, - newHeight, SWP_NOZORDER | SWP_NOACTIVATE); - - return 0; - } - case WM_SIZE: { - RECT rect = GetClientArea(); - if (child_content_ != nullptr) { - // Size and position the child window. - MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, - rect.bottom - rect.top, TRUE); - } - return 0; + LPARAM const lparam) noexcept +{ + switch (message) + { + case WM_DESTROY: + window_handle_ = nullptr; + Destroy(); + if (quit_on_close_) + { + PostQuitMessage(0); } + return 0; - case WM_ACTIVATE: - if (child_content_ != nullptr) { - SetFocus(child_content_); - } - return 0; + case WM_DPICHANGED: + { + auto newRectSize = reinterpret_cast(lparam); + LONG newWidth = newRectSize->right - newRectSize->left; + LONG newHeight = newRectSize->bottom - newRectSize->top; + + SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, + newHeight, SWP_NOZORDER | SWP_NOACTIVATE); + + return 0; + } + case WM_SIZE: + { + RECT rect = GetClientArea(); + if (child_content_ != nullptr) + { + // Size and position the child window. + MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, + rect.bottom - rect.top, TRUE); + } + return 0; + } + + case WM_ACTIVATE: + if (child_content_ != nullptr) + { + SetFocus(child_content_); + } + return 0; + + case WM_DWMCOLORIZATIONCOLORCHANGED: + UpdateTheme(hwnd); + return 0; } return DefWindowProc(window_handle_, message, wparam, lparam); } -void Win32Window::Destroy() { +void Win32Window::Destroy() +{ OnDestroy(); - if (window_handle_) { + if (window_handle_) + { DestroyWindow(window_handle_); window_handle_ = nullptr; } - if (g_active_window_count == 0) { + if (g_active_window_count == 0) + { WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); } } -Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { - return reinterpret_cast( +Win32Window *Win32Window::GetThisFromHandle(HWND const window) noexcept +{ + return reinterpret_cast( GetWindowLongPtr(window, GWLP_USERDATA)); } -void Win32Window::SetChildContent(HWND content) { +void Win32Window::SetChildContent(HWND content) +{ child_content_ = content; SetParent(content, window_handle_); RECT frame = GetClientArea(); @@ -221,25 +282,47 @@ void Win32Window::SetChildContent(HWND content) { SetFocus(child_content_); } -RECT Win32Window::GetClientArea() { +RECT Win32Window::GetClientArea() +{ RECT frame; GetClientRect(window_handle_, &frame); return frame; } -HWND Win32Window::GetHandle() { +HWND Win32Window::GetHandle() +{ return window_handle_; } -void Win32Window::SetQuitOnClose(bool quit_on_close) { +void Win32Window::SetQuitOnClose(bool quit_on_close) +{ quit_on_close_ = quit_on_close; } -bool Win32Window::OnCreate() { +bool Win32Window::OnCreate() +{ // No-op; provided for subclasses. return true; } -void Win32Window::OnDestroy() { +void Win32Window::OnDestroy() +{ // No-op; provided for subclasses. } + +void Win32Window::UpdateTheme(HWND const window) +{ + DWORD light_mode; + DWORD light_mode_size = sizeof(light_mode); + LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey, + kGetPreferredBrightnessRegValue, + RRF_RT_REG_DWORD, nullptr, &light_mode, + &light_mode_size); + + if (result == ERROR_SUCCESS) + { + BOOL enable_dark_mode = light_mode == 0; + DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE, + &enable_dark_mode, sizeof(enable_dark_mode)); + } +} diff --git a/auth/windows/runner/win32_window.h b/auth/windows/runner/win32_window.h index 17ba431125..e3745cd060 100644 --- a/auth/windows/runner/win32_window.h +++ b/auth/windows/runner/win32_window.h @@ -10,15 +10,18 @@ // A class abstraction for a high DPI-aware Win32 Window. Intended to be // inherited from by classes that wish to specialize with custom // rendering and input handling -class Win32Window { - public: - struct Point { +class Win32Window +{ +public: + struct Point + { unsigned int x; unsigned int y; Point(unsigned int x, unsigned int y) : x(x), y(y) {} }; - struct Size { + struct Size + { unsigned int width; unsigned int height; Size(unsigned int width, unsigned int height) @@ -28,15 +31,16 @@ class Win32Window { Win32Window(); virtual ~Win32Window(); - // Creates and shows a win32 window with |title| and position and size using + // Creates a win32 window with |title| that is positioned and sized using // |origin| and |size|. New windows are created on the default monitor. Window // sizes are specified to the OS in physical pixels, hence to ensure a - // consistent size to will treat the width height passed in to this function - // as logical pixels and scale to appropriate for the default monitor. Returns - // true if the window was created successfully. - bool CreateAndShow(const std::wstring& title, - const Point& origin, - const Size& size); + // consistent size this function will scale the inputted width and height as + // as appropriate for the default monitor. The window is invisible until + // |Show| is called. Returns true if the window was created successfully. + bool Create(const std::wstring &title, const Point &origin, const Size &size); + + // Show the current window. Returns true if the window was successfully shown. + bool Show(); // Release OS resources associated with window. void Destroy(); @@ -54,7 +58,7 @@ class Win32Window { // Return a RECT representing the bounds of the current client area. RECT GetClientArea(); - protected: +protected: // Processes and route salient window messages for mouse handling, // size change and DPI. Delegates handling of these to member overloads that // inheriting classes can handle. @@ -70,13 +74,13 @@ class Win32Window { // Called when Destroy is called. virtual void OnDestroy(); - private: +private: friend class WindowClassRegistrar; // OS callback called by message pump. Handles the WM_NCCREATE message which // is passed when the non-client area is being created and enables automatic // non-client DPI scaling so that the non-client area automatically - // responsponds to changes in DPI. All other messages are handled by + // responds to changes in DPI. All other messages are handled by // MessageHandler. static LRESULT CALLBACK WndProc(HWND const window, UINT const message, @@ -84,7 +88,10 @@ class Win32Window { LPARAM const lparam) noexcept; // Retrieves a class instance pointer for |window| - static Win32Window* GetThisFromHandle(HWND const window) noexcept; + static Win32Window *GetThisFromHandle(HWND const window) noexcept; + + // Update the window frame's theme to match the system theme. + static void UpdateTheme(HWND const window); bool quit_on_close_ = false; @@ -95,4 +102,4 @@ class Win32Window { HWND child_content_ = nullptr; }; -#endif // RUNNER_WIN32_WINDOW_H_ +#endif // RUNNER_WIN32_WINDOW_H_ diff --git a/cli/internal/api/file_type.go b/cli/internal/api/file_type.go index f241f8fe58..93a23b7b0b 100644 --- a/cli/internal/api/file_type.go +++ b/cli/internal/api/file_type.go @@ -18,6 +18,10 @@ type File struct { Info *FileInfo `json:"info,omitempty"` } +func (f File) IsRemovedFromAlbum() bool { + return f.IsDeleted || f.File.EncryptedData == "-" +} + // FileInfo has information about storage used by the file & it's metadata(future) type FileInfo struct { FileSize int64 `json:"fileSize,omitempty"` diff --git a/cli/internal/crypto/crypto.go b/cli/internal/crypto/crypto.go index 83200896b3..11868c0ba6 100644 --- a/cli/internal/crypto/crypto.go +++ b/cli/internal/crypto/crypto.go @@ -98,7 +98,8 @@ func DecryptChaChaBase64(data string, key []byte, nonce string) (string, []byte, // Decode data from base64 dataBytes, err := base64.StdEncoding.DecodeString(data) if err != nil { - return "", nil, fmt.Errorf("invalid data: %v", err) + // safe to log the encrypted data + return "", nil, fmt.Errorf("invalid base64 data %s: %v", data, err) } // Decode nonce from base64 nonceBytes, err := base64.StdEncoding.DecodeString(nonce) diff --git a/cli/main.go b/cli/main.go index 157c11fd87..d62cdcffad 100644 --- a/cli/main.go +++ b/cli/main.go @@ -15,7 +15,7 @@ import ( "strings" ) -var AppVersion = "0.1.12" +var AppVersion = "0.1.13" func main() { cliDBPath, err := GetCLIConfigPath() diff --git a/cli/pkg/mapper/photo.go b/cli/pkg/mapper/photo.go index fa55ffae51..ac16f44f73 100644 --- a/cli/pkg/mapper/photo.go +++ b/cli/pkg/mapper/photo.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "errors" + "fmt" "github.com/ente-io/cli/internal/api" eCrypto "github.com/ente-io/cli/internal/crypto" "github.com/ente-io/cli/pkg/model" @@ -41,7 +42,7 @@ func MapCollectionToAlbum(ctx context.Context, collection api.Collection, holder if collection.MagicMetadata != nil { _, encodedJsonBytes, err := eCrypto.DecryptChaChaBase64(collection.MagicMetadata.Data, collectionKey, collection.MagicMetadata.Header) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to decrypt magic metadata for collection %d: %w", collection.ID, err) } err = json.Unmarshal(encodedJsonBytes, &album.PrivateMeta) if err != nil { @@ -51,28 +52,28 @@ func MapCollectionToAlbum(ctx context.Context, collection api.Collection, holder if collection.PublicMagicMetadata != nil { _, encodedJsonBytes, err := eCrypto.DecryptChaChaBase64(collection.PublicMagicMetadata.Data, collectionKey, collection.PublicMagicMetadata.Header) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to decrypt public magic metadata for collection %d: %w", collection.ID, err) } err = json.Unmarshal(encodedJsonBytes, &album.PublicMeta) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to unmarshal public magic metadata for collection %d: %w", collection.ID, err) } } if album.IsShared && collection.SharedMagicMetadata != nil { _, encodedJsonBytes, err := eCrypto.DecryptChaChaBase64(collection.SharedMagicMetadata.Data, collectionKey, collection.SharedMagicMetadata.Header) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to decrypt shared magic metadata for collection %d: %w", collection.ID, err) } err = json.Unmarshal(encodedJsonBytes, &album.SharedMeta) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to unmarshal shared magic metadata for collection %d: %w", collection.ID, err) } } return &album, nil } func MapApiFileToPhotoFile(ctx context.Context, album model.RemoteAlbum, file api.File, holder *secrets.KeyHolder) (*model.RemoteFile, error) { - if file.IsDeleted { + if file.IsRemovedFromAlbum() { return nil, errors.New("file is deleted") } albumKey := album.AlbumKey.MustDecrypt(holder.DeviceKey) @@ -99,7 +100,7 @@ func MapApiFileToPhotoFile(ctx context.Context, album model.RemoteAlbum, file ap if file.Metadata.DecryptionHeader != "" { _, encodedJsonBytes, err := eCrypto.DecryptChaChaBase64(file.Metadata.EncryptedData, fileKey, file.Metadata.DecryptionHeader) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to decrypt metadata for file %d: %w", file.ID, err) } err = json.Unmarshal(encodedJsonBytes, &photoFile.Metadata) if err != nil { @@ -109,7 +110,7 @@ func MapApiFileToPhotoFile(ctx context.Context, album model.RemoteAlbum, file ap if file.MagicMetadata != nil { _, encodedJsonBytes, err := eCrypto.DecryptChaChaBase64(file.MagicMetadata.Data, fileKey, file.MagicMetadata.Header) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to decrypt magic metadata for file %d: %w", file.ID, err) } err = json.Unmarshal(encodedJsonBytes, &photoFile.PrivateMetadata) if err != nil { @@ -119,7 +120,7 @@ func MapApiFileToPhotoFile(ctx context.Context, album model.RemoteAlbum, file ap if file.PubicMagicMetadata != nil { _, encodedJsonBytes, err := eCrypto.DecryptChaChaBase64(file.PubicMagicMetadata.Data, fileKey, file.PubicMagicMetadata.Header) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to decrypt public magic metadata for file %d: %w", file.ID, err) } err = json.Unmarshal(encodedJsonBytes, &photoFile.PublicMetadata) if err != nil { diff --git a/cli/pkg/remote_sync.go b/cli/pkg/remote_sync.go index a37a4a58cc..5ca149d719 100644 --- a/cli/pkg/remote_sync.go +++ b/cli/pkg/remote_sync.go @@ -87,16 +87,16 @@ func (c *ClICtrl) fetchRemoteFiles(ctx context.Context) error { if file.UpdationTime > maxUpdated { maxUpdated = file.UpdationTime } - if isFirstSync && file.IsDeleted { + if isFirstSync && file.IsRemovedFromAlbum() { // on first sync, no need to sync delete markers continue } - albumEntry := model.AlbumFileEntry{AlbumID: album.ID, FileID: file.ID, IsDeleted: file.IsDeleted, SyncedLocally: false} + albumEntry := model.AlbumFileEntry{AlbumID: album.ID, FileID: file.ID, IsDeleted: file.IsRemovedFromAlbum(), SyncedLocally: false} putErr := c.UpsertAlbumEntry(ctx, &albumEntry) if putErr != nil { return putErr } - if file.IsDeleted { + if file.IsRemovedFromAlbum() { continue } photoFile, err := mapper.MapApiFileToPhotoFile(ctx, album, file, c.KeyHolder) diff --git a/desktop/README.md b/desktop/README.md index 2955915841..05149f5d0c 100644 --- a/desktop/README.md +++ b/desktop/README.md @@ -10,6 +10,12 @@ 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 @@ -22,17 +28,12 @@ Install dependencies yarn install ``` -Run in development mode (with hot reload) +Run in development mode (supports hot reload for the renderer process) ```sh yarn dev ``` -> [!CAUTION] -> -> `yarn dev` is currently not working (we'll fix soon). If you just want to -> build from source and use the generated binary, use `yarn build`. - Or create a binary for your platform ```sh diff --git a/desktop/build/icon.icns b/desktop/build/icon.icns deleted file mode 100644 index ab7eface7a..0000000000 Binary files a/desktop/build/icon.icns and /dev/null differ diff --git a/desktop/build/splash.html b/desktop/build/splash.html deleted file mode 100644 index 199c316010..0000000000 --- a/desktop/build/splash.html +++ /dev/null @@ -1,50 +0,0 @@ - - - - - - - ente Photos - - - -
-
- - - - - -
-
- - diff --git a/desktop/build/version.html b/desktop/build/version.html deleted file mode 100644 index b2038edd7a..0000000000 --- a/desktop/build/version.html +++ /dev/null @@ -1,24 +0,0 @@ - - - - Electron Updater Example - - - Current version: vX.Y.Z -
- - - diff --git a/desktop/build/window-icon.png b/desktop/build/window-icon.png deleted file mode 100644 index 5b0458033d..0000000000 Binary files a/desktop/build/window-icon.png and /dev/null differ diff --git a/desktop/docs/dependencies.md b/desktop/docs/dependencies.md index 502ea6ea9d..cf38fe1212 100644 --- a/desktop/docs/dependencies.md +++ b/desktop/docs/dependencies.md @@ -27,7 +27,7 @@ There is also a third environment that gets temporarily created: - The [preload script](../src/preload.ts) acts as a gateway between the _main_ and the _renderer_ process. It runs in its own isolated environment. -### electron-builder +### Packaging [Electron Builder](https://www.electron.build) is used for packaging the app for distribution. @@ -36,22 +36,44 @@ During the build it uses [electron-builder-notarize](https://github.com/karaggeorge/electron-builder-notarize) to notarize the macOS binary. +### Updates + +[electron-updater](https://www.electron.build/auto-update#debugging), while a +separate package, is also a part of Electron Builder. It provides an alternative +to Electron's built in auto updater, with a more flexible API. It supports auto +updates for the DMG, AppImage, DEB, RPM and NSIS packages. + +[compare-versions](https://github.com/omichelsen/compare-versions) is used for +semver comparisons when we decide when to trigger updates. + +### Logging + +[electron-log](https://github.com/megahertz/electron-log) is used for logging. +Specifically, it allows us to log to a file (in addition to the console of the +Node.js process), and also handles log rotation and limiting the size of the log +files. + ### next-electron-server This spins up a server for serving files using a protocol handler inside our Electron process. This allows us to directly use the output produced by `next build` for loading into our renderer process. -### electron-reload +### Others -Reloads contents of the BrowserWindow (renderer process) when source files are -changed. +* [any-shell-escape](https://github.com/boazy/any-shell-escape) is for + escaping shell commands before we execute them (e.g. say when invoking the + embedded ffmpeg CLI). -* TODO (MR): Do we need this? Isn't the next-electron-server HMR covering this? +* [auto-launch](https://github.com/Teamwork/node-auto-launch) is for + automatically starting our app on login, if the user so wishes. -## DX +* [electron-store](https://github.com/sindresorhus/electron-store) is used for + persisting user preferences and other arbitrary data. -See [web/docs/dependencies#DX](../../web/docs/dependencies.md#dx) for the +## Dev + +See [web/docs/dependencies#DX](../../web/docs/dependencies.md#dev) for the general development experience related dependencies like TypeScript etc, which are similar to that in the web code. @@ -59,3 +81,43 @@ Some extra ones specific to the code here are: * [concurrently](https://github.com/open-cli-tools/concurrently) for spawning parallel tasks when we do `yarn dev`. + +* [shx](https://github.com/shelljs/shx) for providing a portable way to use Unix + commands in our `package.json` scripts. This allows us to use the same + commands (like `ln`) across different platforms like Linux and Windows. + +## Functionality + +### Conversion + +The main tool we use is for arbitrary conversions is FFMPEG. To bundle a +(platform specific) static binary of ffmpeg with our app, we use +[ffmpeg-static](https://github.com/eugeneware/ffmpeg-static). + +> There is a significant (~20x) speed difference between using the compiled +> FFMPEG binary and using the WASM one (that our renderer process already has). +> Which is why we bundle it to speed up operations on the desktop app. + +In addition, we also bundle a static Linux binary of imagemagick in our extra +resources (`build`) folder. This is used for thumbnail generation on Linux. + +On macOS, we use the `sips` CLI tool for conversion, but that is already +available on the host machine, and is not bundled with our app. + +### Watch Folders + +[chokidar](https://github.com/paulmillr/chokidar) is used as a file system +watcher for the watch folders functionality. + +### AI/ML + +* [onnxruntime-node](https://github.com/Microsoft/onnxruntime) +* html-entities is used by the bundled clip-bpe-ts. +* GGML binaries are bundled +* We also use [jpeg-js](https://github.com/jpeg-js/jpeg-js#readme) for + conversion of all images to JPEG before processing. + +## ZIP + +[node-stream-zip](https://github.com/antelle/node-stream-zip) is used for +reading of large ZIP files (e.g. during imports of Google Takeout ZIPs). diff --git a/desktop/docs/dev.md b/desktop/docs/dev.md index a175f9b811..507438fdce 100644 --- a/desktop/docs/dev.md +++ b/desktop/docs/dev.md @@ -4,14 +4,11 @@ ### yarn dev -Launch the app in development mode +Launch the app in development mode: -- Runs a development server for the renderer, with HMR. +- Transpiles the files in `src/` and starts the main process. -- Starts tsc in watch mode to recompile the JS files used by the main process. - -- Starts the main process, reloading it on changes to the the TS files in - `src/`. +- Runs a development server for the renderer, with hot module reload. ### yarn build @@ -34,7 +31,7 @@ are built against `electron`'s packaged `node` version. We use to rebuild those modules automatically after each `yarn install` by invoking it in as the `postinstall` step in our package.json. -### lint and lint-fix +### lint, lint-fix Use `yarn lint` to check that your code formatting is as expected, and that there are no linter errors. Use `yarn lint-fix` to try and automatically fix the diff --git a/desktop/electron-builder.yml b/desktop/electron-builder.yml index fb147b5726..9189c34355 100644 --- a/desktop/electron-builder.yml +++ b/desktop/electron-builder.yml @@ -5,36 +5,22 @@ nsis: linux: target: - target: AppImage - arch: - - x64 - - arm64 + arch: [x64, arm64] - target: deb - arch: - - x64 - - arm64 + arch: [x64, arm64] - target: rpm - arch: - - x64 - - arm64 + arch: [x64, arm64] - target: pacman - arch: - - x64 - - arm64 - icon: ./resources/icon.icns + arch: [x64, arm64] category: Photography mac: target: target: default - arch: - - universal + arch: [universal] category: public.app-category.photography hardenedRuntime: true x64ArchFiles: Contents/Resources/ggmlclip-mac afterSign: electron-builder-notarize -asarUnpack: - - node_modules/ffmpeg-static/bin/${os}/${arch}/ffmpeg - - node_modules/ffmpeg-static/index.js - - node_modules/ffmpeg-static/package.json extraFiles: - from: build to: resources diff --git a/desktop/package.json b/desktop/package.json index 3dafdce33b..16ba23eb96 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -9,51 +9,44 @@ "build": "yarn build-renderer && yarn build-main", "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 && rm -f out && ln -sf ../web/apps/photos/out", + "build-renderer": "cd ../web && yarn install && yarn build:photos && cd ../desktop && shx rm -f out && shx ln -sf ../web/apps/photos/out out", "build:quick": "yarn build-renderer && yarn build-main:quick", - "dev": "concurrently --names 'main,rndr,tscw' \"yarn dev-main\" \"yarn dev-renderer\" \"yarn dev-main-watch\"", + "dev": "concurrently --names 'main,rndr' \"yarn dev-main\" \"yarn dev-renderer\"", "dev-main": "tsc && electron app/main.js", - "dev-main-watch": "tsc --watch --preserveWatchOutput", "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" }, "dependencies": { - "any-shell-escape": "^0.1.1", - "auto-launch": "^5.0.5", - "chokidar": "^3.5.3", - "compare-versions": "^6.1.0", - "electron-log": "^4.3.5", - "electron-reload": "^2.0.0-alpha.1", - "electron-store": "^8.0.1", - "electron-updater": "^4.3.8", - "ffmpeg-static": "^5.1.0", - "get-folder-size": "^2.0.1", - "html-entities": "^2.4.0", - "jpeg-js": "^0.4.4", + "any-shell-escape": "^0.1", + "auto-launch": "^5.0", + "chokidar": "^3.6", + "compare-versions": "^6.1", + "electron-log": "^5.1", + "electron-store": "^8.2", + "electron-updater": "^6.1", + "ffmpeg-static": "^5.2", + "html-entities": "^2.5", + "jpeg-js": "^0.4", "next-electron-server": "^1", - "node-fetch": "^2.6.7", - "node-stream-zip": "^1.15.0", - "onnxruntime-node": "^1.16.3", - "promise-fs": "^2.1.1" + "node-stream-zip": "^1.15", + "onnxruntime-node": "^1.17" }, "devDependencies": { - "@types/auto-launch": "^5.0.2", - "@types/ffmpeg-static": "^3.0.1", - "@types/get-folder-size": "^2.0.0", - "@types/node-fetch": "^2.6.2", - "@types/promise-fs": "^2.1.1", + "@types/auto-launch": "^5.0", + "@types/ffmpeg-static": "^3.0", "@typescript-eslint/eslint-plugin": "^7", "@typescript-eslint/parser": "^7", "concurrently": "^8", - "electron": "^25.8.4", - "electron-builder": "^24.6.4", - "electron-builder-notarize": "^1.2.0", + "electron": "^29", + "electron-builder": "^24", + "electron-builder-notarize": "^1.5", "eslint": "^8", "prettier": "^3", "prettier-plugin-organize-imports": "^3.2", "prettier-plugin-packagejson": "^2.4", + "shx": "^0.3", "typescript": "^5" }, "productName": "ente" diff --git a/desktop/src/api/cache.ts b/desktop/src/api/cache.ts deleted file mode 100644 index bf7182fad2..0000000000 --- a/desktop/src/api/cache.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { ipcRenderer } from "electron/renderer"; -import path from "path"; -import { existsSync, mkdir, rmSync } from "promise-fs"; -import { DiskCache } from "../services/diskCache"; - -const ENTE_CACHE_DIR_NAME = "ente"; - -const getCacheDirectory = async () => { - const defaultSystemCacheDir = await ipcRenderer.invoke("get-path", "cache"); - return path.join(defaultSystemCacheDir, ENTE_CACHE_DIR_NAME); -}; - -const getCacheBucketDir = async (cacheName: string) => { - const cacheDir = await getCacheDirectory(); - const cacheBucketDir = path.join(cacheDir, cacheName); - return cacheBucketDir; -}; - -export async function openDiskCache( - cacheName: string, - cacheLimitInBytes?: number, -) { - const cacheBucketDir = await getCacheBucketDir(cacheName); - if (!existsSync(cacheBucketDir)) { - await mkdir(cacheBucketDir, { recursive: true }); - } - return new DiskCache(cacheBucketDir, cacheLimitInBytes); -} - -export async function deleteDiskCache(cacheName: string) { - const cacheBucketDir = await getCacheBucketDir(cacheName); - if (existsSync(cacheBucketDir)) { - rmSync(cacheBucketDir, { recursive: true, force: true }); - return true; - } else { - return false; - } -} diff --git a/desktop/src/api/clip.ts b/desktop/src/api/clip.ts deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/desktop/src/api/common.ts b/desktop/src/api/common.ts deleted file mode 100644 index 64a1bbdfff..0000000000 --- a/desktop/src/api/common.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { ipcRenderer } from "electron/renderer"; -import { logError } from "../services/logging"; - - -export { logToDisk, openLogDirectory } from "../services/logging"; diff --git a/desktop/src/api/electronStore.ts b/desktop/src/api/electronStore.ts index 5f84776e1a..2ee74953df 100644 --- a/desktop/src/api/electronStore.ts +++ b/desktop/src/api/electronStore.ts @@ -1,4 +1,4 @@ -import { logError } from "../services/logging"; +import { logError } from "../main/log"; import { keysStore } from "../stores/keys.store"; import { safeStorageStore } from "../stores/safeStorage.store"; import { uploadStatusStore } from "../stores/upload.store"; diff --git a/desktop/src/api/export.ts b/desktop/src/api/export.ts deleted file mode 100644 index 8adaa236fb..0000000000 --- a/desktop/src/api/export.ts +++ /dev/null @@ -1,23 +0,0 @@ -import * as fs from "promise-fs"; -import { writeStream } from "./../services/fs"; - -export const exists = (path: string) => { - return fs.existsSync(path); -}; - -export const checkExistsAndCreateDir = async (dirPath: string) => { - if (!fs.existsSync(dirPath)) { - await fs.mkdir(dirPath); - } -}; - -export const saveStreamToDisk = async ( - filePath: string, - fileStream: ReadableStream, -) => { - await writeStream(filePath, fileStream); -}; - -export const saveFileToDisk = async (path: string, fileData: string) => { - await fs.writeFile(path, fileData); -}; diff --git a/desktop/src/api/ffmpeg.ts b/desktop/src/api/ffmpeg.ts deleted file mode 100644 index 9d11183a8c..0000000000 --- a/desktop/src/api/ffmpeg.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { ipcRenderer } from "electron"; -import { existsSync } from "fs"; -import { writeStream } from "../services/fs"; -import { logError } from "../services/logging"; -import { ElectronFile } from "../types"; - -export async function runFFmpegCmd( - cmd: string[], - inputFile: File | ElectronFile, - outputFileName: string, - dontTimeout?: boolean, -) { - let inputFilePath = null; - let createdTempInputFile = null; - try { - if (!existsSync(inputFile.path)) { - const tempFilePath = await ipcRenderer.invoke( - "get-temp-file-path", - inputFile.name, - ); - await writeStream(tempFilePath, await inputFile.stream()); - inputFilePath = tempFilePath; - createdTempInputFile = true; - } else { - inputFilePath = inputFile.path; - } - const outputFileData = await ipcRenderer.invoke( - "run-ffmpeg-cmd", - cmd, - inputFilePath, - outputFileName, - dontTimeout, - ); - return new File([outputFileData], outputFileName); - } finally { - if (createdTempInputFile) { - try { - await ipcRenderer.invoke("remove-temp-file", inputFilePath); - } catch (e) { - logError(e, "failed to deleteTempFile"); - } - } - } -} diff --git a/desktop/src/api/fs.ts b/desktop/src/api/fs.ts deleted file mode 100644 index 5acc5fd48a..0000000000 --- a/desktop/src/api/fs.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { getDirFilePaths, getElectronFile } from "../services/fs"; - -export async function getDirFiles(dirPath: string) { - const files = await getDirFilePaths(dirPath); - const electronFiles = await Promise.all(files.map(getElectronFile)); - return electronFiles; -} diff --git a/desktop/src/api/imageProcessor.ts b/desktop/src/api/imageProcessor.ts deleted file mode 100644 index 9d93aecd1b..0000000000 --- a/desktop/src/api/imageProcessor.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { ipcRenderer } from "electron/renderer"; -import { existsSync } from "fs"; -import { CustomErrors } from "../constants/errors"; -import { writeStream } from "../services/fs"; -import { logError } from "../services/logging"; -import { ElectronFile } from "../types"; -import { isPlatform } from "../utils/common/platform"; - -export async function convertToJPEG( - fileData: Uint8Array, - filename: string, -): Promise { - if (isPlatform("windows")) { - throw Error(CustomErrors.WINDOWS_NATIVE_IMAGE_PROCESSING_NOT_SUPPORTED); - } - const convertedFileData = await ipcRenderer.invoke( - "convert-to-jpeg", - fileData, - filename, - ); - return convertedFileData; -} - -export async function generateImageThumbnail( - inputFile: File | ElectronFile, - maxDimension: number, - maxSize: number, -): Promise { - let inputFilePath = null; - let createdTempInputFile = null; - try { - if (isPlatform("windows")) { - throw Error( - CustomErrors.WINDOWS_NATIVE_IMAGE_PROCESSING_NOT_SUPPORTED, - ); - } - if (!existsSync(inputFile.path)) { - const tempFilePath = await ipcRenderer.invoke( - "get-temp-file-path", - inputFile.name, - ); - await writeStream(tempFilePath, await inputFile.stream()); - inputFilePath = tempFilePath; - createdTempInputFile = true; - } else { - inputFilePath = inputFile.path; - } - const thumbnail = await ipcRenderer.invoke( - "generate-image-thumbnail", - inputFilePath, - maxDimension, - maxSize, - ); - return thumbnail; - } finally { - if (createdTempInputFile) { - try { - await ipcRenderer.invoke("remove-temp-file", inputFilePath); - } catch (e) { - logError(e, "failed to deleteTempFile"); - } - } - } -} diff --git a/desktop/src/api/safeStorage.ts b/desktop/src/api/safeStorage.ts index 64c4891950..e3d6749515 100644 --- a/desktop/src/api/safeStorage.ts +++ b/desktop/src/api/safeStorage.ts @@ -1,13 +1,11 @@ -import { ipcRenderer } from "electron"; -import { logError } from "../services/logging"; +import { safeStorage } from "electron/main"; +import { logError } from "../main/log"; import { safeStorageStore } from "../stores/safeStorage.store"; export async function setEncryptionKey(encryptionKey: string) { try { - const encryptedKey: Buffer = await ipcRenderer.invoke( - "safeStorage-encrypt", - encryptionKey, - ); + const encryptedKey: Buffer = + await safeStorage.encryptString(encryptionKey); const b64EncryptedKey = Buffer.from(encryptedKey).toString("base64"); safeStorageStore.set("encryptionKey", b64EncryptedKey); } catch (e) { @@ -20,10 +18,8 @@ export async function getEncryptionKey(): Promise { try { const b64EncryptedKey = safeStorageStore.get("encryptionKey"); if (b64EncryptedKey) { - const keyBuffer = new Uint8Array( - Buffer.from(b64EncryptedKey, "base64"), - ); - return await ipcRenderer.invoke("safeStorage-decrypt", keyBuffer); + const keyBuffer = Buffer.from(b64EncryptedKey, "base64"); + return await safeStorage.decryptString(keyBuffer); } } catch (e) { logError(e, "getEncryptionKey failed"); diff --git a/desktop/src/api/system.ts b/desktop/src/api/system.ts deleted file mode 100644 index 422df34ae5..0000000000 --- a/desktop/src/api/system.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { ipcRenderer } from "electron"; -import { AppUpdateInfo } from "../types"; - -export const registerUpdateEventListener = ( - showUpdateDialog: (updateInfo: AppUpdateInfo) => void, -) => { - ipcRenderer.removeAllListeners("show-update-dialog"); - ipcRenderer.on("show-update-dialog", (_, updateInfo: AppUpdateInfo) => { - showUpdateDialog(updateInfo); - }); -}; - -export const registerForegroundEventListener = (onForeground: () => void) => { - ipcRenderer.removeAllListeners("app-in-foreground"); - ipcRenderer.on("app-in-foreground", () => { - onForeground(); - }); -}; diff --git a/desktop/src/api/upload.ts b/desktop/src/api/upload.ts index 280ff084ff..24d0283ffd 100644 --- a/desktop/src/api/upload.ts +++ b/desktop/src/api/upload.ts @@ -1,12 +1,10 @@ -import { ipcRenderer } from "electron"; -import { logError } from "../services/logging"; +import { getElectronFile } from "../services/fs"; import { getElectronFilesFromGoogleZip, getSavedFilePaths, } from "../services/upload"; import { uploadStatusStore } from "../stores/upload.store"; -import { ElectronFile, FILE_PATH_TYPE } from "../types"; -import { getElectronFile } from "./../services/fs"; +import { ElectronFile, FILE_PATH_TYPE } from "../types/ipc"; export const getPendingUploads = async () => { const filePaths = getSavedFilePaths(FILE_PATH_TYPE.FILES); @@ -36,53 +34,6 @@ export const getPendingUploads = async () => { }; }; -export const showUploadDirsDialog = async () => { - try { - const filePaths: string[] = await ipcRenderer.invoke( - "show-upload-dirs-dialog", - ); - const files = await Promise.all(filePaths.map(getElectronFile)); - return files; - } catch (e) { - logError(e, "error while selecting folders"); - } -}; - -export const showUploadFilesDialog = async () => { - try { - const filePaths: string[] = await ipcRenderer.invoke( - "show-upload-files-dialog", - ); - const files = await Promise.all(filePaths.map(getElectronFile)); - return files; - } catch (e) { - logError(e, "error while selecting files"); - } -}; - -export const showUploadZipDialog = async () => { - try { - const filePaths: string[] = await ipcRenderer.invoke( - "show-upload-zip-dialog", - ); - let files: ElectronFile[] = []; - - for (const filePath of filePaths) { - files = [ - ...files, - ...(await getElectronFilesFromGoogleZip(filePath)), - ]; - } - - return { - zipPaths: filePaths, - files, - }; - } catch (e) { - logError(e, "error while selecting zips"); - } -}; - export { getElectronFilesFromGoogleZip, setToUploadCollection, diff --git a/desktop/src/api/watch.ts b/desktop/src/api/watch.ts deleted file mode 100644 index 1b7a4ac3cf..0000000000 --- a/desktop/src/api/watch.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { ipcRenderer } from "electron"; -import ElectronLog from "electron-log"; -import path from "path"; -import { getElectronFile } from "../services/fs"; -import { getWatchMappings, setWatchMappings } from "../services/watch"; -import { ElectronFile, WatchMapping } from "../types"; -import { isMappingPresent } from "../utils/watch"; - -export async function addWatchMapping( - rootFolderName: string, - folderPath: string, - uploadStrategy: number, -) { - ElectronLog.log(`Adding watch mapping: ${folderPath}`); - const watchMappings = getWatchMappings(); - if (isMappingPresent(watchMappings, folderPath)) { - throw new Error(`Watch mapping already exists`); - } - - await ipcRenderer.invoke("add-watcher", { - dir: folderPath, - }); - - watchMappings.push({ - rootFolderName, - uploadStrategy, - folderPath, - syncedFiles: [], - ignoredFiles: [], - }); - - setWatchMappings(watchMappings); -} - -export async function removeWatchMapping(folderPath: string) { - let watchMappings = getWatchMappings(); - const watchMapping = watchMappings.find( - (mapping) => mapping.folderPath === folderPath, - ); - - if (!watchMapping) { - throw new Error(`Watch mapping does not exist`); - } - - await ipcRenderer.invoke("remove-watcher", { - dir: watchMapping.folderPath, - }); - - watchMappings = watchMappings.filter( - (mapping) => mapping.folderPath !== watchMapping.folderPath, - ); - - setWatchMappings(watchMappings); -} - -export function updateWatchMappingSyncedFiles( - folderPath: string, - files: WatchMapping["syncedFiles"], -): void { - const watchMappings = getWatchMappings(); - const watchMapping = watchMappings.find( - (mapping) => mapping.folderPath === folderPath, - ); - - if (!watchMapping) { - throw Error(`Watch mapping not found`); - } - - watchMapping.syncedFiles = files; - setWatchMappings(watchMappings); -} - -export function updateWatchMappingIgnoredFiles( - folderPath: string, - files: WatchMapping["ignoredFiles"], -): void { - const watchMappings = getWatchMappings(); - const watchMapping = watchMappings.find( - (mapping) => mapping.folderPath === folderPath, - ); - - if (!watchMapping) { - throw Error(`Watch mapping not found`); - } - - watchMapping.ignoredFiles = files; - setWatchMappings(watchMappings); -} - -export function registerWatcherFunctions( - addFile: (file: ElectronFile) => Promise, - removeFile: (path: string) => Promise, - removeFolder: (folderPath: string) => Promise, -) { - ipcRenderer.removeAllListeners("watch-add"); - ipcRenderer.removeAllListeners("watch-change"); - ipcRenderer.removeAllListeners("watch-unlink-dir"); - ipcRenderer.on("watch-add", async (_, filePath: string) => { - filePath = filePath.split(path.sep).join(path.posix.sep); - - await addFile(await getElectronFile(filePath)); - }); - ipcRenderer.on("watch-unlink", async (_, filePath: string) => { - filePath = filePath.split(path.sep).join(path.posix.sep); - - await removeFile(filePath); - }); - ipcRenderer.on("watch-unlink-dir", async (_, folderPath: string) => { - folderPath = folderPath.split(path.sep).join(path.posix.sep); - await removeFolder(folderPath); - }); -} - -export { getWatchMappings } from "../services/watch"; diff --git a/desktop/src/constants/errors.ts b/desktop/src/constants/errors.ts index 97aef616ce..e6225ecf65 100644 --- a/desktop/src/constants/errors.ts +++ b/desktop/src/constants/errors.ts @@ -1,3 +1,17 @@ +/** + * [Note: Custom errors across Electron/Renderer boundary] + * + * We need to use the `message` field to disambiguate between errors thrown by + * the main process when invoked from the renderer process. This is because: + * + * > Errors thrown throw `handle` in the main process are not transparent as + * > they are serialized and only the `message` property from the original error + * > is provided to the renderer process. + * > + * > - https://www.electronjs.org/docs/latest/tutorial/ipc + * > + * > Ref: https://github.com/electron/electron/issues/24427 + */ export const CustomErrors = { WINDOWS_NATIVE_IMAGE_PROCESSING_NOT_SUPPORTED: "Windows native image processing is not supported", diff --git a/desktop/src/main.ts b/desktop/src/main.ts index 4de93046a3..4383fa73f6 100644 --- a/desktop/src/main.ts +++ b/desktop/src/main.ts @@ -1,26 +1,34 @@ -import { app, BrowserWindow } from "electron"; -import electronReload from "electron-reload"; +/** + * @file Entry point for the main (Node.js) process of our Electron app. + * + * The code in this file is invoked by Electron when our app starts - + * Conceptually (after all the transpilation etc has happened) this can be + * thought of `electron main.ts`. We're running in the context of the so called + * "main" process which runs in a Node.js environment. + * + * https://www.electronjs.org/docs/latest/tutorial/process-model#the-main-process + */ +import { app, BrowserWindow, Menu } from "electron/main"; import serveNextAt from "next-electron-server"; -import { initWatcher } from "./services/chokidar"; -import { isDev } from "./utils/common"; -import { addAllowOriginHeader } from "./utils/cors"; -import { createWindow } from "./utils/createWindow"; -import { setupAppEventEmitter } from "./utils/events"; -import setupIpcComs from "./utils/ipcComms"; -import { setupLogging } from "./utils/logging"; +import { existsSync } from "node:fs"; +import fs from "node:fs/promises"; +import path from "node:path"; import { - enableSharedArrayBufferSupport, + addAllowOriginHeader, + createWindow, handleDockIconHideOnAutoLaunch, handleDownloads, handleExternalLinks, - handleUpdates, - logSystemInfo, + logStartupBanner, setupMacWindowOnDockIconClick, - setupMainMenu, setupTrayItem, -} from "./utils/main"; - -let mainWindow: BrowserWindow; +} from "./main/init"; +import { attachFSWatchIPCHandlers, attachIPCHandlers } from "./main/ipc"; +import log, { initLogging } from "./main/log"; +import { createApplicationMenu } from "./main/menu"; +import { isDev } from "./main/util"; +import { setupAutoUpdater } from "./services/appUpdater"; +import { initWatcher } from "./services/chokidar"; let appIsQuitting = false; @@ -42,19 +50,6 @@ export const setIsUpdateAvailable = (value: boolean): void => { updateIsAvailable = value; }; -/** - * Hot reload the main process if anything changes in the source directory that - * we're running from. - * - * In particular, this gets triggered when the `tsc -w` rebuilds JS files in the - * `app/` directory when we change the TS files in the `src/` directory. - */ -const setupMainHotReload = () => { - if (isDev) { - electronReload(__dirname, {}); - } -}; - /** * The URL where the renderer HTML is being served from. */ @@ -77,16 +72,82 @@ const setupRendererServer = () => { serveNextAt(rendererURL); }; -setupMainHotReload(); -setupRendererServer(); -setupLogging(isDev); +function enableSharedArrayBufferSupport() { + app.commandLine.appendSwitch("enable-features", "SharedArrayBuffer"); +} -const gotTheLock = app.requestSingleInstanceLock(); -if (!gotTheLock) { - app.quit(); -} else { +/** + * [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 same web-native caching mechanism 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. We set it to a large value, 5 GB (5 * 1024 * + * 1024 * 1024 = 5368709120) + * 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 + */ +const increaseDiskCache = () => { + app.commandLine.appendSwitch("disk-cache-size", "5368709120"); +}; + +/** + * 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. + * + * See [Note: Increased disk cache for the desktop app] + * + * 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. + */ +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`. + // + // It might be an issue in the types, since at runtime it seems to work. For + // example, on macOS I get `~/Library/Caches`. + // + // Irrespective, we replicate the original behaviour so that we get back the + // same path that the old got was getting. + // + // @ts-expect-error + 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 }); + } +}; + +function setupAppEventEmitter(mainWindow: BrowserWindow) { + // fire event when mainWindow is in foreground + mainWindow.on("focus", () => { + mainWindow.webContents.send("app-in-foreground"); + }); +} + +const main = () => { + const gotTheLock = app.requestSingleInstanceLock(); + if (!gotTheLock) { + app.quit(); + return; + } + + let mainWindow: BrowserWindow; + + initLogging(); + setupRendererServer(); handleDockIconHideOnAutoLaunch(); + increaseDiskCache(); enableSharedArrayBufferSupport(); + app.on("second-instance", () => { // Someone tried to run a second instance, we should focus our window. if (mainWindow) { @@ -98,23 +159,34 @@ if (!gotTheLock) { } }); - // This method will be called when Electron has finished - // initialization and is ready to create browser windows. - // Some APIs can only be used after this event occurs. + // Emitted once, when Electron has finished initializing. + // + // Note that some Electron APIs can only be used after this event occurs. app.on("ready", async () => { - logSystemInfo(); + logStartupBanner(); mainWindow = await createWindow(); - const tray = setupTrayItem(mainWindow); const watcher = initWatcher(mainWindow); + setupTrayItem(mainWindow); setupMacWindowOnDockIconClick(); - setupMainMenu(mainWindow); - setupIpcComs(tray, mainWindow, watcher); - await handleUpdates(mainWindow); + Menu.setApplicationMenu(await createApplicationMenu(mainWindow)); + attachIPCHandlers(); + attachFSWatchIPCHandlers(watcher); + if (!isDev) setupAutoUpdater(mainWindow); handleDownloads(mainWindow); handleExternalLinks(mainWindow); addAllowOriginHeader(mainWindow); setupAppEventEmitter(mainWindow); + + try { + deleteLegacyDiskCacheDirIfExists(); + } catch (e) { + // Log but otherwise ignore errors during non-critical startup + // actions + log.error("Ignoring startup error", e); + } }); app.on("before-quit", () => setIsAppQuitting(true)); -} +}; + +main(); diff --git a/desktop/src/main/dialogs.ts b/desktop/src/main/dialogs.ts new file mode 100644 index 0000000000..5f18878b51 --- /dev/null +++ b/desktop/src/main/dialogs.ts @@ -0,0 +1,54 @@ +import { dialog } from "electron/main"; +import path from "node:path"; +import { getDirFilePaths, getElectronFile } from "../services/fs"; +import { getElectronFilesFromGoogleZip } from "../services/upload"; +import type { ElectronFile } from "../types/ipc"; + +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)); +}; + +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 new file mode 100644 index 0000000000..0da89fb005 --- /dev/null +++ b/desktop/src/main/fs.ts @@ -0,0 +1,133 @@ +/** + * @file file system related functions exposed over the context bridge. + */ +import { createWriteStream, existsSync } from "node:fs"; +import fs from "node:fs/promises"; +import path from "node:path"; +import { Readable } from "node:stream"; + +export const fsExists = (path: string) => existsSync(path); + +/** + * Write a (web) ReadableStream to a file at the given {@link filePath}. + * + * 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 + */ +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; +}; + +const writeNodeStream = async ( + filePath: string, + fileStream: NodeJS.ReadableStream, +) => { + const writeable = createWriteStream(filePath); + + fileStream.on("error", (error) => { + writeable.destroy(error); // 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) => { + if (existsSync(filePath)) { + await fs.unlink(filePath); + } + reject(e); + }); + }); +}; + +/* TODO: Audit below this */ + +export const checkExistsAndCreateDir = (dirPath: string) => + fs.mkdir(dirPath, { recursive: true }); + +export const saveStreamToDisk = writeStream; + +export const saveFileToDisk = (path: string, contents: string) => + fs.writeFile(path, contents); + +export const readTextFile = async (filePath: string) => + fs.readFile(filePath, "utf-8"); + +export const moveFile = async (sourcePath: string, destinationPath: string) => { + if (!existsSync(sourcePath)) { + throw new Error("File does not exist"); + } + if (existsSync(destinationPath)) { + throw new Error("Destination file already exists"); + } + // check if destination folder exists + const destinationFolder = path.dirname(destinationPath); + await fs.mkdir(destinationFolder, { recursive: true }); + await fs.rename(sourcePath, destinationPath); +}; + +export const isFolder = async (dirPath: string) => { + if (!existsSync(dirPath)) return false; + const stats = await fs.stat(dirPath); + return stats.isDirectory(); +}; + +export const deleteFolder = async (folderPath: string) => { + // Ensure it is folder + if (!isFolder(folderPath)) return; + + // Ensure folder is empty + const files = await fs.readdir(folderPath); + if (files.length > 0) throw new Error("Folder is not empty"); + + // rm -rf it + await fs.rmdir(folderPath); +}; + +export const rename = async (oldPath: string, newPath: string) => { + if (!existsSync(oldPath)) throw new Error("Path does not exist"); + await fs.rename(oldPath, newPath); +}; + +export const deleteFile = async (filePath: string) => { + // Ensure it exists + if (!existsSync(filePath)) return; + + // And is a file + const stat = await fs.stat(filePath); + if (!stat.isFile()) throw new Error("Path is not a file"); + + // rm it + return fs.rm(filePath); +}; diff --git a/desktop/src/main/init.ts b/desktop/src/main/init.ts new file mode 100644 index 0000000000..0e94232aa1 --- /dev/null +++ b/desktop/src/main/init.ts @@ -0,0 +1,180 @@ +import { app, BrowserWindow, nativeImage, Tray } from "electron"; +import { existsSync } from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { isAppQuitting, rendererURL } from "../main"; +import autoLauncher from "../services/autoLauncher"; +import { getHideDockIconPreference } from "../services/userPreference"; +import { isPlatform } from "../utils/common/platform"; +import log from "./log"; +import { createTrayContextMenu } from "./menu"; +import { isDev } from "./util"; + +/** + * Create an return the {@link BrowserWindow} that will form our app's UI. + * + * This window will show the HTML served from {@link rendererURL}. + */ +export const createWindow = async () => { + // Create the main window. This'll show our web content. + const mainWindow = new BrowserWindow({ + webPreferences: { + preload: path.join(app.getAppPath(), "preload.js"), + }, + // The color to show in the window until the web content gets loaded. + // See: https://www.electronjs.org/docs/latest/api/browser-window#setting-the-backgroundcolor-property + backgroundColor: "black", + // We'll show it conditionally depending on `wasAutoLaunched` later. + show: false, + }); + + const wasAutoLaunched = await autoLauncher.wasAutoLaunched(); + if (wasAutoLaunched) { + // Keep the macOS dock icon hidden if we were auto launched. + if (process.platform == "darwin") app.dock.hide(); + } else { + // Show our window (maximizing it) if this is not an auto-launch on + // login. + mainWindow.maximize(); + } + + mainWindow.loadURL(rendererURL); + + // Open the DevTools automatically when running in dev mode + if (isDev) mainWindow.webContents.openDevTools(); + + mainWindow.webContents.on("render-process-gone", (_, details) => { + log.error(`render-process-gone: ${details}`); + mainWindow.webContents.reload(); + }); + + mainWindow.webContents.on("unresponsive", () => { + log.error("webContents unresponsive"); + mainWindow.webContents.forcefullyCrashRenderer(); + }); + + mainWindow.on("close", function (event) { + if (!isAppQuitting()) { + event.preventDefault(); + mainWindow.hide(); + } + return false; + }); + + mainWindow.on("hide", () => { + // On macOS, also hide the app's icon in the dock if the user has + // selected the Settings > Hide dock icon checkbox. + const shouldHideDockIcon = getHideDockIconPreference(); + if (process.platform == "darwin" && shouldHideDockIcon) { + app.dock.hide(); + } + }); + + mainWindow.on("show", () => { + if (process.platform == "darwin") app.dock.show(); + }); + + return mainWindow; +}; + +export async function handleUpdates(mainWindow: BrowserWindow) {} + +export const setupTrayItem = (mainWindow: BrowserWindow) => { + const iconName = isPlatform("mac") + ? "taskbar-icon-Template.png" + : "taskbar-icon.png"; + const trayImgPath = path.join( + isDev ? "build" : process.resourcesPath, + iconName, + ); + const trayIcon = nativeImage.createFromPath(trayImgPath); + const tray = new Tray(trayIcon); + tray.setToolTip("ente"); + tray.setContextMenu(createTrayContextMenu(mainWindow)); +}; + +export function handleDownloads(mainWindow: BrowserWindow) { + mainWindow.webContents.session.on("will-download", (_, item) => { + item.setSavePath( + getUniqueSavePath(item.getFilename(), app.getPath("downloads")), + ); + }); +} + +export function handleExternalLinks(mainWindow: BrowserWindow) { + mainWindow.webContents.setWindowOpenHandler(({ url }) => { + if (!url.startsWith(rendererURL)) { + require("electron").shell.openExternal(url); + return { action: "deny" }; + } else { + return { action: "allow" }; + } + }); +} + +export function getUniqueSavePath(filename: string, directory: string): string { + let uniqueFileSavePath = path.join(directory, filename); + const { name: filenameWithoutExtension, ext: extension } = + path.parse(filename); + let n = 0; + while (existsSync(uniqueFileSavePath)) { + n++; + // filter need to remove undefined extension from the array + // else [`${fileName}`, undefined].join(".") will lead to `${fileName}.` as joined string + const fileNameWithNumberedSuffix = [ + `${filenameWithoutExtension}(${n})`, + extension, + ] + .filter((x) => x) // filters out undefined/null values + .join(""); + uniqueFileSavePath = path.join(directory, fileNameWithNumberedSuffix); + } + return uniqueFileSavePath; +} + +export function setupMacWindowOnDockIconClick() { + app.on("activate", function () { + const windows = BrowserWindow.getAllWindows(); + // we allow only one window + windows[0].show(); + }); +} + +export async function handleDockIconHideOnAutoLaunch() { + const shouldHideDockIcon = getHideDockIconPreference(); + const wasAutoLaunched = await autoLauncher.wasAutoLaunched(); + + if (isPlatform("mac") && shouldHideDockIcon && wasAutoLaunched) { + app.dock.hide(); + } +} + +export function logStartupBanner() { + const version = isDev ? "dev" : app.getVersion(); + log.info(`Hello from ente-photos-desktop ${version}`); + + const platform = process.platform; + const osRelease = os.release(); + const systemVersion = process.getSystemVersion(); + log.info("Running on", { platform, osRelease, systemVersion }); +} + +function lowerCaseHeaders(responseHeaders: Record) { + const headers: Record = {}; + for (const key of Object.keys(responseHeaders)) { + headers[key.toLowerCase()] = responseHeaders[key]; + } + return headers; +} + +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, + }); + }, + ); +} diff --git a/desktop/src/main/ipc.ts b/desktop/src/main/ipc.ts new file mode 100644 index 0000000000..be97981867 --- /dev/null +++ b/desktop/src/main/ipc.ts @@ -0,0 +1,268 @@ +/** + * @file Listen for IPC events sent/invoked by the renderer process, and route + * them to their correct handlers. + * + * This file is meant as a sibling to `preload.ts`, but this one runs in the + * context of the main process, and can import other files from `src/`. + * + * See [Note: types.ts <-> preload.ts <-> ipc.ts] + */ + +import type { FSWatcher } from "chokidar"; +import { ipcMain } from "electron/main"; +import { clearElectronStore } from "../api/electronStore"; +import { getEncryptionKey, setEncryptionKey } from "../api/safeStorage"; +import { + getElectronFilesFromGoogleZip, + getPendingUploads, + setToUploadCollection, + setToUploadFiles, +} from "../api/upload"; +import { + appVersion, + muteUpdateNotification, + skipAppUpdate, + updateAndRestart, +} from "../services/appUpdater"; +import { + computeImageEmbedding, + computeTextEmbedding, +} from "../services/clipService"; +import { runFFmpegCmd } from "../services/ffmpeg"; +import { getDirFiles } from "../services/fs"; +import { + convertToJPEG, + generateImageThumbnail, +} from "../services/imageProcessor"; +import { + addWatchMapping, + getWatchMappings, + removeWatchMapping, + updateWatchMappingIgnoredFiles, + updateWatchMappingSyncedFiles, +} from "../services/watch"; +import type { + ElectronFile, + FILE_PATH_TYPE, + Model, + WatchMapping, +} from "../types/ipc"; +import { + selectDirectory, + showUploadDirsDialog, + showUploadFilesDialog, + showUploadZipDialog, +} from "./dialogs"; +import { + checkExistsAndCreateDir, + deleteFile, + deleteFolder, + fsExists, + isFolder, + moveFile, + readTextFile, + rename, + saveFileToDisk, + saveStreamToDisk, +} from "./fs"; +import { logToDisk } from "./log"; +import { openDirectory, openLogDirectory } from "./util"; + +/** + * Listen for IPC events sent/invoked by the renderer process, and route them to + * their correct handlers. + */ +export const attachIPCHandlers = () => { + // Notes: + // + // The first parameter of the handler passed to `ipcMain.handle` is the + // `event`, and is usually ignored. The rest of the parameters are the + // arguments passed to `ipcRenderer.invoke`. + // + // [Note: Catching exception during .send/.on] + // + // While we can use ipcRenderer.send/ipcMain.on for one-way communication, + // that has the disadvantage that any exceptions thrown in the processing of + // the handler are not sent back to the renderer. So we use the + // ipcRenderer.invoke/ipcMain.handle 2-way pattern even for things that are + // conceptually one way. An exception (pun intended) to this is logToDisk, + // which is a primitive, frequently used, operation and shouldn't throw, so + // having its signature by synchronous is a bit convenient. + + // - General + + ipcMain.handle("appVersion", (_) => appVersion()); + + ipcMain.handle("openDirectory", (_, dirPath) => openDirectory(dirPath)); + + ipcMain.handle("openLogDirectory", (_) => openLogDirectory()); + + // See [Note: Catching exception during .send/.on] + ipcMain.on("logToDisk", (_, message) => logToDisk(message)); + + ipcMain.on("clear-electron-store", (_) => { + clearElectronStore(); + }); + + ipcMain.handle("setEncryptionKey", (_, encryptionKey) => + setEncryptionKey(encryptionKey), + ); + + ipcMain.handle("getEncryptionKey", (_) => getEncryptionKey()); + + // - App update + + ipcMain.on("update-and-restart", (_) => updateAndRestart()); + + ipcMain.on("skip-app-update", (_, version) => skipAppUpdate(version)); + + ipcMain.on("mute-update-notification", (_, version) => + muteUpdateNotification(version), + ); + + // - Conversion + + ipcMain.handle("convertToJPEG", (_, fileData, filename) => + convertToJPEG(fileData, filename), + ); + + ipcMain.handle( + "generateImageThumbnail", + (_, inputFile, maxDimension, maxSize) => + generateImageThumbnail(inputFile, maxDimension, maxSize), + ); + + ipcMain.handle( + "runFFmpegCmd", + ( + _, + cmd: string[], + inputFile: File | ElectronFile, + outputFileName: string, + dontTimeout?: boolean, + ) => runFFmpegCmd(cmd, inputFile, outputFileName, dontTimeout), + ); + + // - ML + + ipcMain.handle( + "computeImageEmbedding", + (_, model: Model, imageData: Uint8Array) => + computeImageEmbedding(model, imageData), + ); + + ipcMain.handle("computeTextEmbedding", (_, model: Model, text: string) => + computeTextEmbedding(model, text), + ); + + // - File selection + + ipcMain.handle("selectDirectory", (_) => selectDirectory()); + + ipcMain.handle("showUploadFilesDialog", (_) => showUploadFilesDialog()); + + ipcMain.handle("showUploadDirsDialog", (_) => showUploadDirsDialog()); + + ipcMain.handle("showUploadZipDialog", (_) => showUploadZipDialog()); + + // - FS + + ipcMain.handle("fsExists", (_, path) => fsExists(path)); + + // - FS Legacy + + ipcMain.handle("checkExistsAndCreateDir", (_, dirPath) => + checkExistsAndCreateDir(dirPath), + ); + + ipcMain.handle( + "saveStreamToDisk", + (_, path: string, fileStream: ReadableStream) => + saveStreamToDisk(path, fileStream), + ); + + ipcMain.handle("saveFileToDisk", (_, path: string, file: any) => + saveFileToDisk(path, file), + ); + + ipcMain.handle("readTextFile", (_, path: string) => readTextFile(path)); + + ipcMain.handle("isFolder", (_, dirPath: string) => isFolder(dirPath)); + + ipcMain.handle("moveFile", (_, oldPath: string, newPath: string) => + moveFile(oldPath, newPath), + ); + + ipcMain.handle("deleteFolder", (_, path: string) => deleteFolder(path)); + + ipcMain.handle("deleteFile", (_, path: string) => deleteFile(path)); + + ipcMain.handle("rename", (_, oldPath: string, newPath: string) => + rename(oldPath, newPath), + ); + + // - Upload + + ipcMain.handle("getPendingUploads", (_) => getPendingUploads()); + + ipcMain.handle( + "setToUploadFiles", + (_, type: FILE_PATH_TYPE, filePaths: string[]) => + setToUploadFiles(type, filePaths), + ); + + ipcMain.handle("getElectronFilesFromGoogleZip", (_, filePath: string) => + getElectronFilesFromGoogleZip(filePath), + ); + + ipcMain.handle("setToUploadCollection", (_, collectionName: string) => + setToUploadCollection(collectionName), + ); + + ipcMain.handle("getDirFiles", (_, dirPath: string) => getDirFiles(dirPath)); +}; + +/** + * Sibling of {@link attachIPCHandlers} that attaches handlers specific to the + * watch folder functionality. + * + * It gets passed a {@link FSWatcher} instance which it can then forward to the + * actual handlers. + */ +export const attachFSWatchIPCHandlers = (watcher: FSWatcher) => { + // - Watch + + ipcMain.handle( + "addWatchMapping", + ( + _, + collectionName: string, + folderPath: string, + uploadStrategy: number, + ) => + addWatchMapping( + watcher, + collectionName, + folderPath, + uploadStrategy, + ), + ); + + ipcMain.handle("removeWatchMapping", (_, folderPath: string) => + removeWatchMapping(watcher, folderPath), + ); + + ipcMain.handle("getWatchMappings", (_) => getWatchMappings()); + + ipcMain.handle( + "updateWatchMappingSyncedFiles", + (_, folderPath: string, files: WatchMapping["syncedFiles"]) => + updateWatchMappingSyncedFiles(folderPath, files), + ); + + ipcMain.handle( + "updateWatchMappingIgnoredFiles", + (_, folderPath: string, files: WatchMapping["ignoredFiles"]) => + updateWatchMappingIgnoredFiles(folderPath, files), + ); +}; diff --git a/desktop/src/main/log.ts b/desktop/src/main/log.ts new file mode 100644 index 0000000000..8787a530d2 --- /dev/null +++ b/desktop/src/main/log.ts @@ -0,0 +1,131 @@ +import log from "electron-log"; +import util from "node:util"; +import { isDev } from "./util"; + +/** + * 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 dev builds, it will also log to the console. + */ +export const initLogging = () => { + log.transports.file.fileName = "ente.log"; + log.transports.file.maxSize = 50 * 1024 * 1024; // 50MB; + log.transports.file.format = "[{y}-{m}-{d}T{h}:{i}:{s}{z}] {text}"; + + log.transports.console.level = false; +}; + +/** + * Write a {@link message} to the on-disk log. + * + * This is used by the renderer process (via the contextBridge) to add entries + * in the log that is saved on disk. + */ +export const logToDisk = (message: string) => { + log.info(`[rndr] ${message}`); +}; + +export const logError = logErrorSentry; + +/** Deprecated, but no alternative yet */ +export function logErrorSentry( + error: any, + msg: string, + info?: Record, +) { + logToDisk( + `error: ${error?.name} ${error?.message} ${ + error?.stack + } msg: ${msg} info: ${JSON.stringify(info)}`, + ); + if (isDev) { + console.log(error, { msg, info }); + } +} + +const logError1 = (message: string, e?: unknown) => { + if (!e) { + logError_(message); + return; + } + + let es: string; + if (e instanceof Error) { + // In practice, we expect ourselves to be called with Error objects, so + // this is the happy path so to say. + es = `${e.name}: ${e.message}\n${e.stack}`; + } else { + // For the rest rare cases, use the default string serialization of e. + es = String(e); + } + + logError_(`${message}: ${es}`); +}; + +const logError_ = (message: string) => { + log.error(`[main] [error] ${message}`); + if (isDev) console.error(`[error] ${message}`); +}; + +const logInfo = (...params: any[]) => { + const message = params + .map((p) => (typeof p == "string" ? p : util.inspect(p))) + .join(" "); + log.info(`[main] ${message}`); + if (isDev) console.log(message); +}; + +const logDebug = (param: () => any) => { + if (isDev) console.log(`[debug] ${util.inspect(param())}`); +}; + +/** + * Ente's logger. + * + * This is an object that provides three functions to log at the corresponding + * levels - error, info or debug. + * + * {@link initLogging} needs to be called once before using any of these. + */ +export default { + /** + * Log an error message with an optional associated error object. + * + * {@link e} is generally expected to be an `instanceof Error` but it can be + * any arbitrary object that we obtain, say, when in a try-catch handler. + * + * The log is written to disk. In development builds, the log is also + * printed to the (Node.js process') console. + */ + error: logError1, + /** + * Log a message. + * + * This is meant as a replacement of {@link console.log}, and takes an + * arbitrary number of arbitrary parameters that it then serializes. + * + * The log is written to disk. In development builds, the log is also + * printed to the (Node.js process') console. + */ + info: logInfo, + /** + * Log a debug message. + * + * To avoid running unnecessary code in release builds, this takes a + * function to call to get the log message instead of directly taking the + * message. The provided function will only be called in development builds. + * + * The function can return an arbitrary value which is serialied before + * being logged. + * + * This log is not written to disk. It is printed to the (Node.js process') + * console only on development builds. + */ + debug: logDebug, +}; diff --git a/desktop/src/main/menu.ts b/desktop/src/main/menu.ts new file mode 100644 index 0000000000..6589329611 --- /dev/null +++ b/desktop/src/main/menu.ts @@ -0,0 +1,214 @@ +import { + app, + BrowserWindow, + Menu, + MenuItemConstructorOptions, + shell, +} from "electron"; +import { setIsAppQuitting } from "../main"; +import { forceCheckForUpdateAndNotify } from "../services/appUpdater"; +import autoLauncher from "../services/autoLauncher"; +import { + getHideDockIconPreference, + setHideDockIconPreference, +} from "../services/userPreference"; +import { openLogDirectory } from "./util"; + +/** Create and return the entries in the app's main menu bar */ +export const createApplicationMenu = async (mainWindow: BrowserWindow) => { + // The state of checkboxes + // + // 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 = getHideDockIconPreference(); + + const macOSOnly = (options: MenuItemConstructorOptions[]) => + process.platform == "darwin" ? options : []; + + const handleCheckForUpdates = () => + forceCheckForUpdateAndNotify(mainWindow); + + const handleViewChangelog = () => + shell.openExternal( + "https://github.com/ente-io/ente/blob/main/desktop/CHANGELOG.md", + ); + + const toggleAutoLaunch = () => { + autoLauncher.toggleAutoLaunch(); + isAutoLaunchEnabled = !isAutoLaunchEnabled; + }; + + const toggleHideDockIcon = () => { + setHideDockIconPreference(!shouldHideDockIcon); + shouldHideDockIcon = !shouldHideDockIcon; + }; + + const handleHelp = () => shell.openExternal("https://help.ente.io/photos/"); + + const handleSupport = () => shell.openExternal("mailto:support@ente.io"); + + const handleBlog = () => shell.openExternal("https://ente.io/blog/"); + + const handleViewLogs = openLogDirectory; + + return Menu.buildFromTemplate([ + { + label: "ente", + submenu: [ + ...macOSOnly([ + { + label: "About Ente", + role: "about", + }, + ]), + { type: "separator" }, + { + label: "Check for Updates...", + click: handleCheckForUpdates, + }, + { + label: "View Changelog", + click: handleViewChangelog, + }, + { type: "separator" }, + + { + label: "Preferences", + submenu: [ + { + label: "Open Ente on Startup", + type: "checkbox", + checked: isAutoLaunchEnabled, + click: toggleAutoLaunch, + }, + { + label: "Hide Dock Icon", + type: "checkbox", + checked: shouldHideDockIcon, + click: toggleHideDockIcon, + }, + ], + }, + + { type: "separator" }, + ...macOSOnly([ + { + label: "Hide Ente", + role: "hide", + }, + { + label: "Hide Others", + role: "hideOthers", + }, + { type: "separator" }, + ]), + { + label: "Quit", + role: "quit", + }, + ], + }, + { + label: "Edit", + submenu: [ + { label: "Undo", role: "undo" }, + { label: "Redo", role: "redo" }, + { type: "separator" }, + { label: "Cut", role: "cut" }, + { label: "Copy", role: "copy" }, + { label: "Paste", role: "paste" }, + { label: "Select All", role: "selectAll" }, + ...macOSOnly([ + { type: "separator" }, + { + label: "Speech", + submenu: [ + { + role: "startSpeaking", + label: "start speaking", + }, + { + role: "stopSpeaking", + label: "stop speaking", + }, + ], + }, + ]), + ], + }, + { + label: "View", + submenu: [ + { label: "Reload", role: "reload" }, + { label: "Toggle Dev Tools", role: "toggleDevTools" }, + { type: "separator" }, + { label: "Toggle Full Screen", role: "togglefullscreen" }, + ], + }, + { + label: "Window", + submenu: [ + { label: "Minimize", role: "minimize" }, + { label: "Zoom", role: "zoom" }, + { label: "Close", role: "close" }, + ...macOSOnly([ + { type: "separator" }, + { label: "Bring All to Front", role: "front" }, + { type: "separator" }, + { label: "Ente", role: "window" }, + ]), + ], + }, + { + label: "Help", + submenu: [ + { + label: "Ente Help", + click: handleHelp, + }, + { type: "separator" }, + { + label: "Support", + click: handleSupport, + }, + { + label: "Product Updates", + click: handleBlog, + }, + { type: "separator" }, + { + label: "View Logs", + click: handleViewLogs, + }, + ], + }, + ]); +}; + +/** + * Create and return a {@link Menu} that is shown when the user clicks on our + * system tray icon (e.g. the icon list at the top right of the screen on macOS) + */ +export const createTrayContextMenu = (mainWindow: BrowserWindow) => { + const handleOpen = () => { + mainWindow.maximize(); + mainWindow.show(); + }; + + const handleClose = () => { + setIsAppQuitting(true); + app.quit(); + }; + + return Menu.buildFromTemplate([ + { + label: "Open Ente", + click: handleOpen, + }, + { + label: "Quit Ente", + click: handleClose, + }, + ]); +}; diff --git a/desktop/src/main/util.ts b/desktop/src/main/util.ts new file mode 100644 index 0000000000..d0c6699e9a --- /dev/null +++ b/desktop/src/main/util.ts @@ -0,0 +1,81 @@ +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"; + +/** `true` if the app is running in development mode. */ +export const isDev = !app.isPackaged; + +/** + * Run a shell command asynchronously. + * + * This is a convenience promisified version of child_process.exec. It runs the + * command asynchronously and returns its stdout and stderr if there were no + * errors. + * + * If the command is passed as a string, then it will be executed verbatim. + * + * If the command is passed as an array, then the first argument will be treated + * as the executable and the remaining (optional) items as the command line + * parameters. This function will shellescape and join the array to form the + * command that finally gets executed. + * + * > Note: This is not a 1-1 replacement of child_process.exec - if you're + * > trying to run a trivial shell command, say something that produces a lot of + * > output, this might not be the best option and it might be better to use the + * > underlying functions. + */ +export const execAsync = (command: string | string[]) => { + const escapedCommand = Array.isArray(command) + ? shellescape(command) + : command; + const startTime = Date.now(); + log.debug(() => `Running shell command: ${escapedCommand}`); + const result = execAsync_(escapedCommand); + log.debug( + () => + `Completed in ${Math.round(Date.now() - startTime)} ms (${escapedCommand})`, + ); + return result; +}; + +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`); +}; + +/** + * 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"); + +/** + * Open the app's log directory in the system's folder viewer. + * + * @see {@link openDirectory} + */ +export const openLogDirectory = () => openDirectory(logDirectoryPath()); diff --git a/desktop/src/preload.ts b/desktop/src/preload.ts index 40b37fe516..4b171e28ec 100644 --- a/desktop/src/preload.ts +++ b/desktop/src/preload.ts @@ -7,349 +7,92 @@ * functions as an object on the DOM, so that the renderer process can invoke * functions that live in the main (Node.js) process if needed. * + * Ref: https://www.electronjs.org/docs/latest/tutorial/tutorial-preload + * * Note that this script cannot import other code from `src/` - conceptually it * can be thought of as running in a separate, third, process different from * both the main or a renderer process (technically, it runs in a BrowserWindow * context that runs prior to the renderer process). * - * That said, this can be split into multiple files if we wished. However, - * that'd require us setting up a bundler to package it back up into a single JS - * file that can be used at runtime. - * * > Since enabling the sandbox disables Node.js integration in your preload * > scripts, you can no longer use require("../my-script"). In other words, * > your preload script needs to be a single file. * > * > https://www.electronjs.org/blog/breach-to-barrier * - * Since most of this is just boilerplate code providing a bridge between the - * main and renderer, we avoid introducing another moving part into the mix and - * just keep the entire preload setup in this single file. + * If we really wanted, we could setup a bundler to package this into a single + * file. However, since this is just boilerplate code providing a bridge between + * the main and renderer, we avoid introducing another moving part into the mix + * and just keep the entire preload setup in this single file. + * + * [Note: types.ts <-> preload.ts <-> ipc.ts] + * + * The following three files are boilerplatish linkage of the same functions, + * and when changing one of them, remember to see if the other two also need + * changing: + * + * - [renderer] web/packages/shared/electron/types.ts contains docs + * - [preload] desktop/src/preload.ts ↕︎ + * - [main] desktop/src/main/ipc.ts contains impl */ -import { contextBridge, ipcRenderer } from "electron"; -import { existsSync } from "fs"; -import path from "path"; -import * as fs from "promise-fs"; -import { Readable } from "stream"; -import { deleteDiskCache, openDiskCache } from "./api/cache"; -import { logToDisk, openLogDirectory } from "./api/common"; -import { - checkExistsAndCreateDir, - exists, - saveFileToDisk, - saveStreamToDisk, -} from "./api/export"; -import { runFFmpegCmd } from "./api/ffmpeg"; -import { getDirFiles } from "./api/fs"; -import { convertToJPEG, generateImageThumbnail } from "./api/imageProcessor"; -import { getEncryptionKey, setEncryptionKey } from "./api/safeStorage"; -import { - registerForegroundEventListener, - registerUpdateEventListener, -} from "./api/system"; -import { - getElectronFilesFromGoogleZip, - getPendingUploads, - setToUploadCollection, - setToUploadFiles, - showUploadDirsDialog, - showUploadFilesDialog, - showUploadZipDialog, -} from "./api/upload"; -import { - addWatchMapping, - getWatchMappings, - registerWatcherFunctions, - removeWatchMapping, - updateWatchMappingIgnoredFiles, - updateWatchMappingSyncedFiles, -} from "./api/watch"; -import { setupLogging } from "./utils/logging"; +import { contextBridge, ipcRenderer } from "electron/renderer"; -/* Some of the code below has been duplicated to make this file self contained. -Enhancement: consider alternatives */ +// While we can't import other code, we can import types since they're just +// needed when compiling and will not be needed / looked around for at runtime. +import type { + AppUpdateInfo, + ElectronFile, + FILE_PATH_TYPE, + Model, + WatchMapping, +} from "./types/ipc"; -/* preload: duplicated logError */ -export function logError(error: Error, message: string, info?: string): void { - ipcRenderer.invoke("log-error", error, message, info); -} +// - General -// - +const appVersion = (): Promise => ipcRenderer.invoke("appVersion"); -export const convertBrowserStreamToNode = ( - fileStream: ReadableStream, -) => { - const reader = fileStream.getReader(); - const rs = new Readable(); +const openDirectory = (dirPath: string): Promise => + ipcRenderer.invoke("openDirectory"); - rs._read = async () => { - try { - const result = await reader.read(); +const openLogDirectory = (): Promise => + ipcRenderer.invoke("openLogDirectory"); - if (!result.done) { - rs.push(Buffer.from(result.value)); - } else { - rs.push(null); - return; - } - } catch (e) { - rs.emit("error", e); - } - }; +const logToDisk = (message: string): void => + ipcRenderer.send("logToDisk", message); - return rs; -}; +const fsExists = (path: string): Promise => + ipcRenderer.invoke("fsExists", path); -export async function writeNodeStream( - filePath: string, - fileStream: NodeJS.ReadableStream, -) { - const writeable = fs.createWriteStream(filePath); +// - AUDIT below this - fileStream.on("error", (error) => { - writeable.destroy(error); // Close the writable stream with an error +const registerForegroundEventListener = (onForeground: () => void) => { + ipcRenderer.removeAllListeners("app-in-foreground"); + ipcRenderer.on("app-in-foreground", () => { + onForeground(); }); - - fileStream.pipe(writeable); - - await new Promise((resolve, reject) => { - writeable.on("finish", resolve); - writeable.on("error", async (e) => { - if (existsSync(filePath)) { - await fs.unlink(filePath); - } - reject(e); - }); - }); -} - -/* preload: duplicated writeStream */ -export async function writeStream( - filePath: string, - fileStream: ReadableStream, -) { - const readable = convertBrowserStreamToNode(fileStream); - await writeNodeStream(filePath, readable); -} - -// - - -async function readTextFile(filePath: string) { - if (!existsSync(filePath)) { - throw new Error("File does not exist"); - } - return await fs.readFile(filePath, "utf-8"); -} - -async function moveFile( - sourcePath: string, - destinationPath: string, -): Promise { - if (!existsSync(sourcePath)) { - throw new Error("File does not exist"); - } - if (existsSync(destinationPath)) { - throw new Error("Destination file already exists"); - } - // check if destination folder exists - const destinationFolder = path.dirname(destinationPath); - if (!existsSync(destinationFolder)) { - await fs.mkdir(destinationFolder, { recursive: true }); - } - await fs.rename(sourcePath, destinationPath); -} - -export async function isFolder(dirPath: string) { - try { - const stats = await fs.stat(dirPath); - return stats.isDirectory(); - } catch (e) { - let err = e; - // if code is defined, it's an error from fs.stat - if (typeof e.code !== "undefined") { - // ENOENT means the file does not exist - if (e.code === "ENOENT") { - return false; - } - err = Error(`fs error code: ${e.code}`); - } - logError(err, "isFolder failed"); - return false; - } -} - -async function deleteFolder(folderPath: string): Promise { - if (!existsSync(folderPath)) { - return; - } - if (!fs.statSync(folderPath).isDirectory()) { - throw new Error("Path is not a folder"); - } - // check if folder is empty - const files = await fs.readdir(folderPath); - if (files.length > 0) { - throw new Error("Folder is not empty"); - } - await fs.rmdir(folderPath); -} - -async function rename(oldPath: string, newPath: string) { - if (!existsSync(oldPath)) { - throw new Error("Path does not exist"); - } - await fs.rename(oldPath, newPath); -} - -function deleteFile(filePath: string): void { - if (!existsSync(filePath)) { - return; - } - if (!fs.statSync(filePath).isFile()) { - throw new Error("Path is not a file"); - } - fs.rmSync(filePath); -} - -// - - -/* preload: duplicated Model */ -export enum Model { - GGML_CLIP = "ggml-clip", - ONNX_CLIP = "onnx-clip", -} - -const computeImageEmbedding = async ( - model: Model, - imageData: Uint8Array, -): Promise => { - let tempInputFilePath = null; - try { - tempInputFilePath = await ipcRenderer.invoke("get-temp-file-path", ""); - const imageStream = new Response(imageData.buffer).body; - await writeStream(tempInputFilePath, imageStream); - const embedding = await ipcRenderer.invoke( - "compute-image-embedding", - model, - tempInputFilePath, - ); - return embedding; - } catch (err) { - if (isExecError(err)) { - const parsedExecError = parseExecError(err); - throw Error(parsedExecError); - } else { - throw err; - } - } finally { - if (tempInputFilePath) { - await ipcRenderer.invoke("remove-temp-file", tempInputFilePath); - } - } }; -export async function computeTextEmbedding( - model: Model, - text: string, -): Promise { - try { - const embedding = await ipcRenderer.invoke( - "compute-text-embedding", - model, - text, - ); - return embedding; - } catch (err) { - if (isExecError(err)) { - const parsedExecError = parseExecError(err); - throw Error(parsedExecError); - } else { - throw err; - } - } -} - -// - - -/** - * [Note: Custom errors across Electron/Renderer boundary] - * - * We need to use the `message` field to disambiguate between errors thrown by - * the main process when invoked from the renderer process. This is because: - * - * > Errors thrown throw `handle` in the main process are not transparent as - * > they are serialized and only the `message` property from the original error - * > is provided to the renderer process. - * > - * > - https://www.electronjs.org/docs/latest/tutorial/ipc - * > - * > Ref: https://github.com/electron/electron/issues/24427 - */ -/* preload: duplicated CustomErrors */ -const CustomErrorsP = { - WINDOWS_NATIVE_IMAGE_PROCESSING_NOT_SUPPORTED: - "Windows native image processing is not supported", - INVALID_OS: (os: string) => `Invalid OS - ${os}`, - WAIT_TIME_EXCEEDED: "Wait time exceeded", - UNSUPPORTED_PLATFORM: (platform: string, arch: string) => - `Unsupported platform - ${platform} ${arch}`, - MODEL_DOWNLOAD_PENDING: - "Model download pending, skipping clip search request", - INVALID_FILE_PATH: "Invalid file path", - INVALID_CLIP_MODEL: (model: string) => `Invalid Clip model - ${model}`, -}; - -const isExecError = (err: any) => { - return err.message.includes("Command failed:"); -}; - -const parseExecError = (err: any) => { - const errMessage = err.message; - if (errMessage.includes("Bad CPU type in executable")) { - return CustomErrorsP.UNSUPPORTED_PLATFORM( - process.platform, - process.arch, - ); - } else { - return errMessage; - } -}; - -// - - -const selectDirectory = async (): Promise => { - try { - return await ipcRenderer.invoke("select-dir"); - } catch (e) { - logError(e, "error while selecting root directory"); - } -}; - -const getAppVersion = async (): Promise => { - try { - return await ipcRenderer.invoke("get-app-version"); - } catch (e) { - logError(e, "failed to get release version"); - throw e; - } -}; - -const openDirectory = async (dirPath: string): Promise => { - try { - await ipcRenderer.invoke("open-dir", dirPath); - } catch (e) { - logError(e, "error while opening directory"); - throw e; - } -}; - -// - - const clearElectronStore = () => { ipcRenderer.send("clear-electron-store"); }; -// - +const setEncryptionKey = (encryptionKey: string): Promise => + ipcRenderer.invoke("setEncryptionKey", encryptionKey); + +const getEncryptionKey = (): Promise => + ipcRenderer.invoke("getEncryptionKey"); + +// - App update + +const registerUpdateEventListener = ( + showUpdateDialog: (updateInfo: AppUpdateInfo) => void, +) => { + ipcRenderer.removeAllListeners("show-update-dialog"); + ipcRenderer.on("show-update-dialog", (_, updateInfo: AppUpdateInfo) => { + showUpdateDialog(updateInfo); + }); +}; const updateAndRestart = () => { ipcRenderer.send("update-and-restart"); @@ -363,57 +106,272 @@ const muteUpdateNotification = (version: string) => { ipcRenderer.send("mute-update-notification", version); }; -// - +// - Conversion -setupLogging(); +const convertToJPEG = ( + fileData: Uint8Array, + filename: string, +): Promise => + ipcRenderer.invoke("convertToJPEG", fileData, filename); + +const generateImageThumbnail = ( + inputFile: File | ElectronFile, + maxDimension: number, + maxSize: number, +): Promise => + ipcRenderer.invoke( + "generateImageThumbnail", + inputFile, + maxDimension, + maxSize, + ); + +const runFFmpegCmd = ( + cmd: string[], + inputFile: File | ElectronFile, + outputFileName: string, + dontTimeout?: boolean, +): Promise => + ipcRenderer.invoke( + "runFFmpegCmd", + cmd, + inputFile, + outputFileName, + dontTimeout, + ); + +// - ML + +const computeImageEmbedding = ( + model: Model, + imageData: Uint8Array, +): Promise => + ipcRenderer.invoke("computeImageEmbedding", model, imageData); + +const computeTextEmbedding = ( + model: Model, + text: string, +): Promise => + ipcRenderer.invoke("computeTextEmbedding", model, text); + +// - 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"); + +// - Watch + +const registerWatcherFunctions = ( + addFile: (file: ElectronFile) => Promise, + removeFile: (path: string) => Promise, + removeFolder: (folderPath: string) => Promise, +) => { + ipcRenderer.removeAllListeners("watch-add"); + ipcRenderer.removeAllListeners("watch-unlink"); + ipcRenderer.removeAllListeners("watch-unlink-dir"); + ipcRenderer.on("watch-add", (_, file: ElectronFile) => addFile(file)); + ipcRenderer.on("watch-unlink", (_, filePath: string) => + removeFile(filePath), + ); + ipcRenderer.on("watch-unlink-dir", (_, folderPath: string) => + removeFolder(folderPath), + ); +}; + +const addWatchMapping = ( + collectionName: string, + folderPath: string, + uploadStrategy: number, +): Promise => + ipcRenderer.invoke( + "addWatchMapping", + collectionName, + folderPath, + uploadStrategy, + ); + +const removeWatchMapping = (folderPath: string): Promise => + ipcRenderer.invoke("removeWatchMapping", folderPath); + +const getWatchMappings = (): Promise => + ipcRenderer.invoke("getWatchMappings"); + +const updateWatchMappingSyncedFiles = ( + folderPath: string, + files: WatchMapping["syncedFiles"], +): Promise => + ipcRenderer.invoke("updateWatchMappingSyncedFiles", folderPath, files); + +const updateWatchMappingIgnoredFiles = ( + folderPath: string, + files: WatchMapping["ignoredFiles"], +): Promise => + ipcRenderer.invoke("updateWatchMappingIgnoredFiles", folderPath, files); + +// - FS Legacy + +const checkExistsAndCreateDir = (dirPath: string): Promise => + ipcRenderer.invoke("checkExistsAndCreateDir", dirPath); + +const saveStreamToDisk = ( + path: string, + fileStream: ReadableStream, +): Promise => ipcRenderer.invoke("saveStreamToDisk", path, fileStream); + +const saveFileToDisk = (path: string, file: any): Promise => + ipcRenderer.invoke("saveFileToDisk", path, file); + +const readTextFile = (path: string): Promise => + ipcRenderer.invoke("readTextFile", path); + +const isFolder = (dirPath: string): Promise => + ipcRenderer.invoke("isFolder", dirPath); + +const moveFile = (oldPath: string, newPath: string): Promise => + ipcRenderer.invoke("moveFile", oldPath, newPath); + +const deleteFolder = (path: string): Promise => + ipcRenderer.invoke("deleteFolder", path); + +const deleteFile = (path: string): Promise => + ipcRenderer.invoke("deleteFile", path); + +const rename = (oldPath: string, newPath: string): Promise => + ipcRenderer.invoke("rename", oldPath, newPath); + +// - Upload + +const getPendingUploads = (): Promise<{ + files: ElectronFile[]; + collectionName: string; + type: string; +}> => ipcRenderer.invoke("getPendingUploads"); + +const setToUploadFiles = ( + type: FILE_PATH_TYPE, + filePaths: string[], +): Promise => ipcRenderer.invoke("setToUploadFiles", type, filePaths); + +const getElectronFilesFromGoogleZip = ( + filePath: string, +): Promise => + ipcRenderer.invoke("getElectronFilesFromGoogleZip", filePath); + +const setToUploadCollection = (collectionName: string): Promise => + ipcRenderer.invoke("setToUploadCollection", collectionName); + +const getDirFiles = (dirPath: string): Promise => + ipcRenderer.invoke("getDirFiles", dirPath); // These objects exposed here will become available to the JS code in our // renderer (the web/ code) as `window.ElectronAPIs.*` // -// https://www.electronjs.org/docs/latest/tutorial/tutorial-preload +// There are a few related concepts at play here, and it might be worthwhile to +// read their (excellent) documentation to get an understanding; +//` +// - ContextIsolation: +// https://www.electronjs.org/docs/latest/tutorial/context-isolation +// +// - IPC https://www.electronjs.org/docs/latest/tutorial/ipc +// +// [Note: Transferring large amount of data over IPC] +// +// Electron's IPC implementation uses the HTML standard Structured Clone +// Algorithm to serialize objects passed between processes. +// https://www.electronjs.org/docs/latest/tutorial/ipc#object-serialization +// +// In particular, ArrayBuffer is eligible for structured cloning. +// https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm +// +// Also, ArrayBuffer is "transferable", which means it is a zero-copy operation +// operation when it happens across threads. +// https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Transferable_objects +// +// In our case though, we're not dealing with threads but separate processes. So +// the ArrayBuffer will be copied: +// > "parameters, errors and return values are **copied** when they're sent over +// the bridge". +// https://www.electronjs.org/docs/latest/api/context-bridge#methods +// +// The copy itself is relatively fast, but the problem with transfering large +// amounts of data is potentially running out of memory during the copy. contextBridge.exposeInMainWorld("ElectronAPIs", { - exists, + // - General + appVersion, + openDirectory, + registerForegroundEventListener, + clearElectronStore, + getEncryptionKey, + setEncryptionKey, + + // - Logging + openLogDirectory, + logToDisk, + + // - App update + updateAndRestart, + skipAppUpdate, + muteUpdateNotification, + registerUpdateEventListener, + + // - Conversion + convertToJPEG, + generateImageThumbnail, + runFFmpegCmd, + + // - ML + computeImageEmbedding, + computeTextEmbedding, + + // - File selection + selectDirectory, + showUploadFilesDialog, + showUploadDirsDialog, + showUploadZipDialog, + + // - Watch + registerWatcherFunctions, + addWatchMapping, + removeWatchMapping, + getWatchMappings, + updateWatchMappingSyncedFiles, + updateWatchMappingIgnoredFiles, + + // - FS + fs: { + exists: fsExists, + }, + + // - FS legacy + // TODO: Move these into fs + document + rename if needed checkExistsAndCreateDir, saveStreamToDisk, saveFileToDisk, - selectDirectory, - clearElectronStore, readTextFile, - showUploadFilesDialog, - showUploadDirsDialog, - getPendingUploads, - setToUploadFiles, - showUploadZipDialog, - getElectronFilesFromGoogleZip, - setToUploadCollection, - getEncryptionKey, - setEncryptionKey, - openDiskCache, - deleteDiskCache, - getDirFiles, - getWatchMappings, - addWatchMapping, - removeWatchMapping, - registerWatcherFunctions, isFolder, - updateWatchMappingSyncedFiles, - updateWatchMappingIgnoredFiles, - logToDisk, - convertToJPEG, - openLogDirectory, - registerUpdateEventListener, - updateAndRestart, - skipAppUpdate, - getAppVersion, - runFFmpegCmd, - muteUpdateNotification, - generateImageThumbnail, - registerForegroundEventListener, - openDirectory, moveFile, deleteFolder, - rename, deleteFile, - computeImageEmbedding, - computeTextEmbedding, + rename, + + // - Upload + + getPendingUploads, + setToUploadFiles, + getElectronFilesFromGoogleZip, + setToUploadCollection, + getDirFiles, }); diff --git a/desktop/src/services/appUpdater.ts b/desktop/src/services/appUpdater.ts index 2ddcef7047..98db606a45 100644 --- a/desktop/src/services/appUpdater.ts +++ b/desktop/src/services/appUpdater.ts @@ -2,11 +2,9 @@ import { compareVersions } from "compare-versions"; import { app, BrowserWindow } from "electron"; import { default as ElectronLog, default as log } from "electron-log"; import { autoUpdater } from "electron-updater"; -import fetch from "node-fetch"; import { setIsAppQuitting, setIsUpdateAvailable } from "../main"; -import { AppUpdateInfo, GetFeatureFlagResponse } from "../types"; -import { isPlatform } from "../utils/common/platform"; -import { logErrorSentry } from "./sentry"; +import { logErrorSentry } from "../main/log"; +import { AppUpdateInfo } from "../types/ipc"; import { clearMuteUpdateNotificationVersion, clearSkipAppVersion, @@ -64,56 +62,42 @@ async function checkForUpdateAndNotify(mainWindow: BrowserWindow) { ); return; } - const desktopCutoffVersion = await getDesktopCutoffVersion(); + + let timeout: NodeJS.Timeout; + log.debug("attempting auto update"); + autoUpdater.downloadUpdate(); + const muteUpdateNotificationVersion = + getMuteUpdateNotificationVersion(); if ( - desktopCutoffVersion && - isPlatform("mac") && - compareVersions( - updateCheckResult.updateInfo.version, - desktopCutoffVersion, - ) > 0 + muteUpdateNotificationVersion && + updateCheckResult.updateInfo.version === + muteUpdateNotificationVersion ) { - log.debug("auto update not possible due to key change"); + log.info( + "user chose to mute update notification for version ", + updateCheckResult.updateInfo.version, + ); + return; + } + autoUpdater.on("update-downloaded", () => { + timeout = setTimeout( + () => + showUpdateDialog(mainWindow, { + autoUpdatable: true, + version: updateCheckResult.updateInfo.version, + }), + FIVE_MIN_IN_MICROSECOND, + ); + }); + autoUpdater.on("error", (error) => { + clearTimeout(timeout); + logErrorSentry(error, "auto update failed"); showUpdateDialog(mainWindow, { autoUpdatable: false, version: updateCheckResult.updateInfo.version, }); - } else { - let timeout: NodeJS.Timeout; - log.debug("attempting auto update"); - autoUpdater.downloadUpdate(); - const muteUpdateNotificationVersion = - getMuteUpdateNotificationVersion(); - if ( - muteUpdateNotificationVersion && - updateCheckResult.updateInfo.version === - muteUpdateNotificationVersion - ) { - log.info( - "user chose to mute update notification for version ", - updateCheckResult.updateInfo.version, - ); - return; - } - autoUpdater.on("update-downloaded", () => { - timeout = setTimeout( - () => - showUpdateDialog(mainWindow, { - autoUpdatable: true, - version: updateCheckResult.updateInfo.version, - }), - FIVE_MIN_IN_MICROSECOND, - ); - }); - autoUpdater.on("error", (error) => { - clearTimeout(timeout); - logErrorSentry(error, "auto update failed"); - showUpdateDialog(mainWindow, { - autoUpdatable: false, - version: updateCheckResult.updateInfo.version, - }); - }); - } + }); + setIsUpdateAvailable(true); } catch (e) { logErrorSentry(e, "checkForUpdateAndNotify failed"); @@ -126,9 +110,12 @@ export function updateAndRestart() { autoUpdater.quitAndInstall(); } -export function getAppVersion() { - return `v${app.getVersion()}`; -} +/** + * Return the version of the desktop app + * + * The return value is of the form `v1.2.3`. + */ +export const appVersion = () => `v${app.getVersion()}`; export function skipAppUpdate(version: string) { setSkipAppVersion(version); @@ -138,18 +125,6 @@ export function muteUpdateNotification(version: string) { setMuteUpdateNotificationVersion(version); } -async function getDesktopCutoffVersion() { - try { - const featureFlags = ( - await fetch("https://static.ente.io/feature_flags.json") - ).json() as GetFeatureFlagResponse; - return featureFlags.desktopCutoffVersion; - } catch (e) { - logErrorSentry(e, "failed to get feature flags"); - return undefined; - } -} - function showUpdateDialog( mainWindow: BrowserWindow, updateInfo: AppUpdateInfo, diff --git a/desktop/src/services/autoLauncher.ts b/desktop/src/services/autoLauncher.ts index 5cac556a9e..bc1270ac97 100644 --- a/desktop/src/services/autoLauncher.ts +++ b/desktop/src/services/autoLauncher.ts @@ -1,4 +1,4 @@ -import { AutoLauncherClient } from "../types/autoLauncher"; +import { AutoLauncherClient } from "../types/main"; import { isPlatform } from "../utils/common/platform"; import linuxAndWinAutoLauncher from "./autoLauncherClients/linuxAndWinAutoLauncher"; import macAutoLauncher from "./autoLauncherClients/macAutoLauncher"; diff --git a/desktop/src/services/autoLauncherClients/linuxAndWinAutoLauncher.ts b/desktop/src/services/autoLauncherClients/linuxAndWinAutoLauncher.ts index 132f8d1f57..761b58a067 100644 --- a/desktop/src/services/autoLauncherClients/linuxAndWinAutoLauncher.ts +++ b/desktop/src/services/autoLauncherClients/linuxAndWinAutoLauncher.ts @@ -1,6 +1,6 @@ import AutoLaunch from "auto-launch"; import { app } from "electron"; -import { AutoLauncherClient } from "../../types/autoLauncher"; +import { AutoLauncherClient } from "../../types/main"; const LAUNCHED_AS_HIDDEN_FLAG = "hidden"; diff --git a/desktop/src/services/autoLauncherClients/macAutoLauncher.ts b/desktop/src/services/autoLauncherClients/macAutoLauncher.ts index fcdc7bd814..d4fc343b03 100644 --- a/desktop/src/services/autoLauncherClients/macAutoLauncher.ts +++ b/desktop/src/services/autoLauncherClients/macAutoLauncher.ts @@ -1,5 +1,5 @@ import { app } from "electron"; -import { AutoLauncherClient } from "../../types/autoLauncher"; +import { AutoLauncherClient } from "../../types/main"; class MacAutoLauncher implements AutoLauncherClient { async isEnabled() { diff --git a/desktop/src/services/chokidar.ts b/desktop/src/services/chokidar.ts index f0d217d09b..57a0e504ee 100644 --- a/desktop/src/services/chokidar.ts +++ b/desktop/src/services/chokidar.ts @@ -1,7 +1,16 @@ import chokidar from "chokidar"; import { BrowserWindow } from "electron"; -import { getWatchMappings } from "../api/watch"; -import { logError } from "../services/logging"; +import path from "path"; +import { logError } from "../main/log"; +import { getWatchMappings } from "../services/watch"; +import { getElectronFile } from "./fs"; + +/** + * Convert a file system {@link filePath} that uses the local system specific + * path separators into a path that uses POSIX file separators. + */ +const normalizeToPOSIX = (filePath: string) => + filePath.split(path.sep).join(path.posix.sep); export function initWatcher(mainWindow: BrowserWindow) { const mappings = getWatchMappings(); @@ -13,17 +22,20 @@ export function initWatcher(mainWindow: BrowserWindow) { awaitWriteFinish: true, }); watcher - .on("add", (path) => { - mainWindow.webContents.send("watch-add", path); - }) - .on("change", (path) => { - mainWindow.webContents.send("watch-change", path); + .on("add", async (path) => { + mainWindow.webContents.send( + "watch-add", + await getElectronFile(normalizeToPOSIX(path)), + ); }) .on("unlink", (path) => { - mainWindow.webContents.send("watch-unlink", path); + mainWindow.webContents.send("watch-unlink", normalizeToPOSIX(path)); }) .on("unlinkDir", (path) => { - mainWindow.webContents.send("watch-unlink-dir", path); + mainWindow.webContents.send( + "watch-unlink-dir", + normalizeToPOSIX(path), + ); }) .on("error", (error) => { logError(error, "error while watching files"); diff --git a/desktop/src/services/clipService.ts b/desktop/src/services/clipService.ts index 4a808d7a4e..41e559a9b7 100644 --- a/desktop/src/services/clipService.ts +++ b/desktop/src/services/clipService.ts @@ -1,20 +1,16 @@ -import { app } from "electron"; -import * as log from "electron-log"; +import { app, net } from "electron/main"; import { existsSync } from "fs"; -import fs from "fs/promises"; -import fetch from "node-fetch"; -import path from "path"; -import { readFile } from "promise-fs"; -import util from "util"; +import fs from "node:fs/promises"; +import path from "node:path"; import { CustomErrors } from "../constants/errors"; -import { Model } from "../types"; +import { writeStream } from "../main/fs"; +import log, { logErrorSentry } from "../main/log"; +import { execAsync, isDev } from "../main/util"; +import { Model } from "../types/ipc"; import Tokenizer from "../utils/clip-bpe-ts/mod"; -import { isDev } from "../utils/common"; import { getPlatform } from "../utils/common/platform"; -import { writeNodeStream } from "./fs"; -import { logErrorSentry } from "./sentry"; -const shellescape = require("any-shell-escape"); -const execAsync = util.promisify(require("child_process").exec); +import { generateTempFilePath } from "../utils/temp"; +import { deleteTempFile } from "./ffmpeg"; const jpeg = require("jpeg-js"); const CLIP_MODEL_PATH_PLACEHOLDER = "CLIP_MODEL"; @@ -65,28 +61,18 @@ const TEXT_MODEL_SIZE_IN_BYTES = { onnx: 64173509, // 61.2 MB }; -const MODEL_SAVE_FOLDER = "models"; - -function getModelSavePath(modelName: string) { - let userDataDir: string; - if (isDev) { - userDataDir = "."; - } else { - userDataDir = app.getPath("userData"); - } - return path.join(userDataDir, MODEL_SAVE_FOLDER, modelName); -} +/** Return the path where the given {@link modelName} is meant to be saved */ +const getModelSavePath = (modelName: string) => + path.join(app.getPath("userData"), "models", modelName); async function downloadModel(saveLocation: string, url: string) { // confirm that the save location exists const saveDir = path.dirname(saveLocation); - if (!existsSync(saveDir)) { - log.info("creating model save dir"); - await fs.mkdir(saveDir, { recursive: true }); - } + await fs.mkdir(saveDir, { recursive: true }); log.info("downloading clip model"); - const resp = await fetch(url); - await writeNodeStream(saveLocation, resp.body); + const res = await net.fetch(url); + if (!res.ok) throw new Error(`Failed to fetch ${url}: HTTP ${res.status}`); + await writeStream(saveLocation, res.body); log.info("clip model downloaded"); } @@ -110,8 +96,7 @@ export async function getClipImageModelPath(type: "ggml" | "onnx") { const localFileSize = (await fs.stat(modelSavePath)).size; if (localFileSize !== IMAGE_MODEL_SIZE_IN_BYTES[type]) { log.info( - "clip image model size mismatch, downloading again got:", - localFileSize, + `clip image model size mismatch, downloading again got: ${localFileSize}`, ); imageModelDownloadInProgress = downloadModel( modelSavePath, @@ -149,8 +134,7 @@ export async function getClipTextModelPath(type: "ggml" | "onnx") { const localFileSize = (await fs.stat(modelSavePath)).size; if (localFileSize !== TEXT_MODEL_SIZE_IN_BYTES[type]) { log.info( - "clip text model size mismatch, downloading again got:", - localFileSize, + `clip text model size mismatch, downloading again got: ${localFileSize}`, ); textModelDownloadInProgress = true; downloadModel(modelSavePath, TEXT_MODEL_DOWNLOAD_URL[type]) @@ -210,7 +194,51 @@ function getTokenizer() { return tokenizer; } -export async function computeImageEmbedding( +export const computeImageEmbedding = async ( + model: Model, + imageData: Uint8Array, +): Promise => { + let tempInputFilePath = null; + try { + tempInputFilePath = await generateTempFilePath(""); + const imageStream = new Response(imageData.buffer).body; + await writeStream(tempInputFilePath, imageStream); + const embedding = await computeImageEmbedding_( + model, + tempInputFilePath, + ); + return embedding; + } catch (err) { + if (isExecError(err)) { + const parsedExecError = parseExecError(err); + throw Error(parsedExecError); + } else { + throw err; + } + } finally { + if (tempInputFilePath) { + await deleteTempFile(tempInputFilePath); + } + } +}; + +const isExecError = (err: any) => { + return err.message.includes("Command failed:"); +}; + +const parseExecError = (err: any) => { + const errMessage = err.message; + if (errMessage.includes("Bad CPU type in executable")) { + return CustomErrors.UNSUPPORTED_PLATFORM( + process.platform, + process.arch, + ); + } else { + return errMessage; + } +}; + +async function computeImageEmbedding_( model: Model, inputFilePath: string, ): Promise { @@ -244,11 +272,7 @@ export async function computeGGMLImageEmbedding( } }); - const escapedCmd = shellescape(cmd); - log.info("running clip command", escapedCmd); - const startTime = Date.now(); - const { stdout } = await execAsync(escapedCmd); - log.info("clip command execution time ", Date.now() - startTime); + const { stdout } = await execAsync(cmd); // parse stdout and return embedding // get the last line of stdout const lines = stdout.split("\n"); @@ -257,7 +281,7 @@ export async function computeGGMLImageEmbedding( const embeddingArray = new Float32Array(embedding); return embeddingArray; } catch (err) { - logErrorSentry(err, "Error in computeGGMLImageEmbedding"); + log.error("Failed to compute GGML image embedding", err); throw err; } } @@ -282,7 +306,7 @@ export async function computeONNXImageEmbedding( const imageEmbedding = results["output"].data; // Float32Array return normalizeEmbedding(imageEmbedding); } catch (err) { - logErrorSentry(err, "Error in computeONNXImageEmbedding"); + log.error("Failed to compute ONNX image embedding", err); throw err; } } @@ -290,6 +314,23 @@ export async function computeONNXImageEmbedding( export async function computeTextEmbedding( model: Model, text: string, +): Promise { + try { + const embedding = computeTextEmbedding_(model, text); + return embedding; + } catch (err) { + if (isExecError(err)) { + const parsedExecError = parseExecError(err); + throw Error(parsedExecError); + } else { + throw err; + } + } +} + +async function computeTextEmbedding_( + model: Model, + text: string, ): Promise { if (model === Model.GGML_CLIP) { return await computeGGMLTextEmbedding(text); @@ -316,11 +357,7 @@ export async function computeGGMLTextEmbedding( } }); - const escapedCmd = shellescape(cmd); - log.info("running clip command", escapedCmd); - const startTime = Date.now(); - const { stdout } = await execAsync(escapedCmd); - log.info("clip command execution time ", Date.now() - startTime); + const { stdout } = await execAsync(cmd); // parse stdout and return embedding // get the last line of stdout const lines = stdout.split("\n"); @@ -332,7 +369,7 @@ export async function computeGGMLTextEmbedding( if (err.message === CustomErrors.MODEL_DOWNLOAD_PENDING) { log.info(CustomErrors.MODEL_DOWNLOAD_PENDING); } else { - logErrorSentry(err, "Error in computeGGMLTextEmbedding"); + log.error("Failed to compute GGML text embedding", err); } throw err; } @@ -369,7 +406,7 @@ export async function computeONNXTextEmbedding( } async function getRGBData(inputFilePath: string) { - const jpegData = await readFile(inputFilePath); + const jpegData = await fs.readFile(inputFilePath); let rawImageData; try { rawImageData = jpeg.decode(jpegData, { diff --git a/desktop/src/services/diskCache.ts b/desktop/src/services/diskCache.ts deleted file mode 100644 index 6861bba9f8..0000000000 --- a/desktop/src/services/diskCache.ts +++ /dev/null @@ -1,98 +0,0 @@ -import crypto from "crypto"; -import path from "path"; -import { existsSync, rename, stat, unlink } from "promise-fs"; -import DiskLRUService from "../services/diskLRU"; -import { LimitedCache } from "../types/cache"; -import { getFileStream, writeStream } from "./fs"; -import { logError } from "./logging"; - -const DEFAULT_CACHE_LIMIT = 1000 * 1000 * 1000; // 1GB - -export class DiskCache implements LimitedCache { - constructor( - private cacheBucketDir: string, - private cacheLimit = DEFAULT_CACHE_LIMIT, - ) {} - - async put(cacheKey: string, response: Response): Promise { - const cachePath = path.join(this.cacheBucketDir, cacheKey); - await writeStream(cachePath, response.body); - DiskLRUService.enforceCacheSizeLimit( - this.cacheBucketDir, - this.cacheLimit, - ); - } - - async match( - cacheKey: string, - { sizeInBytes }: { sizeInBytes?: number } = {}, - ): Promise { - const cachePath = path.join(this.cacheBucketDir, cacheKey); - if (existsSync(cachePath)) { - const fileStats = await stat(cachePath); - if (sizeInBytes && fileStats.size !== sizeInBytes) { - logError( - Error(), - "Cache key exists but size does not match. Deleting cache key.", - ); - unlink(cachePath).catch((e) => { - if (e.code === "ENOENT") return; - logError(e, "Failed to delete cache key"); - }); - return undefined; - } - DiskLRUService.touch(cachePath); - return new Response(await getFileStream(cachePath)); - } else { - // add fallback for old cache keys - const oldCachePath = getOldAssetCachePath( - this.cacheBucketDir, - cacheKey, - ); - if (existsSync(oldCachePath)) { - const fileStats = await stat(oldCachePath); - if (sizeInBytes && fileStats.size !== sizeInBytes) { - logError( - Error(), - "Old cache key exists but size does not match. Deleting cache key.", - ); - unlink(oldCachePath).catch((e) => { - if (e.code === "ENOENT") return; - logError(e, "Failed to delete cache key"); - }); - return undefined; - } - const match = new Response(await getFileStream(oldCachePath)); - void migrateOldCacheKey(oldCachePath, cachePath); - return match; - } - return undefined; - } - } - async delete(cacheKey: string): Promise { - const cachePath = path.join(this.cacheBucketDir, cacheKey); - if (existsSync(cachePath)) { - await unlink(cachePath); - return true; - } else { - return false; - } - } -} - -function getOldAssetCachePath(cacheDir: string, cacheKey: string) { - // hashing the key to prevent illegal filenames - const cacheKeyHash = crypto - .createHash("sha256") - .update(cacheKey) - .digest("hex"); - return path.join(cacheDir, cacheKeyHash); -} - -async function migrateOldCacheKey(oldCacheKey: string, newCacheKey: string) { - try { - await rename(oldCacheKey, newCacheKey); - } catch (e) { - logError(e, "Failed to move cache key to new cache key"); - } -} diff --git a/desktop/src/services/diskLRU.ts b/desktop/src/services/diskLRU.ts deleted file mode 100644 index 44b05c0994..0000000000 --- a/desktop/src/services/diskLRU.ts +++ /dev/null @@ -1,105 +0,0 @@ -import getFolderSize from "get-folder-size"; -import path from "path"; -import { close, open, readdir, stat, unlink, utimes } from "promise-fs"; -import { logError } from "../services/logging"; - -export interface LeastRecentlyUsedResult { - atime: Date; - path: string; -} - -class DiskLRUService { - private isRunning: Promise = null; - private reRun: boolean = false; - - async touch(path: string) { - try { - const time = new Date(); - await utimes(path, time, time); - } catch (err) { - logError(err, "utimes method touch failed"); - try { - await close(await open(path, "w")); - } catch (e) { - logError(e, "open-close method touch failed"); - } - // log and ignore - } - } - - enforceCacheSizeLimit(cacheDir: string, maxSize: number) { - if (!this.isRunning) { - this.isRunning = this.evictLeastRecentlyUsed(cacheDir, maxSize); - this.isRunning.then(() => { - this.isRunning = null; - if (this.reRun) { - this.reRun = false; - this.enforceCacheSizeLimit(cacheDir, maxSize); - } - }); - } else { - this.reRun = true; - } - } - - async evictLeastRecentlyUsed(cacheDir: string, maxSize: number) { - try { - await new Promise((resolve) => { - getFolderSize(cacheDir, async (err, size) => { - if (err) { - throw err; - } - if (size >= maxSize) { - const leastRecentlyUsed = - await this.findLeastRecentlyUsed(cacheDir); - try { - await unlink(leastRecentlyUsed.path); - } catch (e) { - // ENOENT: File not found - // which can be ignored as we are trying to delete the file anyway - if (e.code !== "ENOENT") { - logError( - e, - "Failed to evict least recently used", - ); - } - // ignoring the error, as it would get retried on the next run - } - this.evictLeastRecentlyUsed(cacheDir, maxSize); - } - resolve(null); - }); - }); - } catch (e) { - logError(e, "evictLeastRecentlyUsed failed"); - } - } - - private async findLeastRecentlyUsed( - dir: string, - result?: LeastRecentlyUsedResult, - ): Promise { - result = result || { atime: new Date(), path: "" }; - - const files = await readdir(dir); - for (const file of files) { - const newBase = path.join(dir, file); - const stats = await stat(newBase); - if (stats.isDirectory()) { - result = await this.findLeastRecentlyUsed(newBase, result); - } else { - const { atime } = await stat(newBase); - - if (atime.getTime() < result.atime.getTime()) { - result = { - atime, - path: newBase, - }; - } - } - } - return result; - } -} - -export default new DiskLRUService(); diff --git a/desktop/src/services/ffmpeg.ts b/desktop/src/services/ffmpeg.ts index e0a9157909..ddb3361cff 100644 --- a/desktop/src/services/ffmpeg.ts +++ b/desktop/src/services/ffmpeg.ts @@ -1,26 +1,76 @@ -import log from "electron-log"; import pathToFfmpeg from "ffmpeg-static"; -import { existsSync } from "fs"; -import { readFile, rmSync, writeFile } from "promise-fs"; -import util from "util"; -import { promiseWithTimeout } from "../utils/common"; +import { existsSync } from "node:fs"; +import fs from "node:fs/promises"; +import { CustomErrors } from "../constants/errors"; +import { writeStream } from "../main/fs"; +import log from "../main/log"; +import { execAsync } from "../main/util"; +import { ElectronFile } from "../types/ipc"; import { generateTempFilePath, getTempDirPath } from "../utils/temp"; -import { logErrorSentry } from "./sentry"; -const shellescape = require("any-shell-escape"); - -const execAsync = util.promisify(require("child_process").exec); - -const FFMPEG_EXECUTION_WAIT_TIME = 30 * 1000; const INPUT_PATH_PLACEHOLDER = "INPUT"; const FFMPEG_PLACEHOLDER = "FFMPEG"; const OUTPUT_PATH_PLACEHOLDER = "OUTPUT"; -function getFFmpegStaticPath() { - return pathToFfmpeg.replace("app.asar", "app.asar.unpacked"); +/** + * Run a ffmpeg command + * + * [Note: FFMPEG in Electron] + * + * There is a wasm build of FFMPEG, but that is currently 10-20 times slower + * that the native build. That is slow enough to be unusable for our purposes. + * https://ffmpegwasm.netlify.app/docs/performance + * + * So the alternative is to bundle a ffmpeg binary with our app. e.g. + * + * yarn add fluent-ffmpeg ffmpeg-static ffprobe-static + * + * (we only use ffmpeg-static, the rest are mentioned for completeness' sake). + * + * Interestingly, Electron already bundles an ffmpeg library (it comes from the + * ffmpeg fork maintained by Chromium). + * https://chromium.googlesource.com/chromium/third_party/ffmpeg + * https://stackoverflow.com/questions/53963672/what-version-of-ffmpeg-is-bundled-inside-electron + * + * This can be found in (e.g. on macOS) at + * + * $ file ente.app/Contents/Frameworks/Electron\ Framework.framework/Versions/Current/Libraries/libffmpeg.dylib + * .../libffmpeg.dylib: Mach-O 64-bit dynamically linked shared library arm64 + * + * I'm not sure if our code is supposed to be able to use it, and how. + */ +export async function runFFmpegCmd( + cmd: string[], + inputFile: File | ElectronFile, + outputFileName: string, + dontTimeout?: boolean, +) { + let inputFilePath = null; + let createdTempInputFile = null; + try { + if (!existsSync(inputFile.path)) { + const tempFilePath = await generateTempFilePath(inputFile.name); + await writeStream(tempFilePath, await inputFile.stream()); + inputFilePath = tempFilePath; + createdTempInputFile = true; + } else { + inputFilePath = inputFile.path; + } + const outputFileData = await runFFmpegCmd_( + cmd, + inputFilePath, + outputFileName, + dontTimeout, + ); + return new File([outputFileData], outputFileName); + } finally { + if (createdTempInputFile) { + await deleteTempFile(inputFilePath); + } + } } -export async function runFFmpegCmd( +export async function runFFmpegCmd_( cmd: string[], inputFilePath: string, outputFileName: string, @@ -32,7 +82,7 @@ export async function runFFmpegCmd( cmd = cmd.map((cmdPart) => { if (cmdPart === FFMPEG_PLACEHOLDER) { - return getFFmpegStaticPath(); + return ffmpegBinaryPath(); } else if (cmdPart === INPUT_PATH_PLACEHOLDER) { return inputFilePath; } else if (cmdPart === OUTPUT_PATH_PLACEHOLDER) { @@ -41,54 +91,72 @@ export async function runFFmpegCmd( return cmdPart; } }); - const escapedCmd = shellescape(cmd); - log.info("running ffmpeg command", escapedCmd); - const startTime = Date.now(); + if (dontTimeout) { - await execAsync(escapedCmd); + await execAsync(cmd); } else { - await promiseWithTimeout( - execAsync(escapedCmd), - FFMPEG_EXECUTION_WAIT_TIME, - ); + await promiseWithTimeout(execAsync(cmd), 30 * 1000); } + if (!existsSync(tempOutputFilePath)) { throw new Error("ffmpeg output file not found"); } - log.info( - "ffmpeg command execution time ", - escapedCmd, - Date.now() - startTime, - "ms", - ); - - const outputFile = await readFile(tempOutputFilePath); + const outputFile = await fs.readFile(tempOutputFilePath); return new Uint8Array(outputFile); } catch (e) { - logErrorSentry(e, "ffmpeg run command error"); + log.error("FFMPEG command failed", e); throw e; } finally { - try { - rmSync(tempOutputFilePath, { force: true }); - } catch (e) { - logErrorSentry(e, "failed to remove tempOutputFile"); - } + await deleteTempFile(tempOutputFilePath); } } +/** + * Return the path to the `ffmpeg` binary. + * + * At runtime, the ffmpeg binary is present in a path like (macOS example): + * `ente.app/Contents/Resources/app.asar.unpacked/node_modules/ffmpeg-static/ffmpeg` + */ +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"); +}; + export async function writeTempFile(fileStream: Uint8Array, fileName: string) { const tempFilePath = await generateTempFilePath(fileName); - await writeFile(tempFilePath, fileStream); + await fs.writeFile(tempFilePath, fileStream); return tempFilePath; } export async function deleteTempFile(tempFilePath: string) { const tempDirPath = await getTempDirPath(); - if (!tempFilePath.startsWith(tempDirPath)) { - logErrorSentry( - Error("not a temp file"), - "tried to delete a non temp file", - ); - } - rmSync(tempFilePath, { force: true }); + if (!tempFilePath.startsWith(tempDirPath)) + log.error("Attempting to delete a non-temp file ${tempFilePath}"); + await fs.rm(tempFilePath, { force: true }); } + +const promiseWithTimeout = async ( + request: Promise, + timeout: number, +): Promise => { + const timeoutRef: { + current: NodeJS.Timeout; + } = { current: null }; + const rejectOnTimeout = new Promise((_, reject) => { + timeoutRef.current = setTimeout( + () => reject(Error(CustomErrors.WAIT_TIME_EXCEEDED)), + timeout, + ); + }); + const requestWithTimeOutCancellation = async () => { + const resp = await request; + clearTimeout(timeoutRef.current); + return resp; + }; + return await Promise.race([ + requestWithTimeOutCancellation(), + rejectOnTimeout, + ]); +}; diff --git a/desktop/src/services/fs.ts b/desktop/src/services/fs.ts index bcc49ae5ca..d363177201 100644 --- a/desktop/src/services/fs.ts +++ b/desktop/src/services/fs.ts @@ -1,13 +1,18 @@ -import { existsSync } from "fs"; import StreamZip from "node-stream-zip"; -import path from "path"; -import * as fs from "promise-fs"; -import { Readable } from "stream"; -import { ElectronFile } from "../types"; -import { logError } from "./logging"; +import { existsSync } from "node:fs"; +import fs from "node:fs/promises"; +import path from "node:path"; +import { logError } from "../main/log"; +import { ElectronFile } from "../types/ipc"; const FILE_STREAM_CHUNK_SIZE: number = 4 * 1024 * 1024; +export async function getDirFiles(dirPath: string) { + const files = await getDirFilePaths(dirPath); + const electronFiles = await Promise.all(files.map(getElectronFile)); + return electronFiles; +} + // https://stackoverflow.com/a/63111390 export const getDirFilePaths = async (dirPath: string) => { if (!(await fs.stat(dirPath)).isDirectory()) { @@ -25,16 +30,14 @@ export const getDirFilePaths = async (dirPath: string) => { return files; }; -export const getFileStream = async (filePath: string) => { +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); - // original types were not working correctly - const bytesRead = (await fs.read( - file, + const bytesRead = (await file.read( buff, 0, FILE_STREAM_CHUNK_SIZE, @@ -43,16 +46,16 @@ export const getFileStream = async (filePath: string) => { offset += bytesRead; if (bytesRead === 0) { controller.close(); - await fs.close(file); + await file.close(); } else { controller.enqueue(buff.slice(0, bytesRead)); } } catch (e) { - await fs.close(file); + await file.close(); } }, async cancel() { - await fs.close(file); + await file.close(); }, }); return readableStream; @@ -183,58 +186,3 @@ export const getZipFileStream = async ( }); return readableStream; }; - -export const convertBrowserStreamToNode = ( - fileStream: ReadableStream, -) => { - const reader = fileStream.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 async function writeNodeStream( - filePath: string, - fileStream: NodeJS.ReadableStream, -) { - const writeable = fs.createWriteStream(filePath); - - fileStream.on("error", (error) => { - writeable.destroy(error); // Close the writable stream with an error - }); - - fileStream.pipe(writeable); - - await new Promise((resolve, reject) => { - writeable.on("finish", resolve); - writeable.on("error", async (e) => { - if (existsSync(filePath)) { - await fs.unlink(filePath); - } - reject(e); - }); - }); -} - -export async function writeStream( - filePath: string, - fileStream: ReadableStream, -) { - const readable = convertBrowserStreamToNode(fileStream); - await writeNodeStream(filePath, readable); -} diff --git a/desktop/src/services/imageProcessor.ts b/desktop/src/services/imageProcessor.ts index cb6c7416d6..f6a567f8ca 100644 --- a/desktop/src/services/imageProcessor.ts +++ b/desktop/src/services/imageProcessor.ts @@ -1,18 +1,14 @@ -import { exec } from "child_process"; -import util from "util"; - -import log from "electron-log"; -import { existsSync, rmSync } from "fs"; +import { existsSync } from "fs"; +import fs from "node:fs/promises"; import path from "path"; -import { readFile, writeFile } from "promise-fs"; import { CustomErrors } from "../constants/errors"; -import { isDev } from "../utils/common"; +import { writeStream } from "../main/fs"; +import { logError, logErrorSentry } from "../main/log"; +import { execAsync, isDev } from "../main/util"; +import { ElectronFile } from "../types/ipc"; import { isPlatform } from "../utils/common/platform"; import { generateTempFilePath } from "../utils/temp"; -import { logErrorSentry } from "./sentry"; -const shellescape = require("any-shell-escape"); - -const asyncExec = util.promisify(exec); +import { deleteTempFile } from "./ffmpeg"; const IMAGE_MAGICK_PLACEHOLDER = "IMAGE_MAGICK"; const MAX_DIMENSION_PLACEHOLDER = "MAX_DIMENSION"; @@ -59,10 +55,10 @@ const IMAGEMAGICK_HEIC_CONVERT_COMMAND_TEMPLATE = [ const IMAGE_MAGICK_THUMBNAIL_GENERATE_COMMAND_TEMPLATE = [ IMAGE_MAGICK_PLACEHOLDER, + INPUT_PATH_PLACEHOLDER, "-auto-orient", "-define", `jpeg:size=${SAMPLE_SIZE_PLACEHOLDER}x${SAMPLE_SIZE_PLACEHOLDER}`, - INPUT_PATH_PLACEHOLDER, "-thumbnail", `${MAX_DIMENSION_PLACEHOLDER}x${MAX_DIMENSION_PLACEHOLDER}>`, "-unsharp", @@ -81,6 +77,17 @@ function getImageMagickStaticPath() { export async function convertToJPEG( fileData: Uint8Array, filename: string, +): Promise { + if (isPlatform("windows")) { + throw Error(CustomErrors.WINDOWS_NATIVE_IMAGE_PROCESSING_NOT_SUPPORTED); + } + const convertedFileData = await convertToJPEG_(fileData, filename); + return convertedFileData; +} + +async function convertToJPEG_( + fileData: Uint8Array, + filename: string, ): Promise { let tempInputFilePath: string; let tempOutputFilePath: string; @@ -88,47 +95,30 @@ export async function convertToJPEG( tempInputFilePath = await generateTempFilePath(filename); tempOutputFilePath = await generateTempFilePath("output.jpeg"); - await writeFile(tempInputFilePath, fileData); + await fs.writeFile(tempInputFilePath, fileData); - await runConvertCommand(tempInputFilePath, tempOutputFilePath); - - if (!existsSync(tempOutputFilePath)) { - throw new Error("heic convert output file not found"); - } - const convertedFileData = new Uint8Array( - await readFile(tempOutputFilePath), + await execAsync( + constructConvertCommand(tempInputFilePath, tempOutputFilePath), ); - return convertedFileData; + + return new Uint8Array(await fs.readFile(tempOutputFilePath)); } catch (e) { logErrorSentry(e, "failed to convert heic"); throw e; } finally { try { - rmSync(tempInputFilePath, { force: true }); + await fs.rm(tempInputFilePath, { force: true }); } catch (e) { logErrorSentry(e, "failed to remove tempInputFile"); } try { - rmSync(tempOutputFilePath, { force: true }); + await fs.rm(tempOutputFilePath, { force: true }); } catch (e) { logErrorSentry(e, "failed to remove tempOutputFile"); } } } -async function runConvertCommand( - tempInputFilePath: string, - tempOutputFilePath: string, -) { - const convertCmd = constructConvertCommand( - tempInputFilePath, - tempOutputFilePath, - ); - const escapedCmd = shellescape(convertCmd); - log.info("running convert command: " + escapedCmd); - await asyncExec(escapedCmd); -} - function constructConvertCommand( tempInputFilePath: string, tempOutputFilePath: string, @@ -166,6 +156,44 @@ function constructConvertCommand( } export async function generateImageThumbnail( + inputFile: File | ElectronFile, + maxDimension: number, + maxSize: number, +): Promise { + let inputFilePath = null; + let createdTempInputFile = null; + try { + if (isPlatform("windows")) { + throw Error( + CustomErrors.WINDOWS_NATIVE_IMAGE_PROCESSING_NOT_SUPPORTED, + ); + } + if (!existsSync(inputFile.path)) { + const tempFilePath = await generateTempFilePath(inputFile.name); + await writeStream(tempFilePath, await inputFile.stream()); + inputFilePath = tempFilePath; + createdTempInputFile = true; + } else { + inputFilePath = inputFile.path; + } + const thumbnail = await generateImageThumbnail_( + inputFilePath, + maxDimension, + maxSize, + ); + return thumbnail; + } finally { + if (createdTempInputFile) { + try { + await deleteTempFile(inputFilePath); + } catch (e) { + logError(e, "failed to deleteTempFile"); + } + } + } +} + +async function generateImageThumbnail_( inputFilePath: string, width: number, maxSize: number, @@ -176,17 +204,15 @@ export async function generateImageThumbnail( tempOutputFilePath = await generateTempFilePath("thumb.jpeg"); let thumbnail: Uint8Array; do { - await runThumbnailGenerationCommand( - inputFilePath, - tempOutputFilePath, - width, - quality, + await execAsync( + constructThumbnailGenerationCommand( + inputFilePath, + tempOutputFilePath, + width, + quality, + ), ); - - if (!existsSync(tempOutputFilePath)) { - throw new Error("output thumbnail file not found"); - } - thumbnail = new Uint8Array(await readFile(tempOutputFilePath)); + thumbnail = new Uint8Array(await fs.readFile(tempOutputFilePath)); quality -= 10; } while (thumbnail.length > maxSize && quality > MIN_QUALITY); return thumbnail; @@ -195,30 +221,13 @@ export async function generateImageThumbnail( throw e; } finally { try { - rmSync(tempOutputFilePath, { force: true }); + await fs.rm(tempOutputFilePath, { force: true }); } catch (e) { logErrorSentry(e, "failed to remove tempOutputFile"); } } } -async function runThumbnailGenerationCommand( - inputFilePath: string, - tempOutputFilePath: string, - maxDimension: number, - quality: number, -) { - const thumbnailGenerationCmd: string[] = - constructThumbnailGenerationCommand( - inputFilePath, - tempOutputFilePath, - maxDimension, - quality, - ); - const escapedCmd = shellescape(thumbnailGenerationCmd); - log.info("running thumbnail generation command: " + escapedCmd); - await asyncExec(escapedCmd); -} function constructThumbnailGenerationCommand( inputFilePath: string, tempOutputFilePath: string, diff --git a/desktop/src/services/logging.ts b/desktop/src/services/logging.ts deleted file mode 100644 index bcbacd9f56..0000000000 --- a/desktop/src/services/logging.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { ipcRenderer } from "electron"; -import log from "electron-log"; - -export function logToDisk(logLine: string) { - log.info(logLine); -} - -export function openLogDirectory() { - ipcRenderer.invoke("open-log-dir"); -} - -export function logError(error: Error, message: string, info?: string): void { - ipcRenderer.invoke("log-error", error, message, info); -} diff --git a/desktop/src/services/sentry.ts b/desktop/src/services/sentry.ts deleted file mode 100644 index 4c5573152f..0000000000 --- a/desktop/src/services/sentry.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { isDev } from "../utils/common"; -import { logToDisk } from "./logging"; - -/** Deprecated, but no alternative yet */ -export function logErrorSentry( - error: any, - msg: string, - info?: Record, -) { - logToDisk( - `error: ${error?.name} ${error?.message} ${ - error?.stack - } msg: ${msg} info: ${JSON.stringify(info)}`, - ); - if (isDev) { - console.log(error, { msg, info }); - } -} diff --git a/desktop/src/services/upload.ts b/desktop/src/services/upload.ts index 38a628c255..2fc56fef53 100644 --- a/desktop/src/services/upload.ts +++ b/desktop/src/services/upload.ts @@ -1,7 +1,8 @@ import StreamZip from "node-stream-zip"; import path from "path"; import { uploadStatusStore } from "../stores/upload.store"; -import { ElectronFile, FILE_PATH_KEYS, FILE_PATH_TYPE } from "../types"; +import { ElectronFile, FILE_PATH_TYPE } from "../types/ipc"; +import { FILE_PATH_KEYS } from "../types/main"; import { getValidPaths, getZipFileStream } from "./fs"; export const getSavedFilePaths = (type: FILE_PATH_TYPE) => { diff --git a/desktop/src/services/watch.ts b/desktop/src/services/watch.ts index 8b77469648..3505be744b 100644 --- a/desktop/src/services/watch.ts +++ b/desktop/src/services/watch.ts @@ -1,11 +1,95 @@ +import type { FSWatcher } from "chokidar"; +import ElectronLog from "electron-log"; import { watchStore } from "../stores/watch.store"; -import { WatchStoreType } from "../types"; +import { WatchMapping, WatchStoreType } from "../types/ipc"; +import { isMappingPresent } from "../utils/watch"; + +export const addWatchMapping = async ( + watcher: FSWatcher, + rootFolderName: string, + folderPath: string, + uploadStrategy: number, +) => { + ElectronLog.log(`Adding watch mapping: ${folderPath}`); + const watchMappings = getWatchMappings(); + if (isMappingPresent(watchMappings, folderPath)) { + throw new Error(`Watch mapping already exists`); + } + + watcher.add(folderPath); + + watchMappings.push({ + rootFolderName, + uploadStrategy, + folderPath, + syncedFiles: [], + ignoredFiles: [], + }); + + setWatchMappings(watchMappings); +}; + +export const removeWatchMapping = async ( + watcher: FSWatcher, + folderPath: string, +) => { + let watchMappings = getWatchMappings(); + const watchMapping = watchMappings.find( + (mapping) => mapping.folderPath === folderPath, + ); + + if (!watchMapping) { + throw new Error(`Watch mapping does not exist`); + } + + watcher.unwatch(watchMapping.folderPath); + + watchMappings = watchMappings.filter( + (mapping) => mapping.folderPath !== watchMapping.folderPath, + ); + + setWatchMappings(watchMappings); +}; + +export function updateWatchMappingSyncedFiles( + folderPath: string, + files: WatchMapping["syncedFiles"], +): void { + const watchMappings = getWatchMappings(); + const watchMapping = watchMappings.find( + (mapping) => mapping.folderPath === folderPath, + ); + + if (!watchMapping) { + throw Error(`Watch mapping not found`); + } + + watchMapping.syncedFiles = files; + setWatchMappings(watchMappings); +} + +export function updateWatchMappingIgnoredFiles( + folderPath: string, + files: WatchMapping["ignoredFiles"], +): void { + const watchMappings = getWatchMappings(); + const watchMapping = watchMappings.find( + (mapping) => mapping.folderPath === folderPath, + ); + + if (!watchMapping) { + throw Error(`Watch mapping not found`); + } + + watchMapping.ignoredFiles = files; + setWatchMappings(watchMappings); +} export function getWatchMappings() { const mappings = watchStore.get("mappings") ?? []; return mappings; } -export function setWatchMappings(watchMappings: WatchStoreType["mappings"]) { +function setWatchMappings(watchMappings: WatchStoreType["mappings"]) { watchStore.set("mappings", watchMappings); } diff --git a/desktop/src/stores/keys.store.ts b/desktop/src/stores/keys.store.ts index 943bdb1caa..d112f045a5 100644 --- a/desktop/src/stores/keys.store.ts +++ b/desktop/src/stores/keys.store.ts @@ -1,5 +1,5 @@ import Store, { Schema } from "electron-store"; -import { KeysStoreType } from "../types"; +import type { KeysStoreType } from "../types/main"; const keysStoreSchema: Schema = { AnonymizeUserID: { diff --git a/desktop/src/stores/safeStorage.store.ts b/desktop/src/stores/safeStorage.store.ts index 7822e32e35..809c9623f2 100644 --- a/desktop/src/stores/safeStorage.store.ts +++ b/desktop/src/stores/safeStorage.store.ts @@ -1,5 +1,5 @@ import Store, { Schema } from "electron-store"; -import { SafeStorageStoreType } from "../types"; +import type { SafeStorageStoreType } from "../types/main"; const safeStorageSchema: Schema = { encryptionKey: { diff --git a/desktop/src/stores/upload.store.ts b/desktop/src/stores/upload.store.ts index b918fd2833..5ede1fb99f 100644 --- a/desktop/src/stores/upload.store.ts +++ b/desktop/src/stores/upload.store.ts @@ -1,5 +1,5 @@ import Store, { Schema } from "electron-store"; -import { UploadStoreType } from "../types"; +import type { UploadStoreType } from "../types/main"; const uploadStoreSchema: Schema = { filePaths: { diff --git a/desktop/src/stores/userPreferences.store.ts b/desktop/src/stores/userPreferences.store.ts index 7e17182ad1..9545b1261d 100644 --- a/desktop/src/stores/userPreferences.store.ts +++ b/desktop/src/stores/userPreferences.store.ts @@ -1,5 +1,5 @@ import Store, { Schema } from "electron-store"; -import { UserPreferencesType } from "../types"; +import type { UserPreferencesType } from "../types/main"; const userPreferencesSchema: Schema = { hideDockIcon: { diff --git a/desktop/src/stores/watch.store.ts b/desktop/src/stores/watch.store.ts index 6489ba3e8e..cbc71dde73 100644 --- a/desktop/src/stores/watch.store.ts +++ b/desktop/src/stores/watch.store.ts @@ -1,5 +1,5 @@ import Store, { Schema } from "electron-store"; -import { WatchStoreType } from "../types"; +import { WatchStoreType } from "../types/ipc"; const watchStoreSchema: Schema = { mappings: { diff --git a/desktop/src/types/any-shell-escape.d.ts b/desktop/src/types/any-shell-escape.d.ts new file mode 100644 index 0000000000..4172cdb1ef --- /dev/null +++ b/desktop/src/types/any-shell-escape.d.ts @@ -0,0 +1,25 @@ +/** + * Escape and stringify an array of arguments to be executed on the shell. + * + * @example + * + * const shellescape = require('any-shell-escape'); + * + * const args = ['curl', '-v', '-H', 'Location;', '-H', "User-Agent: FooBar's so-called \"Browser\"", 'http://www.daveeddy.com/?name=dave&age=24']; + * + * const escaped = shellescape(args); + * console.log(escaped); + * + * yields (on POSIX shells): + * + * curl -v -H 'Location;' -H 'User-Agent: FoorBar'"'"'s so-called "Browser"' 'http://www.daveeddy.com/?name=dave&age=24' + * + * or (on Windows): + * + * curl -v -H "Location;" -H "User-Agent: FooBar's so-called ""Browser""" "http://www.daveeddy.com/?name=dave&age=24" +Which is suitable for being executed by the shell. + */ +declare module "any-shell-escape" { + declare const shellescape: (args: readonly string | string[]) => string; + export default shellescape; +} diff --git a/desktop/src/types/autoLauncher.ts b/desktop/src/types/autoLauncher.ts deleted file mode 100644 index 9f82d20142..0000000000 --- a/desktop/src/types/autoLauncher.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface AutoLauncherClient { - isEnabled: () => Promise; - toggleAutoLaunch: () => Promise; - wasAutoLaunched: () => Promise; -} diff --git a/desktop/src/types/cache.ts b/desktop/src/types/cache.ts deleted file mode 100644 index 112716eea0..0000000000 --- a/desktop/src/types/cache.ts +++ /dev/null @@ -1,8 +0,0 @@ -export interface LimitedCache { - match: ( - key: string, - options?: { sizeInBytes?: number }, - ) => Promise; - put: (key: string, data: Response) => Promise; - delete: (key: string) => Promise; -} diff --git a/desktop/src/types/index.ts b/desktop/src/types/ipc.ts similarity index 54% rename from desktop/src/types/index.ts rename to desktop/src/types/ipc.ts index 87f724b58a..93586f29a8 100644 --- a/desktop/src/types/index.ts +++ b/desktop/src/types/ipc.ts @@ -1,3 +1,21 @@ +/** + * @file types that are shared across the IPC boundary with the renderer process + * + * This file is manually kept in sync with the renderer code. + * See [Note: types.ts <-> preload.ts <-> ipc.ts] + */ +/** + * 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; @@ -8,18 +26,6 @@ export interface ElectronFile { arrayBuffer: () => Promise; } -export interface UploadStoreType { - filePaths: string[]; - zipPaths: string[]; - collectionName: string; -} - -export interface KeysStoreType { - AnonymizeUserID: { - id: string; - }; -} - interface WatchMappingSyncedFile { path: string; uploadedFileID: number; @@ -43,32 +49,11 @@ export enum FILE_PATH_TYPE { ZIPS = "zips", } -export const FILE_PATH_KEYS: { - [k in FILE_PATH_TYPE]: keyof UploadStoreType; -} = { - [FILE_PATH_TYPE.ZIPS]: "zipPaths", - [FILE_PATH_TYPE.FILES]: "filePaths", -}; - -export interface SafeStorageStoreType { - encryptionKey: string; -} - -export interface UserPreferencesType { - hideDockIcon: boolean; - skipAppVersion: string; - muteUpdateNotificationVersion: string; -} - export interface AppUpdateInfo { autoUpdatable: boolean; version: string; } -export interface GetFeatureFlagResponse { - desktopCutoffVersion?: string; -} - export enum Model { GGML_CLIP = "ggml-clip", ONNX_CLIP = "onnx-clip", diff --git a/desktop/src/types/main.ts b/desktop/src/types/main.ts new file mode 100644 index 0000000000..98d04ec6e1 --- /dev/null +++ b/desktop/src/types/main.ts @@ -0,0 +1,36 @@ +import { FILE_PATH_TYPE } from "./ipc"; + +export interface AutoLauncherClient { + isEnabled: () => Promise; + toggleAutoLaunch: () => Promise; + wasAutoLaunched: () => Promise; +} + +export interface UploadStoreType { + filePaths: string[]; + zipPaths: string[]; + collectionName: string; +} + +export interface KeysStoreType { + AnonymizeUserID: { + id: string; + }; +} + +export const FILE_PATH_KEYS: { + [k in FILE_PATH_TYPE]: keyof UploadStoreType; +} = { + [FILE_PATH_TYPE.ZIPS]: "zipPaths", + [FILE_PATH_TYPE.FILES]: "filePaths", +}; + +export interface SafeStorageStoreType { + encryptionKey: string; +} + +export interface UserPreferencesType { + hideDockIcon: boolean; + skipAppVersion: string; + muteUpdateNotificationVersion: string; +} diff --git a/desktop/src/utils/common/index.ts b/desktop/src/utils/common/index.ts deleted file mode 100644 index e970dfec41..0000000000 --- a/desktop/src/utils/common/index.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { app } from "electron"; -import { CustomErrors } from "../../constants/errors"; -export const isDev = !app.isPackaged; - -export const promiseWithTimeout = async ( - request: Promise, - timeout: number, -): Promise => { - const timeoutRef: { - current: NodeJS.Timeout; - } = { current: null }; - const rejectOnTimeout = new Promise((_, reject) => { - timeoutRef.current = setTimeout( - () => reject(Error(CustomErrors.WAIT_TIME_EXCEEDED)), - timeout, - ); - }); - const requestWithTimeOutCancellation = async () => { - const resp = await request; - clearTimeout(timeoutRef.current); - return resp; - }; - return await Promise.race([ - requestWithTimeOutCancellation(), - rejectOnTimeout, - ]); -}; diff --git a/desktop/src/utils/cors.ts b/desktop/src/utils/cors.ts deleted file mode 100644 index 25f76211a3..0000000000 --- a/desktop/src/utils/cors.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { BrowserWindow } from "electron"; - -function lowerCaseHeaders(responseHeaders: Record) { - const headers: Record = {}; - for (const key of Object.keys(responseHeaders)) { - headers[key.toLowerCase()] = responseHeaders[key]; - } - return headers; -} - -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, - }); - }, - ); -} diff --git a/desktop/src/utils/createWindow.ts b/desktop/src/utils/createWindow.ts deleted file mode 100644 index 11f05f8c1e..0000000000 --- a/desktop/src/utils/createWindow.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { app, BrowserWindow, nativeImage } from "electron"; -import ElectronLog from "electron-log"; -import * as path from "path"; -import { isAppQuitting, rendererURL } from "../main"; -import autoLauncher from "../services/autoLauncher"; -import { logErrorSentry } from "../services/sentry"; -import { getHideDockIconPreference } from "../services/userPreference"; -import { isDev } from "./common"; -import { isPlatform } from "./common/platform"; - -/** - * Create an return the {@link BrowserWindow} that will form our app's UI. - * - * This window will show the HTML served from {@link rendererURL}. - */ -export const createWindow = async () => { - const appImgPath = isDev - ? "resources/window-icon.png" - : path.join(process.resourcesPath, "window-icon.png"); - const appIcon = nativeImage.createFromPath(appImgPath); - // Create the browser window. - const mainWindow = new BrowserWindow({ - webPreferences: { - preload: path.join(__dirname, "../preload.js"), - }, - icon: appIcon, - show: false, // don't show the main window on load, - }); - const wasAutoLaunched = await autoLauncher.wasAutoLaunched(); - ElectronLog.log("wasAutoLaunched", wasAutoLaunched); - - const splash = new BrowserWindow({ - transparent: true, - show: false, - }); - if (isPlatform("mac") && wasAutoLaunched) { - app.dock.hide(); - } - if (!wasAutoLaunched) { - splash.maximize(); - splash.show(); - } - - if (isDev) { - splash.loadFile(`../resources/splash.html`); - mainWindow.loadURL(rendererURL); - // Open the DevTools. - mainWindow.webContents.openDevTools(); - } else { - splash.loadURL( - `file://${path.join(process.resourcesPath, "splash.html")}`, - ); - mainWindow.loadURL(rendererURL); - } - mainWindow.once("ready-to-show", async () => { - try { - splash.destroy(); - if (!wasAutoLaunched) { - mainWindow.maximize(); - mainWindow.show(); - } - } catch (e) { - // ignore - } - }); - mainWindow.webContents.on("render-process-gone", (event, details) => { - mainWindow.webContents.reload(); - logErrorSentry( - Error("render-process-gone"), - "webContents event render-process-gone", - { details }, - ); - ElectronLog.log("webContents event render-process-gone", details); - }); - mainWindow.webContents.on("unresponsive", () => { - mainWindow.webContents.forcefullyCrashRenderer(); - ElectronLog.log("webContents event unresponsive"); - }); - - setTimeout(() => { - try { - splash.destroy(); - if (!wasAutoLaunched) { - mainWindow.maximize(); - mainWindow.show(); - } - } catch (e) { - // ignore - } - }, 2000); - mainWindow.on("close", function (event) { - if (!isAppQuitting()) { - event.preventDefault(); - mainWindow.hide(); - } - return false; - }); - mainWindow.on("hide", () => { - const shouldHideDockIcon = getHideDockIconPreference(); - if (isPlatform("mac") && shouldHideDockIcon) { - app.dock.hide(); - } - }); - mainWindow.on("show", () => { - if (isPlatform("mac")) { - app.dock.show(); - } - }); - return mainWindow; -}; diff --git a/desktop/src/utils/error.ts b/desktop/src/utils/error.ts deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/desktop/src/utils/events.ts b/desktop/src/utils/events.ts deleted file mode 100644 index 4c7ffe7d8e..0000000000 --- a/desktop/src/utils/events.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { BrowserWindow } from "electron"; - -export function setupAppEventEmitter(mainWindow: BrowserWindow) { - // fire event when mainWindow is in foreground - mainWindow.on("focus", () => { - mainWindow.webContents.send("app-in-foreground"); - }); -} diff --git a/desktop/src/utils/ipcComms.ts b/desktop/src/utils/ipcComms.ts deleted file mode 100644 index 861cec75e8..0000000000 --- a/desktop/src/utils/ipcComms.ts +++ /dev/null @@ -1,168 +0,0 @@ -import chokidar from "chokidar"; -import { - app, - BrowserWindow, - dialog, - ipcMain, - safeStorage, - shell, - Tray, -} from "electron"; -import path from "path"; -import { clearElectronStore } from "../api/electronStore"; -import { - getAppVersion, - muteUpdateNotification, - skipAppUpdate, - updateAndRestart, -} from "../services/appUpdater"; -import { - computeImageEmbedding, - computeTextEmbedding, -} from "../services/clipService"; -import { deleteTempFile, runFFmpegCmd } from "../services/ffmpeg"; -import { getDirFilePaths } from "../services/fs"; -import { - convertToJPEG, - generateImageThumbnail, -} from "../services/imageProcessor"; -import { logErrorSentry } from "../services/sentry"; -import { generateTempFilePath } from "./temp"; - -export default function setupIpcComs( - tray: Tray, - mainWindow: BrowserWindow, - watcher: chokidar.FSWatcher, -): void { - ipcMain.handle("select-dir", 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); - } - }); - - ipcMain.handle("show-upload-files-dialog", async () => { - const files = await dialog.showOpenDialog({ - properties: ["openFile", "multiSelections"], - }); - return files.filePaths; - }); - - ipcMain.handle("show-upload-zip-dialog", async () => { - const files = await dialog.showOpenDialog({ - properties: ["openFile", "multiSelections"], - filters: [{ name: "Zip File", extensions: ["zip"] }], - }); - return files.filePaths; - }); - - ipcMain.handle("show-upload-dirs-dialog", async () => { - const dir = await dialog.showOpenDialog({ - properties: ["openDirectory", "multiSelections"], - }); - - let files: string[] = []; - for (const dirPath of dir.filePaths) { - files = [...files, ...(await getDirFilePaths(dirPath))]; - } - - return files; - }); - - ipcMain.handle("add-watcher", async (_, args: { dir: string }) => { - watcher.add(args.dir); - }); - - ipcMain.handle("remove-watcher", async (_, args: { dir: string }) => { - watcher.unwatch(args.dir); - }); - - ipcMain.handle("log-error", (_, err, msg, info?) => { - logErrorSentry(err, msg, info); - }); - - ipcMain.handle("safeStorage-encrypt", (_, message) => { - return safeStorage.encryptString(message); - }); - - ipcMain.handle("safeStorage-decrypt", (_, message) => { - return safeStorage.decryptString(message); - }); - - ipcMain.on("clear-electron-store", () => { - clearElectronStore(); - }); - - ipcMain.handle("get-path", (_, message) => { - // 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 - return app.getPath(message); - }); - - ipcMain.handle("convert-to-jpeg", (_, fileData, filename) => { - return convertToJPEG(fileData, filename); - }); - - ipcMain.handle("open-log-dir", () => { - shell.openPath(app.getPath("logs")); - }); - - ipcMain.handle("open-dir", (_, dirPath) => { - shell.openPath(path.normalize(dirPath)); - }); - - ipcMain.on("update-and-restart", () => { - updateAndRestart(); - }); - ipcMain.on("skip-app-update", (_, version) => { - skipAppUpdate(version); - }); - - ipcMain.on("mute-update-notification", (_, version) => { - muteUpdateNotification(version); - }); - - ipcMain.handle("get-app-version", () => { - return getAppVersion(); - }); - - ipcMain.handle( - "run-ffmpeg-cmd", - (_, cmd, inputFilePath, outputFileName, dontTimeout) => { - return runFFmpegCmd( - cmd, - inputFilePath, - outputFileName, - dontTimeout, - ); - }, - ); - ipcMain.handle("get-temp-file-path", (_, formatSuffix) => { - return generateTempFilePath(formatSuffix); - }); - ipcMain.handle("remove-temp-file", (_, tempFilePath: string) => { - return deleteTempFile(tempFilePath); - }); - - ipcMain.handle( - "generate-image-thumbnail", - (_, fileData, maxDimension, maxSize) => { - return generateImageThumbnail(fileData, maxDimension, maxSize); - }, - ); - - ipcMain.handle("compute-image-embedding", (_, model, inputFilePath) => { - return computeImageEmbedding(model, inputFilePath); - }); - ipcMain.handle("compute-text-embedding", (_, model, text) => { - return computeTextEmbedding(model, text); - }); -} diff --git a/desktop/src/utils/logging.ts b/desktop/src/utils/logging.ts deleted file mode 100644 index e57382a652..0000000000 --- a/desktop/src/utils/logging.ts +++ /dev/null @@ -1,11 +0,0 @@ -import log from "electron-log"; - -export function setupLogging(isDev?: boolean) { - log.transports.file.fileName = "ente.log"; - log.transports.file.maxSize = 50 * 1024 * 1024; // 50MB; - if (!isDev) { - log.transports.console.level = false; - } - log.transports.file.format = - "[{y}-{m}-{d}T{h}:{i}:{s}{z}] [{level}]{scope} {text}"; -} diff --git a/desktop/src/utils/main.ts b/desktop/src/utils/main.ts deleted file mode 100644 index 569752326e..0000000000 --- a/desktop/src/utils/main.ts +++ /dev/null @@ -1,122 +0,0 @@ -import { app, BrowserWindow, Menu, nativeImage, Tray } from "electron"; -import ElectronLog from "electron-log"; -import os from "os"; -import path from "path"; -import { existsSync } from "promise-fs"; -import util from "util"; -import { rendererURL } from "../main"; -import { setupAutoUpdater } from "../services/appUpdater"; -import autoLauncher from "../services/autoLauncher"; -import { getHideDockIconPreference } from "../services/userPreference"; -import { isDev } from "./common"; -import { isPlatform } from "./common/platform"; -import { buildContextMenu, buildMenuBar } from "./menu"; -const execAsync = util.promisify(require("child_process").exec); - -export async function handleUpdates(mainWindow: BrowserWindow) { - const isInstalledViaBrew = await checkIfInstalledViaBrew(); - if (!isDev && !isInstalledViaBrew) { - setupAutoUpdater(mainWindow); - } -} -export function setupTrayItem(mainWindow: BrowserWindow) { - const iconName = isPlatform("mac") - ? "taskbar-icon-Template.png" - : "taskbar-icon.png"; - const trayImgPath = path.join( - isDev ? "build" : process.resourcesPath, - iconName, - ); - const trayIcon = nativeImage.createFromPath(trayImgPath); - const tray = new Tray(trayIcon); - tray.setToolTip("ente"); - tray.setContextMenu(buildContextMenu(mainWindow)); - return tray; -} - -export function handleDownloads(mainWindow: BrowserWindow) { - mainWindow.webContents.session.on("will-download", (_, item) => { - item.setSavePath( - getUniqueSavePath(item.getFilename(), app.getPath("downloads")), - ); - }); -} - -export function handleExternalLinks(mainWindow: BrowserWindow) { - mainWindow.webContents.setWindowOpenHandler(({ url }) => { - if (!url.startsWith(rendererURL)) { - require("electron").shell.openExternal(url); - return { action: "deny" }; - } else { - return { action: "allow" }; - } - }); -} - -export function getUniqueSavePath(filename: string, directory: string): string { - let uniqueFileSavePath = path.join(directory, filename); - const { name: filenameWithoutExtension, ext: extension } = - path.parse(filename); - let n = 0; - while (existsSync(uniqueFileSavePath)) { - n++; - // filter need to remove undefined extension from the array - // else [`${fileName}`, undefined].join(".") will lead to `${fileName}.` as joined string - const fileNameWithNumberedSuffix = [ - `${filenameWithoutExtension}(${n})`, - extension, - ] - .filter((x) => x) // filters out undefined/null values - .join(""); - uniqueFileSavePath = path.join(directory, fileNameWithNumberedSuffix); - } - return uniqueFileSavePath; -} - -export function setupMacWindowOnDockIconClick() { - app.on("activate", function () { - const windows = BrowserWindow.getAllWindows(); - // we allow only one window - windows[0].show(); - }); -} - -export async function setupMainMenu(mainWindow: BrowserWindow) { - Menu.setApplicationMenu(await buildMenuBar(mainWindow)); -} - -export async function handleDockIconHideOnAutoLaunch() { - const shouldHideDockIcon = getHideDockIconPreference(); - const wasAutoLaunched = await autoLauncher.wasAutoLaunched(); - - if (isPlatform("mac") && shouldHideDockIcon && wasAutoLaunched) { - app.dock.hide(); - } -} - -export function enableSharedArrayBufferSupport() { - app.commandLine.appendSwitch("enable-features", "SharedArrayBuffer"); -} - -export function logSystemInfo() { - const systemVersion = process.getSystemVersion(); - const osName = process.platform; - const osRelease = os.release(); - ElectronLog.info({ osName, osRelease, systemVersion }); - const appVersion = app.getVersion(); - ElectronLog.info({ appVersion }); -} - -export async function checkIfInstalledViaBrew() { - if (!isPlatform("mac")) { - return false; - } - try { - await execAsync("brew list --cask ente"); - ElectronLog.info("ente installed via brew"); - return true; - } catch (e) { - ElectronLog.info("ente not installed via brew"); - return false; - } -} diff --git a/desktop/src/utils/menu.ts b/desktop/src/utils/menu.ts deleted file mode 100644 index c86786ff6f..0000000000 --- a/desktop/src/utils/menu.ts +++ /dev/null @@ -1,218 +0,0 @@ -import { - app, - BrowserWindow, - Menu, - MenuItemConstructorOptions, - shell, -} from "electron"; -import ElectronLog from "electron-log"; -import { setIsAppQuitting } from "../main"; -import { forceCheckForUpdateAndNotify } from "../services/appUpdater"; -import autoLauncher from "../services/autoLauncher"; -import { - getHideDockIconPreference, - setHideDockIconPreference, -} from "../services/userPreference"; -import { isPlatform } from "./common/platform"; - -export function buildContextMenu(mainWindow: BrowserWindow): Menu { - // eslint-disable-next-line camelcase - const contextMenu = Menu.buildFromTemplate([ - { - label: "Open ente", - click: function () { - mainWindow.maximize(); - mainWindow.show(); - }, - }, - { - label: "Quit ente", - click: function () { - ElectronLog.log("user quit the app"); - setIsAppQuitting(true); - app.quit(); - }, - }, - ]); - return contextMenu; -} - -export async function buildMenuBar(mainWindow: BrowserWindow): Promise { - let isAutoLaunchEnabled = await autoLauncher.isEnabled(); - const isMac = isPlatform("mac"); - let shouldHideDockIcon = getHideDockIconPreference(); - const template: MenuItemConstructorOptions[] = [ - { - label: "ente", - submenu: [ - ...((isMac - ? [ - { - label: "About ente", - role: "about", - }, - ] - : []) as MenuItemConstructorOptions[]), - { type: "separator" }, - { - label: "Check for updates...", - click: () => { - forceCheckForUpdateAndNotify(mainWindow); - }, - }, - { - label: "View Changelog", - click: () => { - shell.openExternal( - "https://github.com/ente-io/ente/blob/main/desktop/CHANGELOG.md", - ); - }, - }, - { type: "separator" }, - - { - label: "Preferences", - submenu: [ - { - label: "Open ente on startup", - type: "checkbox", - checked: isAutoLaunchEnabled, - click: () => { - autoLauncher.toggleAutoLaunch(); - isAutoLaunchEnabled = !isAutoLaunchEnabled; - }, - }, - { - label: "Hide dock icon", - type: "checkbox", - checked: shouldHideDockIcon, - click: () => { - setHideDockIconPreference(!shouldHideDockIcon); - shouldHideDockIcon = !shouldHideDockIcon; - }, - }, - ], - }, - - { type: "separator" }, - ...((isMac - ? [ - { - label: "Hide ente", - role: "hide", - }, - { - label: "Hide others", - role: "hideOthers", - }, - ] - : []) as MenuItemConstructorOptions[]), - - { type: "separator" }, - { - label: "Quit ente", - role: "quit", - }, - ], - }, - { - label: "Edit", - submenu: [ - { role: "undo", label: "Undo" }, - { role: "redo", label: "Redo" }, - { type: "separator" }, - { role: "cut", label: "Cut" }, - { role: "copy", label: "Copy" }, - { role: "paste", label: "Paste" }, - ...((isMac - ? [ - { - role: "pasteAndMatchStyle", - label: "Paste and match style", - }, - { role: "delete", label: "Delete" }, - { role: "selectAll", label: "Select all" }, - { type: "separator" }, - { - label: "Speech", - submenu: [ - { - role: "startSpeaking", - label: "start speaking", - }, - { - role: "stopSpeaking", - label: "stop speaking", - }, - ], - }, - ] - : [ - { type: "separator" }, - { role: "selectAll", label: "Select all" }, - ]) as MenuItemConstructorOptions[]), - ], - }, - { - label: "View", - submenu: [ - { role: "reload", label: "Reload" }, - { role: "forceReload", label: "Force reload" }, - { role: "toggleDevTools", label: "Toggle dev tools" }, - { type: "separator" }, - { role: "resetZoom", label: "Reset zoom" }, - { role: "zoomIn", label: "Zoom in" }, - { role: "zoomOut", label: "Zoom out" }, - { type: "separator" }, - { role: "togglefullscreen", label: "Toggle fullscreen" }, - ], - }, - { - label: "Window", - submenu: [ - { role: "close", label: "Close" }, - { role: "minimize", label: "Minimize" }, - ...((isMac - ? [ - { type: "separator" }, - { role: "front", label: "Bring to front" }, - { type: "separator" }, - { role: "window", label: "ente" }, - ] - : []) as MenuItemConstructorOptions[]), - ], - }, - { - label: "Help", - submenu: [ - { - label: "FAQ", - click: () => shell.openExternal("https://ente.io/faq/"), - }, - { type: "separator" }, - { - label: "Support", - click: () => shell.openExternal("mailto:support@ente.io"), - }, - { - label: "Product updates", - click: () => shell.openExternal("https://ente.io/blog/"), - }, - { type: "separator" }, - { - label: "View crash reports", - click: () => { - shell.openPath(app.getPath("crashDumps")); - }, - }, - { - label: "View logs", - click: () => { - shell.openPath(app.getPath("logs")); - }, - }, - ], - }, - ]; - return Menu.buildFromTemplate(template); -} diff --git a/desktop/src/utils/temp.ts b/desktop/src/utils/temp.ts index 91496ce139..489e5cbd47 100644 --- a/desktop/src/utils/temp.ts +++ b/desktop/src/utils/temp.ts @@ -1,17 +1,14 @@ -import { app } from "electron"; +import { app } from "electron/main"; +import { existsSync } from "node:fs"; +import fs from "node:fs/promises"; import path from "path"; -import { existsSync, mkdir } from "promise-fs"; - -const ENTE_TEMP_DIRECTORY = "ente"; const CHARACTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; export async function getTempDirPath() { - const tempDirPath = path.join(app.getPath("temp"), ENTE_TEMP_DIRECTORY); - if (!existsSync(tempDirPath)) { - await mkdir(tempDirPath); - } + const tempDirPath = path.join(app.getPath("temp"), "ente"); + await fs.mkdir(tempDirPath, { recursive: true }); return tempDirPath; } diff --git a/desktop/src/utils/watch.ts b/desktop/src/utils/watch.ts index d8575ebd7c..b5bf130297 100644 --- a/desktop/src/utils/watch.ts +++ b/desktop/src/utils/watch.ts @@ -1,4 +1,4 @@ -import { WatchMapping } from "../types"; +import { WatchMapping } from "../types/ipc"; export function isMappingPresent( watchMappings: WatchMapping[], diff --git a/desktop/tsconfig.json b/desktop/tsconfig.json index 30e62626d5..700ea3fa00 100644 --- a/desktop/tsconfig.json +++ b/desktop/tsconfig.json @@ -9,12 +9,12 @@ /* Recommended target, lib and other settings for code running in the version of Node.js bundled with Electron. - Currently, with Electron 25, this is Node.js 18 - https://www.electronjs.org/blog/electron-25-0 + 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/node18/tsconfig.json", + "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 @@ -48,14 +48,6 @@ /* Emit the generated JS into `app/` */ "outDir": "app", - /* Generate source maps */ - "sourceMap": true, - /* Allow absolute imports starting with src as root */ - "baseUrl": "src", - /* Allow imports of paths from node_modules */ - "paths": { - "*": ["node_modules/*"] - }, /* Temporary overrides to get things to compile with the older config */ "strict": false, diff --git a/desktop/yarn.lock b/desktop/yarn.lock index f23ac47cca..a4cc12cfe4 100644 --- a/desktop/yarn.lock +++ b/desktop/yarn.lock @@ -2,10 +2,10 @@ # yarn lockfile v1 -"7zip-bin@~5.1.1": - version "5.1.1" - resolved "https://registry.yarnpkg.com/7zip-bin/-/7zip-bin-5.1.1.tgz#9274ec7460652f9c632c59addf24efb1684ef876" - integrity sha512-sAP4LldeWNz0lNzmTird3uWfFDWWTeg6V/MsmyyLR9X1idwKBWIgt/ZvinqQldJm3LecKEs1emkbquO6PCiLVQ== +"7zip-bin@~5.2.0": + version "5.2.0" + 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" @@ -13,25 +13,27 @@ integrity sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA== "@babel/code-frame@^7.0.0": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.18.6.tgz#3b25d38c89600baa2dcc219edfa88a74eb2c427a" - integrity sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q== + 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.18.6" + "@babel/highlight" "^7.24.2" + picocolors "^1.0.0" -"@babel/helper-validator-identifier@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.18.6.tgz#9c97e30d31b2b8c72a1d08984f2ca9b574d7a076" - integrity sha512-MmetCkz9ej86nJQV+sFCxoGGrUbU3q02kgLciwkrt9QqEB7cP39oKEY0PakknEO0Gu20SskMRi+AYZ3b1TpN9g== +"@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.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.18.6.tgz#81158601e93e2563795adcbfbdf5d64be3f2ecdf" - integrity sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g== +"@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.18.6" - chalk "^2.0.0" + "@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" @@ -59,9 +61,9 @@ ajv-keywords "^3.4.1" "@electron/asar@^3.2.1": - version "3.2.7" - resolved "https://registry.yarnpkg.com/@electron/asar/-/asar-3.2.7.tgz#bb8117dc6fd0c06a922ae7fb1c0e2d433e35a6e5" - integrity sha512-8FaSCAIiZGYFWyjeevPQt+0e9xCK9YmJ2Rjg5SXgdsXon6cRnU0Yxnbe6CvJbQn26baifur2Y2G5EBayRIsjyg== + version "3.2.9" + resolved "https://registry.yarnpkg.com/@electron/asar/-/asar-3.2.9.tgz#7b3a1fd677b485629f334dd80ced8c85353ba7e7" + integrity sha512-Vu2P3X2gcZ3MY9W7yH72X9+AMXwUQZEJBrsPIbX0JsdllLtoh62/Q8Wg370/DawIEVKOyfD6KtTLo645ezqxUA== dependencies: commander "^5.0.0" glob "^7.1.6" @@ -82,10 +84,10 @@ optionalDependencies: global-agent "^3.0.0" -"@electron/notarize@2.1.0": - version "2.1.0" - resolved "https://registry.yarnpkg.com/@electron/notarize/-/notarize-2.1.0.tgz#76aaec10c8687225e8d0a427cc9df67611c46ff3" - integrity sha512-Q02xem1D0sg4v437xHgmBLxI2iz/fc0D4K7fiVWHa/AnW8o7D751xyKNXgziA6HrTOme9ul1JfWN5ark8WH1xA== +"@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== dependencies: debug "^4.1.1" fs-extra "^9.0.1" @@ -103,10 +105,10 @@ minimist "^1.2.6" plist "^3.0.5" -"@electron/universal@1.4.1": - version "1.4.1" - resolved "https://registry.yarnpkg.com/@electron/universal/-/universal-1.4.1.tgz#3fbda2a5ed9ff9f3304c8e8316b94c1e3a7b3785" - integrity sha512-lE/U3UNw1YHuowNbTmKNs9UlS3En3cPgwM5MI+agIgr/B1hSze9NdOP0qn7boZaI9Lph8IDv3/24g9IxnJP7aQ== +"@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== dependencies: "@electron/asar" "^3.2.1" "@malept/cross-spawn-promise" "^1.1.0" @@ -167,6 +169,18 @@ 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" @@ -205,10 +219,10 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" -"@octetstream/promisify@2.0.2": - version "2.0.2" - resolved "https://registry.yarnpkg.com/@octetstream/promisify/-/promisify-2.0.2.tgz#29ac3bd7aefba646db670227f895d812c1a19615" - integrity sha512-7XHoRB61hxsz8lBQrjC1tq/3OEIgpvGWg6DKAdwi7WRzruwkmsdwmOoUXbU4Dtd4RSOMDwed0SkP3y8UlMt1Bg== +"@pkgjs/parseargs@^0.11.0": + version "0.11.0" + resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" + integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== "@pkgr/core@^0.1.0": version "0.1.1" @@ -232,10 +246,10 @@ resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf" integrity sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A== -"@types/auto-launch@^5.0.2": - version "5.0.2" - resolved "https://registry.yarnpkg.com/@types/auto-launch/-/auto-launch-5.0.2.tgz#4970f01e5dd27572489b7fe77590204a19f86bd0" - integrity sha512-b03X09+GCM9t6AUECpwA2gUPYs8s5tJHFJw92sK8EiJ7G4QNbsHmXV7nfCfP6G6ivtm230vi4oNfe8AzRgzxMQ== +"@types/auto-launch@^5.0": + version "5.0.5" + resolved "https://registry.yarnpkg.com/@types/auto-launch/-/auto-launch-5.0.5.tgz#439ed36aaaea501e2e2cfbddd8a20c366c34863b" + integrity sha512-/nGvQZSzM/pvCMCh4Gt2kIeiUmOP/cKGJbjlInI+A+5MoV/7XmT56DJ6EU8bqc3+ItxEe4UC2GVspmPzcCc8cg== "@types/cacheable-request@^6.0.1": version "6.0.3" @@ -248,16 +262,16 @@ "@types/responselike" "^1.0.0" "@types/debug@^4.1.6": - version "4.1.7" - resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.7.tgz#7cc0ea761509124709b8b2d1090d8f6c17aadb82" - integrity sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg== + version "4.1.12" + resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.12.tgz#a155f21690871953410df4b6b6f53187f0500917" + integrity sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ== dependencies: "@types/ms" "*" -"@types/ffmpeg-static@^3.0.1": - version "3.0.1" - resolved "https://registry.yarnpkg.com/@types/ffmpeg-static/-/ffmpeg-static-3.0.1.tgz#1003f003624bcd2f569b56185a62dcbacd935c39" - integrity sha512-hEJdQMv/g1olk9qTiWqh23BfbKsDKE6Tc7DilNJWF1MgZsU9fYOPKrgQ448vfT7aP2Yt5re9vgJDVv9TXEoTyQ== +"@types/ffmpeg-static@^3.0": + version "3.0.3" + resolved "https://registry.yarnpkg.com/@types/ffmpeg-static/-/ffmpeg-static-3.0.3.tgz#605358ac6304507a75c2fd5fd861534837b19e2f" + integrity sha512-wmjANN0CiYs5clQESK+xE6plet0y9ndqaNBdQx4IIw7ZbPBMQw+14Lq4ky2WqMqGlpFJ9ZUxU0O43TvVZziyyA== "@types/fs-extra@9.0.13", "@types/fs-extra@^9.0.11": version "9.0.13" @@ -266,15 +280,10 @@ dependencies: "@types/node" "*" -"@types/get-folder-size@^2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@types/get-folder-size/-/get-folder-size-2.0.0.tgz#acbb5bf5999410c375b2739863a9d2f9483fabf6" - integrity sha512-6VKKrDB20E/6ovi2Pfpy9Pcz8Me1ue/tReaZrwrz9mfVdsr6WAMiDZ+F1oAAcss4U5n2k673i1leDIx2aEBDFQ== - "@types/http-cache-semantics@*": - version "4.0.2" - resolved "https://registry.yarnpkg.com/@types/http-cache-semantics/-/http-cache-semantics-4.0.2.tgz#abe102d06ccda1efdf0ed98c10ccf7f36a785a41" - integrity sha512-FD+nQWA2zJjh4L9+pFXqWOi0Hs1ryBCfI+985NjluQ1p8EYtoLvjLOKidXBtZ4/IcxDX4o8/E8qDS3540tNliw== + version "4.0.4" + resolved "https://registry.yarnpkg.com/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz#b979ebad3919799c979b17c72621c0bc0a31c6c4" + integrity sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA== "@types/json-schema@^7.0.12": version "7.0.15" @@ -289,92 +298,69 @@ "@types/node" "*" "@types/ms@*": - version "0.7.31" - resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.31.tgz#31b7ca6407128a3d2bbc27fe2d21b345397f6197" - integrity sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA== + version "0.7.34" + resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.34.tgz#10964ba0dee6ac4cd462e2795b6bebd407303433" + integrity sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g== -"@types/node-fetch@^2.6.2": - version "2.6.2" - resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.2.tgz#d1a9c5fd049d9415dce61571557104dec3ec81da" - integrity sha512-DHqhlq5jeESLy19TYhLakJ07kNumXWjcDdxXsLUMJZ6ue8VZJj4kLPQVE/2mdHh3xZziNF1xppu5lwmS53HR+A== +"@types/node@*", "@types/node@^20.9.0": + version "20.11.30" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.30.tgz#9c33467fc23167a347e73834f788f4b9f399d66f" + integrity sha512-dHM6ZxwlmuZaRmUPfv1p+KrdD1Dci04FbdEm/9wEMouFqxYoFl5aMkt0VMAUtYRQDyYvD41WJLukhq/ha3YuTw== dependencies: - "@types/node" "*" - form-data "^3.0.0" - -"@types/node@*": - version "18.0.3" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.0.3.tgz#463fc47f13ec0688a33aec75d078a0541a447199" - integrity sha512-HzNRZtp4eepNitP+BD6k2L6DROIDG4Q0fm4x+dwfsr6LGmROENnok75VGw40628xf+iR24WeMFcHuuBDUAzzsQ== + undici-types "~5.26.4" "@types/node@^10.0.3": version "10.17.60" resolved "https://registry.yarnpkg.com/@types/node/-/node-10.17.60.tgz#35f3d6213daed95da7f0f73e75bcc6980e90597b" integrity sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw== -"@types/node@^18.11.18": - version "18.18.0" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.18.0.tgz#bd19d5133a6e5e2d0152ec079ac27c120e7f1763" - integrity sha512-3xA4X31gHT1F1l38ATDIL9GpRLdwVhnEFC8Uikv5ZLlXATwrCYyPq7ZWHxzxc3J/30SUiwiYT+bQe0/XvKlWbw== - "@types/normalize-package-data@^2.4.0": - version "2.4.1" - resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz#d3357479a0fdfdd5907fe67e17e0a85c906e1301" - integrity sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw== + 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.2" - resolved "https://registry.yarnpkg.com/@types/plist/-/plist-3.0.2.tgz#61b3727bba0f5c462fe333542534a0c3e19ccb01" - integrity sha512-ULqvZNGMv0zRFvqn8/4LSPtnmN4MfhlPNtJCTpKuIIxGVGZ2rYWzFXrvEBoh9CVyqSE7D6YFRJ1hydLHI6kbWw== + version "3.0.5" + resolved "https://registry.yarnpkg.com/@types/plist/-/plist-3.0.5.tgz#9a0c49c0f9886c8c8696a7904dd703f6284036e0" + integrity sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA== dependencies: "@types/node" "*" xmlbuilder ">=11.0.1" -"@types/promise-fs@^2.1.1": - version "2.1.2" - resolved "https://registry.yarnpkg.com/@types/promise-fs/-/promise-fs-2.1.2.tgz#7ef6ab00c7fbc68081e34e560d2f008d3dd27fd2" - integrity sha512-s3YON1LmplAUVrvTT2d1I0m2Rk0hSgc/1l5/krnU96YpP4NG9VEN/qopaFv8yk5a2Z+AgYzafS1LCP+kQH0MYw== - dependencies: - "@types/node" "*" - "@types/responselike@^1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@types/responselike/-/responselike-1.0.0.tgz#251f4fe7d154d2bad125abe1b429b23afd262e29" - integrity sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA== + version "1.0.3" + resolved "https://registry.yarnpkg.com/@types/responselike/-/responselike-1.0.3.tgz#cc29706f0a397cfe6df89debfe4bf5cea159db50" + integrity sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw== dependencies: "@types/node" "*" -"@types/semver@^7.3.6": - version "7.3.10" - resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.10.tgz#5f19ee40cbeff87d916eedc8c2bfe2305d957f73" - integrity sha512-zsv3fsC7S84NN6nPK06u79oWgrPVd0NvOyqgghV1haPaFcVxIrP4DLomRwGAXk0ui4HZA7mOcSFL98sMVW9viw== - "@types/semver@^7.5.0": version "7.5.8" resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.8.tgz#8268a8c57a3e4abd25c165ecd36237db7948a55e" integrity sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ== "@types/verror@^1.10.3": - version "1.10.5" - resolved "https://registry.yarnpkg.com/@types/verror/-/verror-1.10.5.tgz#2a1413aded46e67a1fe2386800e291123ed75eb1" - integrity sha512-9UjMCHK5GPgQRoNbqdLIAvAy0EInuiqbW0PBMtVP6B5B2HQJlvoJHM+KodPZMEjOa5VkSc+5LH7xy+cUzQdmHw== + version "1.10.10" + resolved "https://registry.yarnpkg.com/@types/verror/-/verror-1.10.10.tgz#d5a4b56abac169bfbc8b23d291363a682e6fa087" + integrity sha512-l4MM0Jppn18hb9xmM6wwD1uTdShpf9Pn80aXTStnK1C94gtPvJcV2FrDmbOQUAQfJ1cKZHktkQUDwEqaAKXMMg== "@types/yauzl@^2.9.1": - version "2.10.0" - resolved "https://registry.yarnpkg.com/@types/yauzl/-/yauzl-2.10.0.tgz#b3248295276cf8c6f153ebe6a9aba0c988cb2599" - integrity sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw== + version "2.10.3" + resolved "https://registry.yarnpkg.com/@types/yauzl/-/yauzl-2.10.3.tgz#e9b2808b4f109504a03cda958259876f61017999" + integrity sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q== dependencies: "@types/node" "*" "@typescript-eslint/eslint-plugin@^7": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.2.0.tgz#5a5fcad1a7baed85c10080d71ad901f98c38d5b7" - integrity sha512-mdekAHOqS9UjlmyF/LSs6AIEvfceV749GFxoBAjwAv0nkevfKHWQFDMcBZWUiIC5ft6ePWivXoS36aKQ0Cy3sw== + version "7.4.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.4.0.tgz#de61c3083842fc6ac889d2fc83c9a96b55ab8328" + integrity sha512-yHMQ/oFaM7HZdVrVm/M2WHaNPgyuJH4WelkSVEWSSsir34kxW2kDJCxlXRhhGWEsMN0WAW/vLpKfKVcm8k+MPw== dependencies: "@eslint-community/regexpp" "^4.5.1" - "@typescript-eslint/scope-manager" "7.2.0" - "@typescript-eslint/type-utils" "7.2.0" - "@typescript-eslint/utils" "7.2.0" - "@typescript-eslint/visitor-keys" "7.2.0" + "@typescript-eslint/scope-manager" "7.4.0" + "@typescript-eslint/type-utils" "7.4.0" + "@typescript-eslint/utils" "7.4.0" + "@typescript-eslint/visitor-keys" "7.4.0" debug "^4.3.4" graphemer "^1.4.0" ignore "^5.2.4" @@ -383,46 +369,46 @@ ts-api-utils "^1.0.1" "@typescript-eslint/parser@^7": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-7.2.0.tgz#44356312aea8852a3a82deebdacd52ba614ec07a" - integrity sha512-5FKsVcHTk6TafQKQbuIVkXq58Fnbkd2wDL4LB7AURN7RUOu1utVP+G8+6u3ZhEroW3DF6hyo3ZEXxgKgp4KeCg== + version "7.4.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-7.4.0.tgz#540f4321de1e52b886c0fa68628af1459954c1f1" + integrity sha512-ZvKHxHLusweEUVwrGRXXUVzFgnWhigo4JurEj0dGF1tbcGh6buL+ejDdjxOQxv6ytcY1uhun1p2sm8iWStlgLQ== dependencies: - "@typescript-eslint/scope-manager" "7.2.0" - "@typescript-eslint/types" "7.2.0" - "@typescript-eslint/typescript-estree" "7.2.0" - "@typescript-eslint/visitor-keys" "7.2.0" + "@typescript-eslint/scope-manager" "7.4.0" + "@typescript-eslint/types" "7.4.0" + "@typescript-eslint/typescript-estree" "7.4.0" + "@typescript-eslint/visitor-keys" "7.4.0" debug "^4.3.4" -"@typescript-eslint/scope-manager@7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-7.2.0.tgz#cfb437b09a84f95a0930a76b066e89e35d94e3da" - integrity sha512-Qh976RbQM/fYtjx9hs4XkayYujB/aPwglw2choHmf3zBjB4qOywWSdt9+KLRdHubGcoSwBnXUH2sR3hkyaERRg== +"@typescript-eslint/scope-manager@7.4.0": + version "7.4.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-7.4.0.tgz#acfc69261f10ece7bf7ece1734f1713392c3655f" + integrity sha512-68VqENG5HK27ypafqLVs8qO+RkNc7TezCduYrx8YJpXq2QGZ30vmNZGJJJC48+MVn4G2dCV8m5ZTVnzRexTVtw== dependencies: - "@typescript-eslint/types" "7.2.0" - "@typescript-eslint/visitor-keys" "7.2.0" + "@typescript-eslint/types" "7.4.0" + "@typescript-eslint/visitor-keys" "7.4.0" -"@typescript-eslint/type-utils@7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-7.2.0.tgz#7be5c30e9b4d49971b79095a1181324ef6089a19" - integrity sha512-xHi51adBHo9O9330J8GQYQwrKBqbIPJGZZVQTHHmy200hvkLZFWJIFtAG/7IYTWUyun6DE6w5InDReePJYJlJA== +"@typescript-eslint/type-utils@7.4.0": + version "7.4.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-7.4.0.tgz#cfcaab21bcca441c57da5d3a1153555e39028cbd" + integrity sha512-247ETeHgr9WTRMqHbbQdzwzhuyaJ8dPTuyuUEMANqzMRB1rj/9qFIuIXK7l0FX9i9FXbHeBQl/4uz6mYuCE7Aw== dependencies: - "@typescript-eslint/typescript-estree" "7.2.0" - "@typescript-eslint/utils" "7.2.0" + "@typescript-eslint/typescript-estree" "7.4.0" + "@typescript-eslint/utils" "7.4.0" debug "^4.3.4" ts-api-utils "^1.0.1" -"@typescript-eslint/types@7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-7.2.0.tgz#0feb685f16de320e8520f13cca30779c8b7c403f" - integrity sha512-XFtUHPI/abFhm4cbCDc5Ykc8npOKBSJePY3a3s+lwumt7XWJuzP5cZcfZ610MIPHjQjNsOLlYK8ASPaNG8UiyA== +"@typescript-eslint/types@7.4.0": + version "7.4.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-7.4.0.tgz#ee9dafa75c99eaee49de6dcc9348b45d354419b6" + integrity sha512-mjQopsbffzJskos5B4HmbsadSJQWaRK0UxqQ7GuNA9Ga4bEKeiO6b2DnB6cM6bpc8lemaPseh0H9B/wyg+J7rw== -"@typescript-eslint/typescript-estree@7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-7.2.0.tgz#5beda2876c4137f8440c5a84b4f0370828682556" - integrity sha512-cyxS5WQQCoBwSakpMrvMXuMDEbhOo9bNHHrNcEWis6XHx6KF518tkF1wBvKIn/tpq5ZpUYK7Bdklu8qY0MsFIA== +"@typescript-eslint/typescript-estree@7.4.0": + version "7.4.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-7.4.0.tgz#12dbcb4624d952f72c10a9f4431284fca24624f4" + integrity sha512-A99j5AYoME/UBQ1ucEbbMEmGkN7SE0BvZFreSnTd1luq7yulcHdyGamZKizU7canpGDWGJ+Q6ZA9SyQobipePg== dependencies: - "@typescript-eslint/types" "7.2.0" - "@typescript-eslint/visitor-keys" "7.2.0" + "@typescript-eslint/types" "7.4.0" + "@typescript-eslint/visitor-keys" "7.4.0" debug "^4.3.4" globby "^11.1.0" is-glob "^4.0.3" @@ -430,25 +416,25 @@ semver "^7.5.4" ts-api-utils "^1.0.1" -"@typescript-eslint/utils@7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-7.2.0.tgz#fc8164be2f2a7068debb4556881acddbf0b7ce2a" - integrity sha512-YfHpnMAGb1Eekpm3XRK8hcMwGLGsnT6L+7b2XyRv6ouDuJU1tZir1GS2i0+VXRatMwSI1/UfcyPe53ADkU+IuA== +"@typescript-eslint/utils@7.4.0": + version "7.4.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-7.4.0.tgz#d889a0630cab88bddedaf7c845c64a00576257bd" + integrity sha512-NQt9QLM4Tt8qrlBVY9lkMYzfYtNz8/6qwZg8pI3cMGlPnj6mOpRxxAm7BMJN9K0AiY+1BwJ5lVC650YJqYOuNg== dependencies: "@eslint-community/eslint-utils" "^4.4.0" "@types/json-schema" "^7.0.12" "@types/semver" "^7.5.0" - "@typescript-eslint/scope-manager" "7.2.0" - "@typescript-eslint/types" "7.2.0" - "@typescript-eslint/typescript-estree" "7.2.0" + "@typescript-eslint/scope-manager" "7.4.0" + "@typescript-eslint/types" "7.4.0" + "@typescript-eslint/typescript-estree" "7.4.0" semver "^7.5.4" -"@typescript-eslint/visitor-keys@7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-7.2.0.tgz#5035f177752538a5750cca1af6044b633610bf9e" - integrity sha512-c6EIQRHhcpl6+tO8EMR+kjkkV+ugUNXOmeASA1rlzkd8EPIriavpWoiEz1HR/VLhbVIdhqnV6E7JZm00cBDx2A== +"@typescript-eslint/visitor-keys@7.4.0": + version "7.4.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-7.4.0.tgz#0c8ff2c1f8a6fe8d7d1a57ebbd4a638e86a60a94" + integrity sha512-0zkC7YM0iX5Y41homUUeW1CHtZR01K3ybjM1l6QczoMuay0XKtrb93kv95AxUGwdjGr64nNqnOCwmEl616N8CA== dependencies: - "@typescript-eslint/types" "7.2.0" + "@typescript-eslint/types" "7.4.0" eslint-visitor-keys "^3.4.1" "@ungap/structured-clone@^1.2.0": @@ -501,9 +487,9 @@ 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.11.0" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.11.0.tgz#977e91dd96ca669f54a11e23e378e33b884a565f" - integrity sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg== + version "8.12.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.12.0.tgz#d1a0527323e22f53562c567c00991577dfbe19d1" + integrity sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA== dependencies: fast-deep-equal "^3.1.1" json-schema-traverse "^1.0.0" @@ -515,6 +501,11 @@ ansi-regex@^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" @@ -529,15 +520,20 @@ ansi-styles@^4.0.0, ansi-styles@^4.1.0: dependencies: color-convert "^2.0.1" -any-shell-escape@^0.1.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" integrity sha512-36j4l5HVkboyRhIWgtMh1I9i8LTdFqVwDEHy1cp+QioJyKgAUG40X0W8s7jakWRta/Sjvm8mUG1fU6Tj8mWagQ== anymatch@~3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716" - integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg== + version "3.1.3" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" + integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== dependencies: normalize-path "^3.0.0" picomatch "^2.0.4" @@ -547,26 +543,25 @@ 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.6.4: - version "24.6.4" - resolved "https://registry.yarnpkg.com/app-builder-lib/-/app-builder-lib-24.6.4.tgz#5bf77dd89d3ee557bc615b9ddfaf383f3e51577b" - integrity sha512-m9931WXb83teb32N0rKg+ulbn6+Hl8NV5SUpVDOVz9MWOXfhV6AQtTdftf51zJJvCQnQugGtSqoLvgw6mdF/Rg== +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== dependencies: - "7zip-bin" "~5.1.1" "@develar/schema-utils" "~2.6.5" - "@electron/notarize" "2.1.0" + "@electron/notarize" "2.2.1" "@electron/osx-sign" "1.0.5" - "@electron/universal" "1.4.1" + "@electron/universal" "1.5.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.5.0" - builder-util-runtime "9.2.1" + builder-util "24.13.1" + builder-util-runtime "9.2.4" chromium-pickle-js "^0.2.0" debug "^4.3.4" ejs "^3.1.8" - electron-publish "24.5.0" + electron-publish "24.13.1" form-data "^4.0.0" fs-extra "^10.1.0" hosted-git-info "^4.1.0" @@ -619,9 +614,9 @@ async-exit-hook@^2.0.1: integrity sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw== async@^3.2.3: - version "3.2.4" - resolved "https://registry.yarnpkg.com/async/-/async-3.2.4.tgz#2d22e00f8cddeb5fde5dd33522b56d1cf569a81c" - integrity sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ== + version "3.2.5" + resolved "https://registry.yarnpkg.com/async/-/async-3.2.5.tgz#ebd52a8fdaf7a2289a24df399f8d8485c8a46b66" + integrity sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg== asynckit@^0.4.0: version "0.4.0" @@ -638,10 +633,10 @@ atomically@^1.7.0: resolved "https://registry.yarnpkg.com/atomically/-/atomically-1.7.0.tgz#c07a0458432ea6dbc9a3506fffa424b48bccaafe" integrity sha512-Xcz9l0z7y9yQ9rdDaxlmaI4uJHf/T8g9hOEzJcsEqX2SjCj4J20uK7+ldkDHMbpJDK76wF7xEIgxc/vSlsfw5w== -auto-launch@^5.0.5: - version "5.0.5" - resolved "https://registry.yarnpkg.com/auto-launch/-/auto-launch-5.0.5.tgz#d14bd002b1ef642f85e991a6195ff5300c8ad3c0" - integrity sha512-ppdF4mihhYzMYLuCcx9H/c5TUOCev8uM7en53zWVQhyYAJrurd2bFZx3qQVeJKF2jrc7rsPRNN5cD+i23l6PdA== +auto-launch@^5.0: + version "5.0.6" + resolved "https://registry.yarnpkg.com/auto-launch/-/auto-launch-5.0.6.tgz#ccc238ddc07b2fa84e96a1bc2fd11b581a20cb2d" + integrity sha512-OgxiAm4q9EBf9EeXdPBiVNENaWE3jUZofwrhAkWjHDYGezu1k3FRZHU8V2FBxGuSJOHzKmTJEd0G7L7/0xDGFA== dependencies: applescript "^1.0.0" mkdirp "^0.5.1" @@ -660,9 +655,9 @@ base64-js@^1.3.1, base64-js@^1.5.1: integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== binary-extensions@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" - integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== + version "2.3.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522" + integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw== bluebird-lst@^1.0.9: version "1.0.9" @@ -726,32 +721,32 @@ buffer@^5.1.0: base64-js "^1.3.1" ieee754 "^1.1.13" -builder-util-runtime@8.9.2: - version "8.9.2" - resolved "https://registry.yarnpkg.com/builder-util-runtime/-/builder-util-runtime-8.9.2.tgz#a9669ae5b5dcabfe411ded26678e7ae997246c28" - integrity sha512-rhuKm5vh7E0aAmT6i8aoSfEjxzdYEFX7zDApK+eNgOhjofnWb74d9SRJv0H/8nsgOkos0TZ4zxW0P8J4N7xQ2A== - dependencies: - debug "^4.3.2" - sax "^1.2.4" - -builder-util-runtime@9.2.1: - version "9.2.1" - resolved "https://registry.yarnpkg.com/builder-util-runtime/-/builder-util-runtime-9.2.1.tgz#3184dcdf7ed6c47afb8df733813224ced4f624fd" - integrity sha512-2rLv/uQD2x+dJ0J3xtsmI12AlRyk7p45TEbE/6o/fbb633e/S3pPgm+ct+JHsoY7r39dKHnGEFk/AASRFdnXmA== +builder-util-runtime@9.2.3: + version "9.2.3" + resolved "https://registry.yarnpkg.com/builder-util-runtime/-/builder-util-runtime-9.2.3.tgz#0a82c7aca8eadef46d67b353c638f052c206b83c" + integrity sha512-FGhkqXdFFZ5dNC4C+yuQB9ak311rpGAw+/ASz8ZdxwODCv1GGMWgLDeofRkdi0F3VCHQEWy/aXcJQozx2nOPiw== dependencies: debug "^4.3.4" sax "^1.2.4" -builder-util@24.5.0: - version "24.5.0" - resolved "https://registry.yarnpkg.com/builder-util/-/builder-util-24.5.0.tgz#8683c9a7a1c5c9f9a4c4d2789ecca0e47dddd3f9" - integrity sha512-STnBmZN/M5vGcv01u/K8l+H+kplTaq4PAIn3yeuufUKSpcdro0DhJWxPI81k5XcNfC//bjM3+n9nr8F9uV4uAQ== +builder-util-runtime@9.2.4: + version "9.2.4" + resolved "https://registry.yarnpkg.com/builder-util-runtime/-/builder-util-runtime-9.2.4.tgz#13cd1763da621e53458739a1e63f7fcba673c42a" + integrity sha512-upp+biKpN/XZMLim7aguUyW8s0FUpDvOtK6sbanMFDAMBzpHDqdhgVYm6zc9HJ6nWo7u2Lxk60i2M6Jd3aiNrA== dependencies: - "7zip-bin" "~5.1.1" + 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== + 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.1" + builder-util-runtime "9.2.4" chalk "^4.1.2" cross-spawn "^7.0.3" debug "^4.3.4" @@ -792,7 +787,7 @@ caseless@^0.12.0: resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" integrity sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw== -chalk@^2.0.0: +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== @@ -809,10 +804,10 @@ chalk@^4.0.0, chalk@^4.0.2, chalk@^4.1.2: ansi-styles "^4.1.0" supports-color "^7.1.0" -chokidar@^3.5.2, chokidar@^3.5.3: - version "3.5.3" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" - integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== +chokidar@^3.6: + version "3.6.0" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" + integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== dependencies: anymatch "~3.1.2" braces "~3.0.2" @@ -835,9 +830,9 @@ chromium-pickle-js@^0.2.0: integrity sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw== ci-info@^3.2.0: - version "3.3.2" - resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.3.2.tgz#6d2967ffa407466481c6c90b6e16b3098f080128" - integrity sha512-xmDt/QIAdeZ9+nfdPsaBCpMvHNLFiLdjj59qjqn+6iPe6YmHGQ35sBnQ8uslRBXFmXkiZQOJRjvQeoGppoTjjg== + version "3.9.0" + resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.9.0.tgz#4279a62028a7b1f262f3473fc9605f5e218c59b4" + integrity sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ== cli-truncate@^2.1.0: version "2.1.0" @@ -857,9 +852,9 @@ cliui@^8.0.1: wrap-ansi "^7.0.0" clone-response@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/clone-response/-/clone-response-1.0.2.tgz#d1dc973920314df67fbeb94223b4ee350239e96b" - integrity sha512-yjLXh88P599UOyPTFX0POsd7WxnbsVsGohcwzHOLspIhhpalPw1BcqED8NblyZLKcGrL8dTgMlcaZxV2jAD41Q== + version "1.0.3" + resolved "https://registry.yarnpkg.com/clone-response/-/clone-response-1.0.3.tgz#af2032aa47816399cf5f0a1d0db902f517abb8c3" + integrity sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA== dependencies: mimic-response "^1.0.0" @@ -904,7 +899,7 @@ compare-version@^0.1.2: resolved "https://registry.yarnpkg.com/compare-version/-/compare-version-0.1.2.tgz#0162ec2d9351f5ddd59a9202cba935366a725080" integrity sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A== -compare-versions@^6.1.0: +compare-versions@^6.1: version "6.1.0" resolved "https://registry.yarnpkg.com/compare-versions/-/compare-versions-6.1.0.tgz#3f2131e3ae93577df111dba133e6db876ffe127a" integrity sha512-LNZQXhqUvqUTotpZ00qLSaify3b4VFD588aRr8MKFw4CMUr98ytzCW5wDH5qx/DEY5kCDXcbcRuCqL0szEf2tg== @@ -939,10 +934,10 @@ concurrently@^8: tree-kill "^1.2.2" yargs "^17.7.2" -conf@^10.1.2: - version "10.1.2" - resolved "https://registry.yarnpkg.com/conf/-/conf-10.1.2.tgz#50132158f388756fa9dea3048f6b47935315c14e" - integrity sha512-o9Fv1Mv+6A0JpoayQ8JleNp3hhkbOJP/Re/Q+QqxMPHPkABVsRjQGWZn9A5GcqLiTNC6d89p2PB5ZhHVDSMwyg== +conf@^10.2.0: + version "10.2.0" + resolved "https://registry.yarnpkg.com/conf/-/conf-10.2.0.tgz#838e757be963f1a2386dfe048a98f8f69f7b55d6" + integrity sha512-8fLl9F04EJqjSqH+QjITQfJF8BrOVaYr1jewVgSRAEWePfxT0sku4w2hrGQ60BC/TNLGQ2pgxNlTbWQmMPFvXg== dependencies: ajv "^8.6.3" ajv-formats "^2.1.1" @@ -956,12 +951,12 @@ conf@^10.1.2: semver "^7.3.5" config-file-ts@^0.2.4: - version "0.2.4" - resolved "https://registry.yarnpkg.com/config-file-ts/-/config-file-ts-0.2.4.tgz#6c0741fbe118a7cf786c65f139030f0448a2cc99" - integrity sha512-cKSW0BfrSaAUnxpgvpXPLaaW/umg4bqg4k3GO1JqlRfpx+d5W0GDXznCMkWotJQek5Mmz1MJVChQnz3IVaeMZQ== + version "0.2.6" + resolved "https://registry.yarnpkg.com/config-file-ts/-/config-file-ts-0.2.6.tgz#b424ff74612fb37f626d6528f08f92ddf5d22027" + integrity sha512-6boGVaglwblBgJqGyxm4+xCmEGcWgnWHSWHY5jad58awQhB6gftq0G8HbzU39YqCIYHMLAiL1yjwiZ36m/CL8w== dependencies: - glob "^7.1.6" - typescript "^4.0.2" + glob "^10.3.10" + typescript "^5.3.3" core-util-is@1.0.2: version "1.0.2" @@ -975,7 +970,7 @@ crc@^3.8.0: dependencies: buffer "^5.1.0" -cross-spawn@^7.0.1, cross-spawn@^7.0.2, cross-spawn@^7.0.3: +cross-spawn@^7.0.0, cross-spawn@^7.0.1, cross-spawn@^7.0.2, cross-spawn@^7.0.3: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== @@ -1022,11 +1017,21 @@ defer-to-connect@^2.0.0: resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-2.0.1.tgz#8016bdb4143e4632b77a3449c6236277de520587" integrity sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg== -define-properties@^1.1.3: +define-data-property@^1.0.1: version "1.1.4" - resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.4.tgz#0b14d7bd7fbeb2f3572c3a7eda80ea5d57fb05b1" - integrity sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA== + resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" + integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + gopd "^1.0.1" + +define-properties@^1.1.3: + version "1.2.1" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.1.tgz#10781cc616eb951a80a034bafcaa7377f6af2b6c" + integrity sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg== + dependencies: + define-data-property "^1.0.1" has-property-descriptors "^1.0.0" object-keys "^1.1.1" @@ -1065,14 +1070,14 @@ dir-glob@^3.0.1: dependencies: path-type "^4.0.0" -dmg-builder@24.6.4: - version "24.6.4" - resolved "https://registry.yarnpkg.com/dmg-builder/-/dmg-builder-24.6.4.tgz#e19b8305f7e1ea0b4faaa30382c81b9d6de39863" - integrity sha512-BNcHRc9CWEuI9qt0E655bUBU/j/3wUCYBVKGu1kVpbN5lcUdEJJJeiO0NHK3dgKmra6LUUZlo+mWqc+OCbi0zw== +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== dependencies: - app-builder-lib "24.6.4" - builder-util "24.5.0" - builder-util-runtime "9.2.1" + app-builder-lib "24.13.3" + builder-util "24.13.1" + builder-util-runtime "9.2.4" fs-extra "^10.1.0" iconv-lite "^0.6.2" js-yaml "^4.1.0" @@ -1122,6 +1127,11 @@ dotenv@^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" @@ -1129,26 +1139,26 @@ ejs@^3.1.8: dependencies: jake "^10.8.5" -electron-builder-notarize@^1.2.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/electron-builder-notarize/-/electron-builder-notarize-1.5.0.tgz#ef17c3f0eac6b3908ff2bc3568aea6e4f8612b5b" - integrity sha512-kbVCnZX3pCKTXiPhyoMjCZYSQnwS04QmlTM2NB2D/2LCsab5UJA0Me9ZqDT3W35ENPglf1WYDKT+tx9i+xuaPA== +electron-builder-notarize@^1.5: + version "1.5.1" + resolved "https://registry.yarnpkg.com/electron-builder-notarize/-/electron-builder-notarize-1.5.1.tgz#e00b868a67ef20a77f00017606626f24fdbdc445" + integrity sha512-xS7s9gE+1AcJIuJ4DU/LqCrmRypE1zOR/6b66egKzgP/UVh9YSa7rINos34gF/KcueNDQU39HcXcCEKiEI5wPQ== dependencies: dotenv "^8.2.0" electron-notarize "^1.1.1" js-yaml "^3.14.0" read-pkg-up "^7.0.0" -electron-builder@^24.6.4: - version "24.6.4" - resolved "https://registry.yarnpkg.com/electron-builder/-/electron-builder-24.6.4.tgz#c51271e49b9a02c9a3ec444f866b6008c4d98a1d" - integrity sha512-uNWQoU7pE7qOaIQ6CJHpBi44RJFVG8OHRBIadUxrsDJVwLLo8Nma3K/EEtx5/UyWAQYdcK4nVPYKoRqBb20hbA== +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.6.4" - builder-util "24.5.0" - builder-util-runtime "9.2.1" + app-builder-lib "24.13.3" + builder-util "24.13.1" + builder-util-runtime "9.2.4" chalk "^4.1.2" - dmg-builder "24.6.4" + dmg-builder "24.13.3" fs-extra "^10.1.0" is-ci "^3.0.0" lazy-val "^1.0.5" @@ -1156,68 +1166,61 @@ electron-builder@^24.6.4: simple-update-notifier "2.0.0" yargs "^17.6.2" -electron-log@^4.3.5: - version "4.4.8" - resolved "https://registry.yarnpkg.com/electron-log/-/electron-log-4.4.8.tgz#fcb9f714dbcaefb6ac7984c4683912c74730248a" - integrity sha512-QQ4GvrXO+HkgqqEOYbi+DHL7hj5JM+nHi/j+qrN9zeeXVKy8ZABgbu4CnG+BBqDZ2+tbeq9tUC4DZfIWFU5AZA== +electron-log@^5.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/electron-log/-/electron-log-5.1.2.tgz#fb40ad7f4ae694dd0e4c02c662d1a65c03e1243e" + integrity sha512-Cpg4hAZ27yM9wzE77c4TvgzxzavZ+dVltCczParXN+Vb3jocojCSAuSMCVOI9fhFuuOR+iuu3tZLX1cu0y0kgQ== electron-notarize@^1.1.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/electron-notarize/-/electron-notarize-1.2.1.tgz#347c18eca8e29dddadadee511b870c13d4008baf" - integrity sha512-u/ECWhIrhkSQpZM4cJzVZ5TsmkaqrRo5LDC/KMbGF0sPkm53Ng59+M0zp8QVaql0obfJy9vlVT+4iOkAi2UDlA== + 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.5.0: - version "24.5.0" - resolved "https://registry.yarnpkg.com/electron-publish/-/electron-publish-24.5.0.tgz#492a4d7caa232e88ee3c18f5c3b4dc637e5e1b3a" - integrity sha512-zwo70suH15L15B4ZWNDoEg27HIYoPsGJUF7xevLJLSI7JUPC8l2yLBdLGwqueJ5XkDL7ucYyRZzxJVR8ElV9BA== +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== dependencies: "@types/fs-extra" "^9.0.11" - builder-util "24.5.0" - builder-util-runtime "9.2.1" + builder-util "24.13.1" + builder-util-runtime "9.2.4" chalk "^4.1.2" fs-extra "^10.1.0" lazy-val "^1.0.5" mime "^2.5.2" -electron-reload@^2.0.0-alpha.1: - version "2.0.0-alpha.1" - resolved "https://registry.yarnpkg.com/electron-reload/-/electron-reload-2.0.0-alpha.1.tgz#6cad98df96695ca1d5462dc9407f7c620028ce99" - integrity sha512-hTde7gv0TEqxbxlB3pj2CwoyCQ9sdiQrcP8GkpzhosxyVeYM3mZbMEVKCZK3L0fED7Mz5A9IWmK7zEvi4H3P1g== +electron-store@^8.2: + version "8.2.0" + resolved "https://registry.yarnpkg.com/electron-store/-/electron-store-8.2.0.tgz#114e6e453e8bb746ab4ccb542424d8c881ad2ca1" + integrity sha512-ukLL5Bevdil6oieAOXz3CMy+OgaItMiVBg701MNlG6W5RaC0AHN7rvlqTCmeb6O7jP0Qa1KKYTE0xV0xbhF4Hw== dependencies: - chokidar "^3.5.2" + conf "^10.2.0" + type-fest "^2.17.0" -electron-store@^8.0.1: - version "8.0.2" - resolved "https://registry.yarnpkg.com/electron-store/-/electron-store-8.0.2.tgz#95c8cf81c1e1cf48b24f3ceeea24b921c1ff62d7" - integrity sha512-9GwUMv51w8ydbkaG7X0HrPlElXLApg63zYy1/VZ/a08ndl0gfm4iCoD3f0E1JvP3V16a+7KxqriCI0c122stiA== +electron-updater@^6.1: + version "6.1.8" + resolved "https://registry.yarnpkg.com/electron-updater/-/electron-updater-6.1.8.tgz#17637bca165322f4e526b13c99165f43e6f697d8" + integrity sha512-hhOTfaFAd6wRHAfUaBhnAOYc+ymSGCWJLtFkw4xJqOvtpHmIdNHnXDV9m1MHC+A6q08Abx4Ykgyz/R5DGKNAMQ== dependencies: - conf "^10.1.2" - type-fest "^2.12.2" - -electron-updater@^4.3.8: - version "4.6.5" - resolved "https://registry.yarnpkg.com/electron-updater/-/electron-updater-4.6.5.tgz#e9a75458bbfd6bb41a58a829839e150ad2eb2d3d" - integrity sha512-kdTly8O9mSZfm9fslc1mnCY+mYOeaYRy7ERa2Fed240u01BKll3aiupzkd07qKw69KvhBSzuHroIW3mF0D8DWA== - dependencies: - "@types/semver" "^7.3.6" - builder-util-runtime "8.9.2" - fs-extra "^10.0.0" + builder-util-runtime "9.2.3" + fs-extra "^10.1.0" js-yaml "^4.1.0" lazy-val "^1.0.5" lodash.escaperegexp "^4.1.2" lodash.isequal "^4.5.0" - semver "^7.3.5" + semver "^7.3.8" + tiny-typed-emitter "^2.1.0" -electron@^25.8.4: - version "25.8.4" - resolved "https://registry.yarnpkg.com/electron/-/electron-25.8.4.tgz#b50877aac7d96323920437baf309ad86382cb455" - integrity sha512-hUYS3RGdaa6E1UWnzeGnsdsBYOggwMMg4WGxNGvAoWtmRrr6J1BsjFW/yRq4WsJHJce2HdzQXtz4OGXV6yUCLg== +electron@^29: + version "29.1.5" + resolved "https://registry.yarnpkg.com/electron/-/electron-29.1.5.tgz#b745b4d201c1ac9f84d6aa034126288dde34d5a1" + integrity sha512-1uWGRw/ffA62lcrklxGUgVxVtOHojsg/nwsYr+/F9cVjipZJn8iPv/ABGIIexhmUqWcho8BqfTJ4osCBa29gBg== dependencies: "@electron/get" "^2.0.0" - "@types/node" "^18.11.18" + "@types/node" "^20.9.0" extract-zip "^2.0.1" emoji-regex@^8.0.0: @@ -1225,6 +1228,11 @@ 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== + end-of-stream@^1.1.0: version "1.4.4" resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" @@ -1249,15 +1257,27 @@ error-ex@^1.3.1: 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" + integrity sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ== + dependencies: + get-intrinsic "^1.2.4" + +es-errors@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" + integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== + es6-error@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/es6-error/-/es6-error-4.1.1.tgz#9e3af407459deed47e9a91f9b885a84eb05c561d" integrity sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg== escalade@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" - integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== + version "3.1.2" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.2.tgz#54076e9ab29ea5bf3d8f1ed62acffbb88272df27" + integrity sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA== escape-string-regexp@^1.0.5: version "1.0.5" @@ -1277,12 +1297,7 @@ eslint-scope@^7.2.2: esrecurse "^4.3.0" estraverse "^5.2.0" -eslint-visitor-keys@^3.3.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz#f6480fa6b1f30efe2d1968aa8ac745b862469826" - integrity sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA== - -eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4.3: +eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4.3: version "3.4.3" resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800" integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== @@ -1390,18 +1405,7 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== -fast-glob@^3.2.9: - version "3.2.11" - resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.11.tgz#a1172ad95ceb8a16e20caa5c5e56480e5129c1d9" - integrity sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew== - dependencies: - "@nodelib/fs.stat" "^2.0.2" - "@nodelib/fs.walk" "^1.2.3" - glob-parent "^5.1.2" - merge2 "^1.3.0" - micromatch "^4.0.4" - -fast-glob@^3.3.0: +fast-glob@^3.2.9, fast-glob@^3.3.0: version "3.3.2" resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129" integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow== @@ -1423,9 +1427,9 @@ fast-levenshtein@^2.0.6: integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== fastq@^1.6.0: - version "1.13.0" - resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.13.0.tgz#616760f88a7526bdfc596b7cab8c18938c36b98c" - integrity sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw== + version "1.17.1" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.17.1.tgz#2a523f07a4e7b1e81a42b91b8bf2254107753b47" + integrity sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w== dependencies: reusify "^1.0.4" @@ -1436,10 +1440,10 @@ fd-slicer@~1.1.0: dependencies: pend "~1.2.0" -ffmpeg-static@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/ffmpeg-static/-/ffmpeg-static-5.1.0.tgz#133500f4566570c5a0e96795152b0526d8c936ad" - integrity sha512-eEWOiGdbf7HKPeJI5PoJ0oCwkL0hckL2JdS4JOuB/gUETppwkEpq8nF0+e6VEQnDCo/iuoipbTUsn9QJmtpNkg== +ffmpeg-static@^5.2: + version "5.2.0" + resolved "https://registry.yarnpkg.com/ffmpeg-static/-/ffmpeg-static-5.2.0.tgz#6ca64a5ed6e69ec4896d175c1f69dd575db7c5ef" + integrity sha512-WrM7kLW+do9HLr+H6tk7LzQ7kPqbAgLjdzNE32+u3Ff11gXt9Kkkd2nusGFrlWMIe+XaA97t+I8JS7sZIrvRgA== dependencies: "@derhuerst/http-basic" "^8.2.0" env-paths "^2.2.0" @@ -1453,7 +1457,7 @@ file-entry-cache@^6.0.1: dependencies: flat-cache "^3.0.4" -filelist@^1.0.1: +filelist@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/filelist/-/filelist-1.0.4.tgz#f78978a1e944775ff9e62e744424f215e58352b5" integrity sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q== @@ -1503,14 +1507,13 @@ flatted@^3.1.0: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.6.tgz#022e9218c637f9f3fc9c35ab9c9193f05add60b2" integrity sha512-0sQoMh9s0BYsm+12Huy/rkKxVu4R1+r96YX5cG44rHV0pQ6iC3Q+mkoMFaGWObMFYQxCVT+ssG1ksneA2MI9KQ== -form-data@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.1.tgz#ebd53791b78356a99af9a300d4282c4d5eb9755f" - integrity sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg== +foreground-child@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.1.1.tgz#1d173e776d75d2772fed08efe4a0de1ea1b12d0d" + integrity sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg== dependencies: - asynckit "^0.4.0" - combined-stream "^1.0.8" - mime-types "^2.1.12" + cross-spawn "^7.0.0" + signal-exit "^4.0.1" form-data@^4.0.0: version "4.0.0" @@ -1562,41 +1565,30 @@ fs.realpath@^1.0.0: integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== fsevents@~2.3.2: - version "2.3.2" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" - integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== + version "2.3.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== -function-bind@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" - integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== - -gar@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/gar/-/gar-1.0.4.tgz#f777bc7db425c0572fdeb52676172ca1ae9888b8" - integrity sha512-w4n9cPWyP7aHxKxYHFQMegj7WIAsL/YX/C4Bs5Rr8s1H9M1rNtRWRsw+ovYMkXDQ5S4ZbYHsHAPmevPjPgw44w== +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== 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" integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== -get-folder-size@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/get-folder-size/-/get-folder-size-2.0.1.tgz#3fe0524dd3bad05257ef1311331417bcd020a497" - integrity sha512-+CEb+GDCM7tkOS2wdMKTn9vU7DgnKUTuDlehkNJKNSovdCOVxs14OfKCk4cvSaR3za4gj+OBdl9opPN9xrJ0zA== +get-intrinsic@^1.1.3, get-intrinsic@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd" + integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ== dependencies: - gar "^1.0.4" - tiny-each-async "2.0.3" - -get-intrinsic@^1.1.1: - version "1.1.2" - resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.2.tgz#336975123e05ad0b7ba41f152ee4aadbea6cf598" - integrity sha512-Jfm3OyCxHh9DJyc28qGk+JmfkpO41A4XkneDSujN9MDXrm4oDKdHvndhZ2dN94+ERNfkYJWDclW6k2L/ZGHjXA== - dependencies: - function-bind "^1.1.1" - has "^1.0.3" + es-errors "^1.3.0" + function-bind "^1.1.2" + has-proto "^1.0.1" has-symbols "^1.0.3" + hasown "^2.0.0" get-stdin@^9.0.0: version "9.0.0" @@ -1629,7 +1621,18 @@ glob-parent@^6.0.2: dependencies: is-glob "^4.0.3" -glob@^7.1.3, glob@^7.1.6: +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== + dependencies: + foreground-child "^3.1.0" + jackspeak "^2.3.5" + minimatch "^9.0.1" + minipass "^5.0.0 || ^6.0.2 || ^7.0.0" + path-scurry "^1.10.1" + +glob@^7.0.0, glob@^7.1.3, glob@^7.1.6: version "7.2.3" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== @@ -1690,6 +1693,13 @@ globby@^13.1.2: merge2 "^1.4.1" slash "^4.0.0" +gopd@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" + integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA== + dependencies: + get-intrinsic "^1.1.3" + got@^11.8.5: version "11.8.6" resolved "https://registry.yarnpkg.com/got/-/got-11.8.6.tgz#276e827ead8772eddbcfc97170590b841823233a" @@ -1708,9 +1718,9 @@ got@^11.8.5: responselike "^2.0.0" graceful-fs@^4.1.6, graceful-fs@^4.2.0: - version "4.2.10" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" - integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== + 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== graphemer@^1.4.0: version "1.4.0" @@ -1728,23 +1738,28 @@ has-flag@^4.0.0: integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== has-property-descriptors@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz#610708600606d36961ed04c196193b6a607fa861" - integrity sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ== + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854" + integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== dependencies: - get-intrinsic "^1.1.1" + es-define-property "^1.0.0" + +has-proto@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.3.tgz#b31ddfe9b0e6e9914536a6ab286426d0214f77fd" + integrity sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q== has-symbols@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== -has@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" - integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== +hasown@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" + integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== dependencies: - function-bind "^1.1.1" + function-bind "^1.1.2" hosted-git-info@^2.1.4: version "2.8.9" @@ -1758,15 +1773,15 @@ hosted-git-info@^4.1.0: dependencies: lru-cache "^6.0.0" -html-entities@^2.4.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-2.4.0.tgz#edd0cee70402584c8c76cc2c0556db09d1f45061" - integrity sha512-igBTJcNNNhvZFRtm8uA6xMY6xYleeDwn3PeBCkDz7tHttv4F2hsDI2aPgNERWzvRcNYHNT3ymRaQzllmXj4YsQ== +html-entities@^2.5: + version "2.5.2" + 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: - version "4.1.0" - resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390" - integrity sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ== + 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== http-proxy-agent@^5.0.0: version "5.0.0" @@ -1820,12 +1835,7 @@ ieee754@^1.1.13: resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== -ignore@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.0.tgz#6d3bac8fa7fe0d45d9f9be7bac2fc279577e345a" - integrity sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ== - -ignore@^5.2.4: +ignore@^5.2.0, ignore@^5.2.4: version "5.3.1" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.1.tgz#5073e554cd42c5b33b394375f538b8593e34d4ef" integrity sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw== @@ -1856,6 +1866,11 @@ inherits@2, inherits@^2.0.3: resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== +interpret@^1.0.0: + version "1.4.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" @@ -1875,12 +1890,12 @@ is-ci@^3.0.0: dependencies: ci-info "^3.2.0" -is-core-module@^2.9.0: - version "2.9.0" - resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.9.0.tgz#e1c34429cd51c6dd9e09e0799e396e27b19a9c69" - integrity sha512-+5FPy5PnwmO3lvfMb0AsoPaBG+5KHUI0wYFXOtYPnVVVspTFUuMZNfNaNVRt3FZadstu2c8x23vykRW/NBoU6A== +is-core-module@^2.13.0: + version "2.13.1" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.13.1.tgz#ad0d7532c6fea9da1ebdc82742d74525c6273384" + integrity sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw== dependencies: - has "^1.0.3" + hasown "^2.0.0" is-extglob@^2.1.1: version "2.1.1" @@ -1925,26 +1940,35 @@ isbinaryfile@^4.0.8: integrity sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw== isbinaryfile@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-5.0.0.tgz#034b7e54989dab8986598cbcea41f66663c65234" - integrity sha512-UDdnyGvMajJUWCkib7Cei/dvyJrrvo4FIrsvSFWdPpXSUorzXrDJ0S+X5Q4ZlasfPjca4yqCNNsjbCeiy8FFeg== + version "5.0.2" + resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-5.0.2.tgz#fe6e4dfe2e34e947ffa240c113444876ba393ae0" + integrity sha512-GvcjojwonMjWbTkfMpnVHVqXW/wKMYDfEpY94/8zy8HFMOqb/VL6oeONq9v87q4ttVlaTLnGXnJD4B5B1OTGIg== isexe@^2.0.0: version "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== + dependencies: + "@isaacs/cliui" "^8.0.2" + optionalDependencies: + "@pkgjs/parseargs" "^0.11.0" + jake@^10.8.5: - version "10.8.5" - resolved "https://registry.yarnpkg.com/jake/-/jake-10.8.5.tgz#f2183d2c59382cb274226034543b9c03b8164c46" - integrity sha512-sVpxYeuAhWt0OTWITwT98oyV0GsXyMlXCF+3L1SuafBVUIr/uILGRB+NqwkzhgXKvoJpDIpQvqkUALgdmQsQxw== + version "10.8.7" + resolved "https://registry.yarnpkg.com/jake/-/jake-10.8.7.tgz#63a32821177940c33f356e0ba44ff9d34e1c7d8f" + integrity sha512-ZDi3aP+fG/LchyBzUM804VjddnwfSfsdeYkwt8NcbKRvo4rFkjhs456iLFn3k2ZUWvNe4i48WACDbza8fhq2+w== dependencies: async "^3.2.3" chalk "^4.0.2" - filelist "^1.0.1" - minimatch "^3.0.4" + filelist "^1.0.4" + minimatch "^3.1.2" -jpeg-js@^0.4.4: +jpeg-js@^0.4: version "0.4.4" resolved "https://registry.yarnpkg.com/jpeg-js/-/jpeg-js-0.4.4.tgz#a9f1c6f1f9f0fa80cdb3484ed9635054d28936aa" integrity sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg== @@ -2005,9 +2029,9 @@ json-stringify-safe@^5.0.1: integrity sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA== json5@^2.2.0: - version "2.2.1" - resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.1.tgz#655d50ed1e6f95ad1a3caababd2b0efda10b395c" - integrity sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA== + version "2.2.3" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" + integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== jsonfile@^4.0.0: version "4.0.0" @@ -2026,9 +2050,9 @@ jsonfile@^6.0.1: graceful-fs "^4.1.6" keyv@^4.0.0: - version "4.5.3" - resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.3.tgz#00873d2b046df737963157bd04f294ca818c9c25" - integrity sha512-QCiSav9WaX1PgETJ+SpNnx2PRRapJ/oRSXM4VO5OGYGSjrxbKPVFVhB3l2OCbLCk329N8qyAtsJjSjvVBWzEug== + version "4.5.4" + resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" + integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw== dependencies: json-buffer "3.0.1" @@ -2104,6 +2128,11 @@ 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== + matcher@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/matcher/-/matcher-3.0.0.tgz#bd9060f4c5b70aa8041ccc6f80368760994f30ca" @@ -2161,7 +2190,7 @@ mimic-response@^3.1.0: resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-3.1.0.tgz#2d1d59af9c1b129815accc2c46a022a5ce1fa3c9" integrity sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ== -minimatch@9.0.3: +minimatch@9.0.3, 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== @@ -2175,29 +2204,22 @@ minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: dependencies: brace-expansion "^1.1.7" -minimatch@^5.0.1: - version "5.1.0" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.0.tgz#1717b464f4971b144f6aabe8f2d0b8e4511e09c7" - integrity sha512-9TPBGGak4nHfGZsPBohm9AWg6NoT7QTCehS3BIJABslyZbzxfV78QM2Y6+i741OPZIafFAaiiEMh5OyIrJPgtg== - dependencies: - brace-expansion "^2.0.1" - -minimatch@^5.1.1: +minimatch@^5.0.1, minimatch@^5.1.1: version "5.1.6" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96" integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g== dependencies: brace-expansion "^2.0.1" -minimist@^1.2.6: - version "1.2.6" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" - integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== +minimist@^1.2.3, minimist@^1.2.6: + version "1.2.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" + integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== minipass@^3.0.0: - version "3.3.4" - resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.3.4.tgz#ca99f95dd77c43c7a76bf51e6d200025eee0ffae" - integrity sha512-I9WPbWHCGu8W+6k1ZiGpPu0GkoKBeorkfKNuAFBNS1HNFJvke82sxvI5bzcCNpWPorkOO5QQ+zomzzwRxejXiw== + version "3.3.6" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.3.6.tgz#7bba384db3a1520d18c9c0e5251c3444e95dd94a" + integrity sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw== dependencies: yallist "^4.0.0" @@ -2206,6 +2228,11 @@ 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": + version "7.0.4" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.0.4.tgz#dbce03740f50a4786ba994c1fb908844d27b038c" + integrity sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ== + minizlib@^2.1.1: version "2.1.2" resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931" @@ -2246,14 +2273,7 @@ node-addon-api@^1.6.3: resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-1.7.2.tgz#3df30b95720b53c24e59948b49532b662444f54d" integrity sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg== -node-fetch@^2.6.7: - version "2.6.7" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" - integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ== - dependencies: - whatwg-url "^5.0.0" - -node-stream-zip@^1.15.0: +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== @@ -2297,17 +2317,17 @@ onetime@^5.1.2: dependencies: mimic-fn "^2.1.0" -onnxruntime-common@~1.16.3: - version "1.16.3" - resolved "https://registry.yarnpkg.com/onnxruntime-common/-/onnxruntime-common-1.16.3.tgz#216bd1318d171496f1e92906a801c95bd2fb1aaa" - integrity sha512-ZZfFzEqBf6YIGwB9PtBLESHI53jMXA+/hn+ACVUbEfPuK2xI5vMGpLPn+idpwCmHsKJNRzRwqV12K+6TQj6tug== +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-node@^1.16.3: - version "1.16.3" - resolved "https://registry.yarnpkg.com/onnxruntime-node/-/onnxruntime-node-1.16.3.tgz#8530439f4a513b17e4d3df0073f54c4614a46070" - integrity sha512-6T2pjwg5ik74VnI1IXFzxvPAm2UCo+vNNsDGbMP+A2q6GZPMYai2pMA17g3YMUvgOZLwsjWBUwNIlP4QaVRFlA== +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== dependencies: - onnxruntime-common "~1.16.3" + onnxruntime-common "1.17.0" optionator@^0.9.3: version "0.9.3" @@ -2413,6 +2433,14 @@ 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== + dependencies: + lru-cache "^9.1.1 || ^10.0.0" + minipass "^5.0.0 || ^6.0.2 || ^7.0.0" + path-type@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" @@ -2423,16 +2451,16 @@ pend@~1.2.0: resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50" integrity sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg== -picomatch@^2.0.4, picomatch@^2.3.1: +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" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== -picomatch@^2.2.1: - version "2.2.2" - resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz" - integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg== - pkg-up@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/pkg-up/-/pkg-up-3.1.0.tgz#100ec235cc150e4fd42519412596a28512a0def5" @@ -2440,15 +2468,7 @@ pkg-up@^3.1.0: dependencies: find-up "^3.0.0" -plist@^3.0.4: - version "3.0.6" - resolved "https://registry.yarnpkg.com/plist/-/plist-3.0.6.tgz#7cfb68a856a7834bca6dbfe3218eb9c7740145d3" - integrity sha512-WiIVYyrp8TD4w8yCvyeIr+lkmrGRd5u0VbRnU+tP/aRLxP/YadJUYOMZJ/6hIa3oUyVCsycXvtNRgd5XBJIbiA== - dependencies: - base64-js "^1.5.1" - xmlbuilder "^15.1.1" - -plist@^3.0.5: +plist@^3.0.4, plist@^3.0.5: version "3.1.0" resolved "https://registry.yarnpkg.com/plist/-/plist-3.1.0.tgz#797a516a93e62f5bde55e0b9cc9c967f860893c9" integrity sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ== @@ -2485,13 +2505,6 @@ progress@^2.0.3: resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== -promise-fs@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/promise-fs/-/promise-fs-2.1.1.tgz#0b725a592c165ff16157d1f13640ba390637e557" - integrity sha512-43p7e4QzAQ3w6eyN0+gbBL7jXiZFWLWYITg9wIObqkBySu/a5K1EDcQ/S6UyB/bmiZWDA4NjTbcopKLTaKcGSw== - dependencies: - "@octetstream/promisify" "2.0.2" - promise-retry@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/promise-retry/-/promise-retry-2.0.1.tgz#ff747a13620ab57ba688f5fc67855410c370da22" @@ -2509,9 +2522,9 @@ pump@^3.0.0: once "^1.3.1" punycode@^2.1.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" - integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== + version "2.3.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" + integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== queue-microtask@^1.2.2: version "1.2.3" @@ -2555,9 +2568,9 @@ read-pkg@^5.2.0: type-fest "^0.6.0" readable-stream@^3.0.2: - version "3.6.0" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" - integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== + version "3.6.2" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" + integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== dependencies: inherits "^2.0.3" string_decoder "^1.1.1" @@ -2570,6 +2583,13 @@ readdirp@~3.6.0: dependencies: picomatch "^2.2.1" +rechoir@^0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384" + integrity sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw== + dependencies: + resolve "^1.1.6" + regenerator-runtime@^0.14.0: version "0.14.1" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz#356ade10263f685dda125100cd862c1db895327f" @@ -2595,12 +2615,12 @@ 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.10.0: - version "1.22.1" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177" - integrity sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw== +resolve@^1.1.6, resolve@^1.10.0: + version "1.22.8" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" + integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== dependencies: - is-core-module "^2.9.0" + is-core-module "^2.13.0" path-parse "^1.0.7" supports-preserve-symlinks-flag "^1.0.0" @@ -2621,7 +2641,7 @@ reusify@^1.0.4: resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== -rimraf@^3.0.0, rimraf@^3.0.2: +rimraf@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== @@ -2672,9 +2692,9 @@ sanitize-filename@^1.6.3: truncate-utf8-bytes "^1.0.0" sax@^1.2.4: - version "1.2.4" - resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" - integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== + version "1.3.0" + resolved "https://registry.yarnpkg.com/sax/-/sax-1.3.0.tgz#a5dbe77db3be05c9d1ee7785dbd3ea9de51593d0" + integrity sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA== semver-compare@^1.0.0: version "1.0.0" @@ -2682,30 +2702,16 @@ semver-compare@^1.0.0: integrity sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow== "semver@2 || 3 || 4 || 5": - version "5.7.1" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" - integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== + 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.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" - integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== + version "6.3.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" + integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== -semver@^7.3.2, semver@^7.3.5: - version "7.3.7" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.7.tgz#12c5b649afdbf9049707796e22a4028814ce523f" - integrity sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g== - dependencies: - lru-cache "^6.0.0" - -semver@^7.3.8, semver@^7.5.3: - version "7.5.4" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" - integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== - dependencies: - lru-cache "^6.0.0" - -semver@^7.5.4: +semver@^7.3.2, semver@^7.3.5, semver@^7.3.8, semver@^7.5.3, semver@^7.5.4: version "7.6.0" resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.0.tgz#1a46a4db4bffcccd97b743b5005c8325f23d4e2d" integrity sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg== @@ -2736,6 +2742,28 @@ shell-quote@^1.8.1: resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.1.tgz#6dbf4db75515ad5bac63b4f1894c3a154c766680" integrity sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA== +shelljs@^0.8.5: + version "0.8.5" + resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.8.5.tgz#de055408d8361bed66c669d2f000538ced8ee20c" + integrity sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow== + dependencies: + glob "^7.0.0" + interpret "^1.0.0" + rechoir "^0.6.2" + +shx@^0.3: + version "0.3.4" + resolved "https://registry.yarnpkg.com/shx/-/shx-0.3.4.tgz#74289230b4b663979167f94e1935901406e40f02" + integrity sha512-N6A9MLVqjxZYcVn8hLmtneQWIJtp8IKzMP4eMnx+nqkvXoqinUPCbUFLp2UcWTEIUONhlk0ewxr/jaVGlc+J+g== + dependencies: + minimist "^1.2.3" + shelljs "^0.8.5" + +signal-exit@^4.0.1: + version "4.1.0" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" + integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== + simple-update-notifier@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz#d70b92bdab7d6d90dfd73931195a30b6e3d7cebb" @@ -2804,17 +2832,17 @@ spawn-command@0.0.2: integrity sha512-zC8zGoGkmc8J9ndvml8Xksr1Amk9qBujgbF0JAIWO7kXr43w0h/0GJNM/Vustixu+YE8N/MTrQ7N31FvHUACxQ== spdx-correct@^3.0.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.1.tgz#dece81ac9c1e6713e5f7d1b6f17d468fa53d89a9" - integrity sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w== + 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.3.0" - resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz#3f28ce1a77a00372683eade4a433183527a2163d" - integrity sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A== + 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" @@ -2825,14 +2853,14 @@ spdx-expression-parse@^3.0.0: spdx-license-ids "^3.0.0" spdx-license-ids@^3.0.0: - version "3.0.11" - resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.11.tgz#50c0d8c40a14ec1bf449bae69a0ea4685a9d9f95" - integrity sha512-Ctl2BrFiM0X3MANYgj3CkygxhRmr9mi6xhejbdO960nF6EDJApTYpn0BQnDKlnNBULKiCN1n3w9EBkHK8ZWg+g== + 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: - version "1.1.2" - resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.1.2.tgz#da1765262bf8c0f571749f2ad6c26300207ae673" - integrity sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug== + 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" @@ -2844,7 +2872,7 @@ stat-mode@^1.0.0: resolved "https://registry.yarnpkg.com/stat-mode/-/stat-mode-1.0.0.tgz#68b55cb61ea639ff57136f36b216a291800d1465" integrity sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg== -string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0", 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== @@ -2853,6 +2881,15 @@ string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: 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" @@ -2860,13 +2897,20 @@ string_decoder@^1.1.1: dependencies: safe-buffer "~5.2.0" -strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm: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" @@ -2914,9 +2958,9 @@ synckit@0.9.0: tslib "^2.6.2" tar@^6.1.12: - version "6.2.0" - resolved "https://registry.yarnpkg.com/tar/-/tar-6.2.0.tgz#b14ce49a79cb1cd23bc9b016302dea5474493f73" - integrity sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ== + version "6.2.1" + resolved "https://registry.yarnpkg.com/tar/-/tar-6.2.1.tgz#717549c541bc3c2af15751bea94b1dd068d4b03a" + integrity sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A== dependencies: chownr "^2.0.0" fs-minipass "^2.0.0" @@ -2938,10 +2982,10 @@ text-table@^0.2.0: resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== -tiny-each-async@2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/tiny-each-async/-/tiny-each-async-2.0.3.tgz#8ebbbfd6d6295f1370003fbb37162afe5a0a51d1" - integrity sha512-5ROII7nElnAirvFn8g7H7MtpfV1daMcyfTGQwsn/x2VtyV+VPiO5CjReCJtWLvoKTDEDmZocf3cNPraiMnBXLA== +tiny-typed-emitter@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/tiny-typed-emitter/-/tiny-typed-emitter-2.1.0.tgz#b3b027fdd389ff81a152c8e847ee2f5be9fad7b5" + integrity sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA== tmp-promise@^3.0.2: version "3.0.3" @@ -2951,11 +2995,9 @@ tmp-promise@^3.0.2: tmp "^0.2.0" tmp@^0.2.0: - version "0.2.1" - resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.1.tgz#8457fc3037dcf4719c251367a1af6500ee1ccf14" - integrity sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ== - dependencies: - rimraf "^3.0.0" + version "0.2.3" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.3.tgz#eb783cc22bc1e8bebd0671476d46ea4eb32a79ae" + integrity sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w== to-regex-range@^5.0.1: version "5.0.1" @@ -2964,11 +3006,6 @@ to-regex-range@^5.0.1: dependencies: is-number "^7.0.0" -tr46@~0.0.3: - version "0.0.3" - resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" - integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== - tree-kill@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc" @@ -3023,25 +3060,25 @@ type-fest@^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.12.2: - version "2.16.0" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.16.0.tgz#1250fbd64dafaf4c8e405e393ef3fb16d9651db2" - integrity sha512-qpaThT2HQkFb83gMOrdKVsfCN7LKxP26Yq+smPzY1FqoHRjqmjqHXA7n5Gkxi8efirtbeEUxzfEdePthQWCuHw== +type-fest@^2.17.0: + version "2.19.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.19.0.tgz#88068015bb33036a598b952e55e9311a60fd3a9b" + integrity sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA== typedarray@^0.0.6: version "0.0.6" resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA== -typescript@^4.0.2: - version "4.9.5" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a" - integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g== +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== -typescript@^5: - version "5.4.2" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.4.2.tgz#0ae9cebcfae970718474fe0da2c090cad6577372" - integrity sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ== +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== universalify@^0.1.0: version "0.1.2" @@ -3049,9 +3086,9 @@ universalify@^0.1.0: integrity sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg== universalify@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" - integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== + version "2.0.1" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d" + integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw== untildify@^3.0.2: version "3.0.3" @@ -3092,19 +3129,6 @@ verror@^1.10.0: core-util-is "1.0.2" extsprintf "^1.2.0" -webidl-conversions@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" - integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== - -whatwg-url@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" - integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw== - dependencies: - tr46 "~0.0.3" - webidl-conversions "^3.0.0" - which@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" @@ -3117,7 +3141,7 @@ winreg@1.2.4: resolved "https://registry.yarnpkg.com/winreg/-/winreg-1.2.4.tgz#ba065629b7a925130e15779108cf540990e98d1b" integrity sha512-IHpzORub7kYlb8A43Iig3reOvlcBJGX9gZ0WycHhghHtA65X0LYnMRuJs+aH1abVnMJztQkvQNlltnbPi5aGIA== -wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", 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== @@ -3126,6 +3150,15 @@ wrap-ansi@^7.0.0: 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" diff --git a/docs/docs/.vitepress/sidebar.ts b/docs/docs/.vitepress/sidebar.ts index b7700f8e9a..b7c3946a2b 100644 --- a/docs/docs/.vitepress/sidebar.ts +++ b/docs/docs/.vitepress/sidebar.ts @@ -101,6 +101,10 @@ export const sidebar = [ collapsed: true, items: [ { text: "General", link: "/photos/faq/general" }, + { + text: "Export", + link: "/photos/faq/export", + }, { text: "Security and privacy", link: "/photos/faq/security-and-privacy", @@ -138,7 +142,7 @@ export const sidebar = [ { text: "FAQ", link: "/auth/faq/" }, { text: "Migration", - collapsed: false, + collapsed: true, items: [ { text: "Introduction", link: "/auth/migration-guides/" }, { @@ -188,14 +192,11 @@ export const sidebar = [ { text: "FAQ", items: [ + { text: "General", link: "/self-hosting/faq/" }, { text: "Verification code", link: "/self-hosting/faq/otp", }, - { - text: "Increase storage space", - link: "/self-hosting/faq/storage-space", - }, ], }, { diff --git a/docs/docs/auth/migration-guides/authy/index.md b/docs/docs/auth/migration-guides/authy/index.md index 268d7e4f74..48ce3965d9 100644 --- a/docs/docs/auth/migration-guides/authy/index.md +++ b/docs/docs/auth/migration-guides/authy/index.md @@ -10,7 +10,7 @@ A guide written by Green, an ente.io lover > [!WARNING] > > Authy will soon be dropping support for its desktop apps in the near future. -> If you are looking to switch to ente Authenticator from Authy, I heavily +> If you are looking to switch to Ente Authenticator from Authy, I heavily > recommend you export your codes as soon as you can. --- @@ -19,7 +19,7 @@ Migrating from Authy can be tiring, as you cannot export your 2FA codes through the app, meaning that you would have to reconfigure 2FA for all of your accounts for your new 2FA authenticator. However, easier ways exist to export your codes out of Authy. This guide will cover two of the most used methods for mograting -from Authy to ente Authenticator. +from Authy to Ente Authenticator. > [!CAUTION] > @@ -39,7 +39,7 @@ hard (and rather technical) parts of the process.

One way to export is to [use this tool by Neeraj](https://github.com/ua741/authy-export/releases/tag/v0.0.4) -to simplify the process and skip directly to importing to ente Authenticator. +to simplify the process and skip directly to importing to Ente Authenticator. To export from Authy, download the tool for your specific OS, then type the following in your terminal: @@ -72,7 +72,7 @@ For Windows: ``` This will generate a text file called `authy_codes.txt`, which contains your -Authy codes in ente's plaintext export format. You can now import this to ente +Authy codes in Ente's plaintext export format. You can now import this to Ente Authenticator! ## Method 2: Use gboudreau's GitHub guide @@ -89,23 +89,23 @@ To export your data, please follow This will create a JSON file called `authy-to-bitwarden-export.json`, which contains your Authy codes in Bitwarden's export format. You can now import this -to ente Authenticator! +to Ente Authenticator! ### Method 2.1: If the export worked, but the import didn't > [!NOTE] > > This is intended only for users who successfully exported their codes using -> the guide in method 2, but could not import it to ente Authenticator for +> the guide in method 2, but could not import it to Ente Authenticator for > whatever reason. If the import was successful, or you haven't tried to import > the codes yet, ignore this section. > > If the export itself failed, try using > [**method 1**](#method-1-use-neerajs-export-tool) instead. -Usually, you should be able to import Bitwarden exports directly into ente +Usually, you should be able to import Bitwarden exports directly into Ente Authenticator. In case this didn't work for whatever reason, I've written a -program in Python that converts the JSON file into a TXT file that ente +program in Python that converts the JSON file into a TXT file that Ente Authenticator can use, so you can try importing using plain text import instead. You can download my program @@ -140,18 +140,18 @@ To run the Python program, open it in your IDE and run the program, or open your terminal and type `python3 authy_to_ente.py` (MacOS/Linux, or any other OS that uses bash) or `py -3 authy_to_ente.py` (Windows). Once you run it, a new TXT file called `auth_codes.txt` will be generated. You can now import your data to -ente Authenticator! +Ente Authenticator! --- You should now have a TXT file (method 1, method 2.1) or a JSON file (method 2) -that countains your TOTP secrets, which can now be imported into ente +that countains your TOTP secrets, which can now be imported into Ente Authenticator. To import your codes, please follow one of the steps below, depending on which method you used to export your codes. -## Importing to ente Authenticator (Method 1, method 2.1) +## Importing to Ente Authenticator (Method 1, method 2.1) -1. Copy the TXT file to one of your devices with ente Authenticator. +1. Copy the TXT file to one of your devices with Ente Authenticator. 2. Log in to your account (if you haven't already), or press "Use without backups". 3. Open the navigation menu (hamburger button on the top left), then press @@ -159,9 +159,9 @@ depending on which method you used to export your codes. 4. Select the "Plain text" option. 5. Select the TXT file that was made earlier. -## Importing to ente Authenticator (Method 2) +## Importing to Ente Authenticator (Method 2) -1. Copy the JSON file to one of your devices with ente Authenticator. +1. Copy the JSON file to one of your devices with Ente Authenticator. 2. Log in to your account (if you haven't already), or press "Use without backups". 3. Open the navigation menu (hamburger button on the top left), then press @@ -172,7 +172,7 @@ depending on which method you used to export your codes. If this didn't work, refer to [**method 2.1**](#method-21-if-the-export-worked-but-the-import-didnt).

-And that's it! You have now successfully migrated from Authy to ente +And that's it! You have now successfully migrated from Authy to Ente Authenticator. Now that your secrets are safely stored, I recommend you delete the unencrypted diff --git a/docs/docs/photos/faq/export.md b/docs/docs/photos/faq/export.md new file mode 100644 index 0000000000..ace1cd7361 --- /dev/null +++ b/docs/docs/photos/faq/export.md @@ -0,0 +1,29 @@ +--- +title: Export FAQ +description: Frequently asked questions about keeping extra backups of your data +--- + +# Export + +## Can I backup my data in a local drive outside Ente? + +Yes! You can use our CLI tool or our desktop app to set up exports of your data +in a local drive or NAS of your choice. This way, you can use Ente in your day +to day use, but will have an additional guarantee that a copy of your original +photos and videos are always available in normal directories and files. + +* You can use [Ente's CLI](https://github.com/ente-io/ente/tree/main/cli#export) + to export your data in a cron job to a location of your choice. The exports + are incremental, and will also gracefully handle interruptions. + +* Similarly, you can use Ente's [desktop app](https://ente.io/download/desktop) + to export your data to a folder of your choice. The desktop app also supports + "continuous" exports, where it will automatically export new items in the + background without you needing to run any other cron jobs. See + [migration/export](/photos/migration/export/) for more details. + +## Does the exported data from Ente photos preserve the same folder and album structure as in the app? + +When you export your data for local backup, it will maintain the exact structure +how you have set up within Ente. The exported data will reflect the same photos +and album structure intact. diff --git a/docs/docs/photos/faq/general.md b/docs/docs/photos/faq/general.md index 65d548a2db..c49b299790 100644 --- a/docs/docs/photos/faq/general.md +++ b/docs/docs/photos/faq/general.md @@ -74,3 +74,29 @@ If you would like to fund the development of this project, please consider ## How do I pronounce ente? It's like cafe 😊. kaf-_ay_. en-_tay_. + +## Does Ente apply compression to uploaded photos? + +Ente does not apply compression to uploaded photos. The file size of your photos in Ente will be similar to the original file sizes you have. + +## Can I add photos from a shared album to albums that I created in Ente? + +Currently, Ente does not support adding photos from a shared album to your personal albums. If you want to include photos from a shared album in your own albums, you will need to ask the owner of the photos to add them to your album. + +## How do I ensure that the Ente desktop app stays up to date on my system? + +Ente desktop includes an auto-update feature, ensuring that whenever updates are deployed, the app will automatically download and install them. You don't need to manually update the software. + +## Can I sync a folder containing multiple subfolders, each representing an album? + +Yes, when you drag and drop the folder onto the desktop app, the app will detect the multiple folders and prompt you to choose whether you want to create a single album or separate albums for each folder. + +## What is the difference between **Magic** and **Content** search results on the desktop? + +**Magic** is where you can search for long queries. Like, "baby in red dress", or "dog playing at the beach". + +**Content** is where you can search for single-words. Like, "car" or "pizza". + +## How do I identify which files experienced upload issues within the desktop app? + +Check the sections within the upload progress bar for "Failed Uploads," "Ignored Uploads," and "Unsuccessful Uploads." \ No newline at end of file diff --git a/docs/docs/photos/faq/subscription.md b/docs/docs/photos/faq/subscription.md index 58eb1857ac..ed4852151a 100644 --- a/docs/docs/photos/faq/subscription.md +++ b/docs/docs/photos/faq/subscription.md @@ -156,3 +156,7 @@ Sorry, since we're building a business that does not involve monetization of user data, we have to charge to remain sustainable. We do offer a generous free trial for you to experience the product. + +## Will I need to pay for Ente Auth after my Ente Photos free plan expires? + +No, you will not need to pay for Ente Auth after your Ente Photos free plan expires. Ente Auth is completely free to use, and the expiration of your Ente Photos free plan will not impact your ability to access or use Ente Auth. diff --git a/docs/docs/photos/migration/export/continuous-sync.webp b/docs/docs/photos/migration/export/continuous-sync.webp new file mode 100644 index 0000000000..f5b1346ebb Binary files /dev/null and b/docs/docs/photos/migration/export/continuous-sync.webp differ diff --git a/docs/docs/photos/migration/export/export-1.png b/docs/docs/photos/migration/export/export-1.png index e00bd49c54..49f03b28cb 100644 Binary files a/docs/docs/photos/migration/export/export-1.png and b/docs/docs/photos/migration/export/export-1.png differ diff --git a/docs/docs/photos/migration/export/export-2.png b/docs/docs/photos/migration/export/export-2.png index cfef40bb35..f181a24229 100644 Binary files a/docs/docs/photos/migration/export/export-2.png and b/docs/docs/photos/migration/export/export-2.png differ diff --git a/docs/docs/photos/migration/export/export-3.png b/docs/docs/photos/migration/export/export-3.png index 0434ffdb68..3df2a56602 100644 Binary files a/docs/docs/photos/migration/export/export-3.png and b/docs/docs/photos/migration/export/export-3.png differ diff --git a/docs/docs/photos/migration/export/export-4.png b/docs/docs/photos/migration/export/export-4.png index 2a5d16d599..e8abcf0f2c 100644 Binary files a/docs/docs/photos/migration/export/export-4.png and b/docs/docs/photos/migration/export/export-4.png differ diff --git a/docs/docs/photos/migration/export/export-5.png b/docs/docs/photos/migration/export/export-5.png new file mode 100644 index 0000000000..aa939e5667 Binary files /dev/null and b/docs/docs/photos/migration/export/export-5.png differ diff --git a/docs/docs/photos/migration/export/index.md b/docs/docs/photos/migration/export/index.md index 958ad77863..54d193226a 100644 --- a/docs/docs/photos/migration/export/index.md +++ b/docs/docs/photos/migration/export/index.md @@ -11,26 +11,57 @@ videos you have uploaded to Ente. 1. Sign in to [our desktop app](https://ente.io/download/desktop), if you haven't done so already. + ![Ente - Sign in to export data](sign-in.png) + 2. Open the side bar, and select the option to **export data**. ![Ente - Export data](export-1.png) -3. Select the destination folder and click on **start**. +3. Choose the destination folder by clicking on three dots icon. - ![Ente - Select destination folder and start](export-2.png) +
-4. Wait for the export to get completed. +![Ente - Select destination folder and start](export-2.png){width=400px} - ![Ente - Export in progress](export-3.png) +
-5. Later on if you wish to sync newer files that were uploaded since the last - time you exported, simply select **export data** again and click on - **resync**. +4. Select the folder and then click on **Start** + +
+ +![Ente - Export in progress](export-3.png){width=400px} + +
+ +5. Wait for the export to get completed. + +
+ +![Ente - Rexport](export-4.png){width=400px} + +
+ +6. In case your download gets interrupted, Ente will resume from where it left + off. Simply select **export data** again and click on **Resync**. + +
+ +![Ente - Rexport](export-5.png){width=400px} + +
+ +7. **Sync continuously** : You can utilize Continuous Sync to eliminate manual + exports each time new photos are added to Ente. This feature automatically + detects new files and runs exports accordingly, It also ensures that exported + data reflects the latest album states with new files, moves, and deletions. + + ![Ente - Continuous sync](continuous-sync.webp) - ![Ente - Rexport](export-4.png) -In case your download gets interrupted, Ente will resume from where it left off. -Simply select **export data** again and click on **resync**. If you run into any issues during your data export, please reach out to [support@ente.io](mailto:support@ente.io) and we will be happy to help you! + +Note that we also provide a [CLI +tool](https://github.com/ente-io/ente/tree/main/cli#export) to export your data. +Some more details are in this [FAQ entry](/photos/faq/export). diff --git a/docs/docs/photos/migration/export/sign-in.png b/docs/docs/photos/migration/export/sign-in.png new file mode 100644 index 0000000000..4a9fa4a236 Binary files /dev/null and b/docs/docs/photos/migration/export/sign-in.png differ diff --git a/docs/docs/self-hosting/faq/index.md b/docs/docs/self-hosting/faq/index.md new file mode 100644 index 0000000000..622e74a242 --- /dev/null +++ b/docs/docs/self-hosting/faq/index.md @@ -0,0 +1,33 @@ +--- +title: FAQ - Self hosting +description: Frequently asked questions about self hosting Ente +--- + +# Frequently Asked Questions + +### Do Ente Photos and Ente Auth share the same backend? + +Yes. The apps share the same backend, the same database and the same object +storage namespace. The same user account works for both of them. + +### Can I just self host Ente Auth? + +Yes, if you wish, you can self-host the server and use it only for the 2FA auth +app. The starter Docker compose will work fine for either Photos or Auth (or +both!) + +### Can I use the server with _X_ as the object storage? + +Yes. As long as whatever X you're using provides an S3 compatible API, you can +use it as the underlying object storage. For example, the starter self-hosting +Docker compose file we offer uses MinIO, and on our production deployments we +use Backblaze/Wasabi/Scaleway. But that's not the full list - as long as the +service you intend to use has a S3 compatible API, it can be used. + +### How do I increase storage space for users on my self hosted instance? + +See the [guide for administering your server](/self-hosting/guides/admin). In +particular, you can use the `ente admin update-subscription` CLI command to +increase the +[storage and account validity](https://github.com/ente-io/ente/blob/main/cli/docs/generated/ente_admin_update-subscription.md) +of accounts on your instance. diff --git a/docs/docs/self-hosting/faq/otp.md b/docs/docs/self-hosting/faq/otp.md index a224d1f668..1ffe860e4b 100644 --- a/docs/docs/self-hosting/faq/otp.md +++ b/docs/docs/self-hosting/faq/otp.md @@ -1,6 +1,6 @@ --- title: Verification code -description: Getting the OTP for a self host Ente +description: Getting the OTP for a self hosted Ente --- # Verification code diff --git a/docs/docs/self-hosting/faq/storage-space.md b/docs/docs/self-hosting/faq/storage-space.md deleted file mode 100644 index f1ad78c718..0000000000 --- a/docs/docs/self-hosting/faq/storage-space.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -title: Increase storage space -description: Increasing the storage quota for users on your self hosted instance ---- - -# Increase storage space - -See the [guide for administering your server](/self-hosting/guides/admin). In -particular, you can use the `ente admin update-subscription` CLI command to -increase the -[storage and account validity](https://github.com/ente-io/ente/blob/main/cli/docs/generated/ente_admin_update-subscription.md) -of accounts on your instance. diff --git a/docs/docs/self-hosting/index.md b/docs/docs/self-hosting/index.md index 53db6ab299..c1ae7075e9 100644 --- a/docs/docs/self-hosting/index.md +++ b/docs/docs/self-hosting/index.md @@ -24,6 +24,11 @@ cd ente/server docker compose up --build ``` +> [!TIP] +> +> You can also use a pre-built Docker image from `ghcr.io/ente-io/server` ([More +> info](https://github.com/ente-io/ente/blob/main/server/docs/docker.md)) + Then in a separate terminal, you can run (e.g) the web client ```sh @@ -42,7 +47,7 @@ For the mobile apps, you don't even need to build, and can install normal Ente apps and configure them to use your [custom self-hosted server](guides/custom-server/). -> If you want to build from source, see the instructions +> If you want to build the mobile apps from source, see the instructions > [here](guides/mobile-build). ## Next steps diff --git a/infra/workers/github-discord-notifier/src/index.ts b/infra/workers/github-discord-notifier/src/index.ts index 19191d9587..4e67c8469b 100644 --- a/infra/workers/github-discord-notifier/src/index.ts +++ b/infra/workers/github-discord-notifier/src/index.ts @@ -65,7 +65,10 @@ const handleRequest = async (request: Request, discordWebhookURL: string) => { // arrangement (we shouldn't be getting 429s forever), so just try to // see if we can extract a URL from something we recognize. let activityURL: string | undefined; - if (requestJSON["issue"]) { + if (requestJSON["comment"]) { + activityURL = requestJSON["comment"]["html_url"]; + } + if (!activityURL && requestJSON["issue"]) { activityURL = requestJSON["issue"]["html_url"]; } if (!activityURL && requestJSON["discussion"]) { diff --git a/mobile/CHANGELOG.md b/mobile/CHANGELOG.md index 445bb9147f..a4b33abdf4 100644 --- a/mobile/CHANGELOG.md +++ b/mobile/CHANGELOG.md @@ -1,5 +1,16 @@ # CHANGELOG +## v0.8.72 +### Added +* #### Share an Album to Multiple Contacts at Once + + Adding multiple viewers and collaborators just got easier! + You can now select multiple contacts and add all of them at once. + +* #### Bug Fixes and Performance Improvements + + Many a bugs were squashed in this release. If you run into any, please write to team@ente.io, or let us know on Discord! 🙏 + ## v0.8.67 @@ -85,22 +96,3 @@ If you would like to help us improve ente, come join the party @ ente.io/community! -## v0.7.71 - -### Added -* #### Map View ✨ - - You can now explore the photos you've taken around the world! - - Click on the Map icon on the Search screen to view your photos laid out on a map. - -* #### Cover Photos ✨ - You can now set cover photos for your albums. - - Open an album, and click on the overflow menu on the top right corner to pick your favorite memory from that album. - -### Improvements - -* **Translations**: Add support for German language -* This release contains massive improvements to how smoothly our gallery - scrolls. More improvements are on the way! diff --git a/mobile/fastlane/metadata/android/zh/full_description.txt b/mobile/fastlane/metadata/android/zh/full_description.txt index 3660c62591..2fffa8a0c7 100644 --- a/mobile/fastlane/metadata/android/zh/full_description.txt +++ b/mobile/fastlane/metadata/android/zh/full_description.txt @@ -4,7 +4,7 @@ ente 是一个简单的应用程序来备份和分享您的照片和视频。 我们在Android、iOS、web 和桌面上有开源应用, 和您的照片将以端到端加密方式 (e2ee) 无缝同步。 -ente也使分享相册给自己的爱人、亲人变得轻而易举,即使他们可能并不使用ente。 您可以分享可公开查看的链接,使他们可以查看您的相册,并通过添加照片来协作而不需要注册账户或下载app。 ente也使分享相册给自己的爱人、亲人变得轻而易举,即使他们可能并不使用ente。 您可以分享可公开查看的链接,使他们可以查看您的相册,并通过添加照片来协作而不需要注册账户或下载app。 权限 +ente也使分享相册给自己的爱人、亲人变得轻而易举,即使他们可能并不使用ente。 您可以分享可公开查看的链接,使他们可以查看您的相册,并通过添加照片来协作而不需要注册账户或下载app。 您可以共享公开可见的链接,他们可以在其中查看您的相册并通过向其中添加照片进行协作,即使没有账户或应用程序也是如此。 您的加密数据已复制到三个不同的地点,包括巴黎的一个安全屋。 我们认真对待子孙后代,并确保您的回忆比您长寿。 我们认真对待子孙后代,并确保您的回忆比您长寿。 diff --git a/mobile/gallery_scroll_perf_test.sh b/mobile/gallery_scroll_perf_test.sh new file mode 100755 index 0000000000..3faee8c2d8 --- /dev/null +++ b/mobile/gallery_scroll_perf_test.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +# Make sure to go through home_gallery_scroll_test.dart and +# fill in email and password. +# Specify destination directory for the perf results in perf_driver.dart. + + +export ENDPOINT="https://api.ente.io" + +flutter drive \ + --driver=test_driver/perf_driver.dart \ + --target=integration_test/home_gallery_scroll_test.dart \ + --dart-define=endpoint=$ENDPOINT \ + --profile --flavor independent \ + --no-dds + +exit $? diff --git a/mobile/integration_test/app_test.dart b/mobile/integration_test/app_test.dart deleted file mode 100644 index b0b16d46d0..0000000000 --- a/mobile/integration_test/app_test.dart +++ /dev/null @@ -1,122 +0,0 @@ -import "package:flutter/material.dart"; -import "package:flutter_test/flutter_test.dart"; -import "package:integration_test/integration_test.dart"; -import "package:photos/main.dart" as app; -import "package:scrollable_positioned_list/scrollable_positioned_list.dart"; - -void main() { - group("App test", () { - final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - binding.framePolicy = LiveTestWidgetsFlutterBindingFramePolicy.fullyLive; - testWidgets("Demo test", (tester) async { - app.main(); - - await tester.pumpAndSettle(const Duration(seconds: 5)); - - await dismissUpdateAppDialog(tester); - - //Automatically clicks the sign in button on the landing page - final signInButton = find.byKey(const ValueKey("signInButton")); - await tester.tap(signInButton); - await tester.pumpAndSettle(); - - //Need to enter email address manually and clicks the login button automatically - final emailInputField = find.byKey(const ValueKey("emailInputField")); - final logInButton = find.byKey(const ValueKey("logInButton")); - await tester.tap(emailInputField); - await tester.pumpAndSettle(const Duration(seconds: 12)); - await findAndTapFAB(tester, logInButton); - - //Need to enter OTT manually and clicks the verify button automatically - final ottVerificationInputField = - find.byKey(const ValueKey("ottVerificationInputField")); - final verifyOttButton = find.byKey(const ValueKey("verifyOttButton")); - await tester.tap(ottVerificationInputField); - await tester.pumpAndSettle(const Duration(seconds: 6)); - await findAndTapFAB(tester, verifyOttButton); - - //Need to enter password manually and clicks the verify button automatically - final passwordInputField = - find.byKey(const ValueKey("passwordInputField")); - final verifyPasswordButton = - find.byKey(const ValueKey("verifyPasswordButton")); - await tester.tap(passwordInputField); - await tester.pumpAndSettle(const Duration(seconds: 10)); - await findAndTapFAB(tester, verifyPasswordButton); - - await tester.pumpAndSettle(const Duration(seconds: 1)); - await dismissUpdateAppDialog(tester); - - //Grant permission to access photos. Must manually click the system dialog. - final grantPermissionButton = - find.byKey(const ValueKey("grantPermissionButton")); - await tester.tap(grantPermissionButton); - await tester.pumpAndSettle(const Duration(seconds: 1)); - await tester.pumpAndSettle(const Duration(seconds: 3)); - - //Automatically skips backup - final skipBackupButton = find.byKey(const ValueKey("skipBackupButton")); - await tester.tap(skipBackupButton); - await tester.pumpAndSettle(const Duration(seconds: 2)); - - await binding.traceAction( - () async { - //scroll gallery - final scrollablePositionedList = - find.byType(ScrollablePositionedList); - await tester.fling( - scrollablePositionedList, - const Offset(0, -5000), - 4500, - ); - await tester.pumpAndSettle(); - await tester.fling( - scrollablePositionedList, - const Offset(0, 5000), - 4500, - ); - - await tester.fling( - scrollablePositionedList, - const Offset(0, -7000), - 4500, - ); - await tester.pumpAndSettle(); - await tester.fling( - scrollablePositionedList, - const Offset(0, 7000), - 4500, - ); - - await tester.fling( - scrollablePositionedList, - const Offset(0, -9000), - 4500, - ); - await tester.pumpAndSettle(); - await tester.fling( - scrollablePositionedList, - const Offset(0, 9000), - 4500, - ); - await tester.pumpAndSettle(); - }, - reportKey: 'scrolling_summary', - ); - }); - }); -} - -Future findAndTapFAB(WidgetTester tester, Finder finder) async { - final RenderBox box = tester.renderObject(finder); - final Offset desiredOffset = Offset(box.size.width - 10, box.size.height / 2); - // Calculate the global position of the desired offset within the widget. - final Offset globalPosition = box.localToGlobal(desiredOffset); - await tester.tapAt(globalPosition); - await tester.pumpAndSettle(const Duration(seconds: 3)); -} - -Future dismissUpdateAppDialog(WidgetTester tester) async { - await tester.tapAt(const Offset(0, 0)); - await tester.pumpAndSettle(); -} diff --git a/mobile/integration_test/home_gallery_scroll_test.dart b/mobile/integration_test/home_gallery_scroll_test.dart new file mode 100644 index 0000000000..3a87b6856c --- /dev/null +++ b/mobile/integration_test/home_gallery_scroll_test.dart @@ -0,0 +1,127 @@ +import "dart:async"; + +import "package:flutter/material.dart"; +import "package:flutter_test/flutter_test.dart"; +import "package:integration_test/integration_test.dart"; +import "package:logging/logging.dart"; +import "package:photos/main.dart" as app; +import "package:scrollable_positioned_list/scrollable_positioned_list.dart"; + +void main() { + group("Home gallery scroll test", () { + final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + binding.framePolicy = LiveTestWidgetsFlutterBindingFramePolicy.fullyLive; + testWidgets("Home gallery scroll test", semanticsEnabled: false, + (tester) async { + // https://github.com/flutter/flutter/issues/89749#issuecomment-1029965407 + tester.testTextInput.register(); + + await runZonedGuarded( + () async { + ///Ignore exceptions thrown by the app for the test to pass + WidgetsFlutterBinding.ensureInitialized(); + FlutterError.onError = (FlutterErrorDetails errorDetails) { + FlutterError.dumpErrorToConsole(errorDetails); + }; + + app.main(); + + await tester.pumpAndSettle(const Duration(seconds: 1)); + + await dismissUpdateAppDialog(tester); + + final signInButton = find.byKey(const ValueKey("signInButton")); + await tester.tap(signInButton); + await tester.pumpAndSettle(); + + final emailInputField = find.byType(TextFormField); + final logInButton = find.byKey(const ValueKey("logInButton")); + //Fill email id here + await tester.enterText(emailInputField, "enter email here"); + await tester.pumpAndSettle(const Duration(seconds: 1)); + await tester.tap(logInButton); + await tester.pumpAndSettle(const Duration(seconds: 3)); + + final passwordInputField = + find.byKey(const ValueKey("passwordInputField")); + final verifyPasswordButton = + find.byKey(const ValueKey("verifyPasswordButton")); + //Fill password here + await tester.enterText(passwordInputField, "enter password here"); + await tester.pumpAndSettle(const Duration(seconds: 1)); + await tester.tap(verifyPasswordButton); + await tester.pumpAndSettle(); + + await tester.pumpAndSettle(const Duration(seconds: 1)); + await dismissUpdateAppDialog(tester); + + //Grant permission to access photos. Must manually click the system dialog. + final grantPermissionButton = + find.byKey(const ValueKey("grantPermissionButton")); + await tester.tap(grantPermissionButton); + await tester.pumpAndSettle(const Duration(seconds: 1)); + await tester.pumpAndSettle(const Duration(seconds: 3)); + + //Automatically skips backup + final skipBackupButton = + find.byKey(const ValueKey("skipBackupButton")); + await tester.tap(skipBackupButton); + await tester.pumpAndSettle(const Duration(seconds: 2)); + + await binding.traceAction( + () async { + //scroll gallery + final scrollablePositionedList = + find.byType(ScrollablePositionedList); + await tester.fling( + scrollablePositionedList, + const Offset(0, -5000), + 4500, + ); + await tester.pumpAndSettle(); + await tester.fling( + scrollablePositionedList, + const Offset(0, 5000), + 4500, + ); + + await tester.fling( + scrollablePositionedList, + const Offset(0, -7000), + 4500, + ); + await tester.pumpAndSettle(); + await tester.fling( + scrollablePositionedList, + const Offset(0, 7000), + 4500, + ); + + await tester.fling( + scrollablePositionedList, + const Offset(0, -9000), + 4500, + ); + await tester.pumpAndSettle(); + await tester.fling( + scrollablePositionedList, + const Offset(0, 9000), + 4500, + ); + await tester.pumpAndSettle(); + }, + reportKey: 'home_gallery_scrolling_summary', + ); + }, + (error, stack) { + Logger("gallery_scroll_test").info(error, stack); + }, + ); + }); + }); +} + +Future dismissUpdateAppDialog(WidgetTester tester) async { + await tester.tapAt(const Offset(0, 0)); + await tester.pumpAndSettle(); +} diff --git a/mobile/lib/db/file_updation_db.dart b/mobile/lib/db/file_updation_db.dart index 6496b71ed3..426cc7a526 100644 --- a/mobile/lib/db/file_updation_db.dart +++ b/mobile/lib/db/file_updation_db.dart @@ -15,6 +15,7 @@ class FileUpdationDB { static const columnLocalID = 'local_id'; static const columnReason = 'reason'; static const livePhotoCheck = 'livePhotoCheck'; + static const androidMissingGPS = 'androidMissingGPS'; static const modificationTimeUpdated = 'modificationTimeUpdated'; diff --git a/mobile/lib/db/files_db.dart b/mobile/lib/db/files_db.dart index 39ff832103..62122a29ba 100644 --- a/mobile/lib/db/files_db.dart +++ b/mobile/lib/db/files_db.dart @@ -1533,6 +1533,24 @@ class FilesDB { return result; } + Future> getLocalFilesBackedUpWithoutLocation(int userId) async { + final db = await instance.database; + final rows = await db.query( + filesTable, + columns: [columnLocalID], + distinct: true, + where: + '$columnOwnerID = ? AND $columnLocalID IS NOT NULL AND ($columnUploadedFileID IS NOT NULL AND $columnUploadedFileID IS NOT -1) ' + 'AND ($columnLatitude IS NULL OR $columnLongitude IS NULL OR $columnLongitude = 0.0 or $columnLongitude = 0.0)', + whereArgs: [userId], + ); + final result = []; + for (final row in rows) { + result.add(row[columnLocalID] as String); + } + return result; + } + // updateSizeForUploadIDs takes a map of upploadedFileID and fileSize and // update the fileSize for the given uploadedFileID Future updateSizeForUploadIDs( diff --git a/mobile/lib/generated/intl/messages_de.dart b/mobile/lib/generated/intl/messages_de.dart index ca3be9e414..39fdb3b961 100644 --- a/mobile/lib/generated/intl/messages_de.dart +++ b/mobile/lib/generated/intl/messages_de.dart @@ -131,90 +131,87 @@ class MessageLookup extends MessageLookupByLibrary { static String m37(providerName) => "Bitte kontaktiere den Support von ${providerName}, falls etwas abgebucht wurde"; - static String m38(reason) => - "Leider ist deine Zahlung aus folgendem Grund fehlgeschlagen: ${reason}"; - - static String m39(endDate) => + static String m38(endDate) => "Kostenlose Testversion gültig bis ${endDate}.\nSie können anschließend ein bezahltes Paket auswählen."; - static String m40(toEmail) => "Bitte sende uns eine E-Mail an ${toEmail}"; + static String m39(toEmail) => "Bitte sende uns eine E-Mail an ${toEmail}"; - static String m41(toEmail) => "Bitte sende die Protokolle an ${toEmail}"; + static String m40(toEmail) => "Bitte sende die Protokolle an ${toEmail}"; - static String m42(storeName) => "Bewerte uns auf ${storeName}"; + static String m41(storeName) => "Bewerte uns auf ${storeName}"; - static String m43(storageInGB) => + static String m42(storageInGB) => "3. Ihr beide erhaltet ${storageInGB} GB* kostenlos"; - static String m44(userEmail) => + static String m43(userEmail) => "${userEmail} wird aus diesem geteilten Album entfernt\n\nAlle von ihnen hinzugefügte Fotos werden ebenfalls aus dem Album entfernt"; - static String m45(endDate) => "Erneuert am ${endDate}"; + static String m44(endDate) => "Erneuert am ${endDate}"; - static String m46(count) => + static String m45(count) => "${Intl.plural(count, one: '${count} Ergebnis gefunden', other: '${count} Ergebnisse gefunden')}"; - static String m47(count) => "${count} ausgewählt"; + static String m46(count) => "${count} ausgewählt"; - static String m48(count, yourCount) => + static String m47(count, yourCount) => "${count} ausgewählt (${yourCount} von Ihnen)"; - static String m49(verificationID) => + static String m48(verificationID) => "Hier ist meine Verifizierungs-ID: ${verificationID} für ente.io."; - static String m50(verificationID) => + static String m49(verificationID) => "Hey, kannst du bestätigen, dass dies deine ente.io Verifizierungs-ID ist: ${verificationID}"; - static String m51(referralCode, referralStorageInGB) => + static String m50(referralCode, referralStorageInGB) => "ente Weiterempfehlungs-Code: ${referralCode} \n\nEinlösen unter Einstellungen → Allgemein → Weiterempfehlungen, um ${referralStorageInGB} GB kostenlos zu erhalten, sobald Sie einen kostenpflichtigen Tarif abgeschlossen haben\n\nhttps://ente.io"; - static String m52(numberOfPeople) => + static String m51(numberOfPeople) => "${Intl.plural(numberOfPeople, zero: 'Teile mit bestimmten Personen', one: 'Teilen mit 1 Person', other: 'Teilen mit ${numberOfPeople} Personen')}"; - static String m53(emailIDs) => "Geteilt mit ${emailIDs}"; + static String m52(emailIDs) => "Geteilt mit ${emailIDs}"; - static String m54(fileType) => + static String m53(fileType) => "Dieses ${fileType} wird von deinem Gerät gelöscht."; - static String m55(fileType) => + static String m54(fileType) => "Dieses ${fileType} existiert auf ente.io und deinem Gerät."; - static String m56(fileType) => + static String m55(fileType) => "Dieses ${fileType} wird auf ente.io gelöscht."; - static String m57(storageAmountInGB) => "${storageAmountInGB} GB"; + static String m56(storageAmountInGB) => "${storageAmountInGB} GB"; - static String m58( + static String m57( usedAmount, usedStorageUnit, totalAmount, totalStorageUnit) => "${usedAmount} ${usedStorageUnit} von ${totalAmount} ${totalStorageUnit} verwendet"; - static String m59(id) => + static String m58(id) => "Ihr ${id} ist bereits mit einem anderen \'ente\'-Konto verknüpft.\nWenn Sie Ihre ${id} mit diesem Konto verwenden möchten, kontaktieren Sie bitte unseren Support\'"; - static String m60(endDate) => "Ihr Abo endet am ${endDate}"; + static String m59(endDate) => "Ihr Abo endet am ${endDate}"; - static String m61(completed, total) => + static String m60(completed, total) => "${completed}/${total} Erinnerungsstücke gesichert"; - static String m62(storageAmountInGB) => + static String m61(storageAmountInGB) => "Diese erhalten auch ${storageAmountInGB} GB"; - static String m63(email) => "Dies ist ${email}s Verifizierungs-ID"; + static String m62(email) => "Dies ist ${email}s Verifizierungs-ID"; - static String m64(count) => + static String m63(count) => "${Intl.plural(count, zero: '', one: '1 Tag', other: '${count} Tage')}"; - static String m65(endDate) => "Gültig bis ${endDate}"; + static String m64(endDate) => "Gültig bis ${endDate}"; - static String m66(email) => "Verifiziere ${email}"; + static String m65(email) => "Verifiziere ${email}"; - static String m67(email) => + static String m66(email) => "Wir haben eine E-Mail an ${email} gesendet"; - static String m68(count) => + static String m67(count) => "${Intl.plural(count, one: 'vor einem Jahr', other: 'vor ${count} Jahren')}"; - static String m69(storageSaved) => + static String m68(storageSaved) => "Du hast ${storageSaved} erfolgreich freigegeben!"; final messages = _notInlinedMessages(_notInlinedMessages); @@ -1026,7 +1023,6 @@ class MessageLookup extends MessageLookupByLibrary { "paymentFailed": MessageLookupByLibrary.simpleMessage("Zahlung fehlgeschlagen"), "paymentFailedTalkToProvider": m37, - "paymentFailedWithReason": m38, "pendingItems": MessageLookupByLibrary.simpleMessage("Ausstehende Elemente"), "pendingSync": @@ -1053,7 +1049,7 @@ class MessageLookup extends MessageLookupByLibrary { "pinAlbum": MessageLookupByLibrary.simpleMessage("Album anheften"), "playOnTv": MessageLookupByLibrary.simpleMessage( "Album auf dem Fernseher wiedergeben"), - "playStoreFreeTrialValidTill": m39, + "playStoreFreeTrialValidTill": m38, "playstoreSubscription": MessageLookupByLibrary.simpleMessage("PlayStore Abo"), "pleaseCheckYourInternetConnectionAndTryAgain": @@ -1065,12 +1061,12 @@ class MessageLookup extends MessageLookupByLibrary { "pleaseContactSupportIfTheProblemPersists": MessageLookupByLibrary.simpleMessage( "Bitte wenden Sie sich an den Support, falls das Problem weiterhin besteht"), - "pleaseEmailUsAt": m40, + "pleaseEmailUsAt": m39, "pleaseGrantPermissions": MessageLookupByLibrary.simpleMessage( "Bitte erteile die nötigen Berechtigungen"), "pleaseLoginAgain": MessageLookupByLibrary.simpleMessage("Bitte logge dich erneut ein"), - "pleaseSendTheLogsTo": m41, + "pleaseSendTheLogsTo": m40, "pleaseTryAgain": MessageLookupByLibrary.simpleMessage("Bitte versuche es erneut"), "pleaseVerifyTheCodeYouHaveEntered": @@ -1106,7 +1102,7 @@ class MessageLookup extends MessageLookupByLibrary { "raiseTicket": MessageLookupByLibrary.simpleMessage("Ticket erstellen"), "rateTheApp": MessageLookupByLibrary.simpleMessage("App bewerten"), "rateUs": MessageLookupByLibrary.simpleMessage("Bewerte uns"), - "rateUsOnStore": m42, + "rateUsOnStore": m41, "recover": MessageLookupByLibrary.simpleMessage("Wiederherstellen"), "recoverAccount": MessageLookupByLibrary.simpleMessage("Konto wiederherstellen"), @@ -1139,7 +1135,7 @@ class MessageLookup extends MessageLookupByLibrary { "1. Gib diesen Code an deine Freunde"), "referralStep2": MessageLookupByLibrary.simpleMessage( "2. Sie schließen ein bezahltes Abo ab"), - "referralStep3": m43, + "referralStep3": m42, "referrals": MessageLookupByLibrary.simpleMessage("Weiterempfehlungen"), "referralsAreCurrentlyPaused": MessageLookupByLibrary.simpleMessage( "Einlösungen sind derzeit pausiert"), @@ -1165,7 +1161,7 @@ class MessageLookup extends MessageLookupByLibrary { "removeLink": MessageLookupByLibrary.simpleMessage("Link entfernen"), "removeParticipant": MessageLookupByLibrary.simpleMessage("Teilnehmer entfernen"), - "removeParticipantBody": m44, + "removeParticipantBody": m43, "removePublicLink": MessageLookupByLibrary.simpleMessage("Öffentlichen Link entfernen"), "removeShareItemsWarning": MessageLookupByLibrary.simpleMessage( @@ -1179,7 +1175,7 @@ class MessageLookup extends MessageLookupByLibrary { "renameFile": MessageLookupByLibrary.simpleMessage("Datei umbenennen"), "renewSubscription": MessageLookupByLibrary.simpleMessage("Abonnement erneuern"), - "renewsOn": m45, + "renewsOn": m44, "reportABug": MessageLookupByLibrary.simpleMessage("Fehler melden"), "reportBug": MessageLookupByLibrary.simpleMessage("Fehler melden"), "resendEmail": @@ -1242,7 +1238,7 @@ class MessageLookup extends MessageLookupByLibrary { "Gruppiere Fotos, die innerhalb des Radius eines bestimmten Fotos aufgenommen wurden"), "searchPeopleEmptySection": MessageLookupByLibrary.simpleMessage( "Laden Sie Personen ein, damit Sie geteilte Fotos hier einsehen können"), - "searchResultCount": m46, + "searchResultCount": m45, "security": MessageLookupByLibrary.simpleMessage("Sicherheit"), "selectALocation": MessageLookupByLibrary.simpleMessage("Standort auswählen"), @@ -1269,8 +1265,8 @@ class MessageLookup extends MessageLookupByLibrary { "selectedItemsWillBeDeletedFromAllAlbumsAndMoved": MessageLookupByLibrary.simpleMessage( "Ausgewählte Elemente werden aus allen Alben gelöscht und in den Papierkorb verschoben."), - "selectedPhotos": m47, - "selectedPhotosWithYours": m48, + "selectedPhotos": m46, + "selectedPhotosWithYours": m47, "send": MessageLookupByLibrary.simpleMessage("Absenden"), "sendEmail": MessageLookupByLibrary.simpleMessage("E-Mail senden"), "sendInvite": MessageLookupByLibrary.simpleMessage("Einladung senden"), @@ -1293,16 +1289,16 @@ class MessageLookup extends MessageLookupByLibrary { "shareAnAlbumNow": MessageLookupByLibrary.simpleMessage("Teile jetzt ein Album"), "shareLink": MessageLookupByLibrary.simpleMessage("Link teilen"), - "shareMyVerificationID": m49, + "shareMyVerificationID": m48, "shareOnlyWithThePeopleYouWant": MessageLookupByLibrary.simpleMessage( "Teile mit ausgewählten Personen"), - "shareTextConfirmOthersVerificationID": m50, + "shareTextConfirmOthersVerificationID": m49, "shareTextRecommendUsingEnte": MessageLookupByLibrary.simpleMessage( "Lade ente herunter, damit wir einfach Fotos und Videos in höchster Qualität teilen können\n\nhttps://ente.io"), - "shareTextReferralCode": m51, + "shareTextReferralCode": m50, "shareWithNonenteUsers": MessageLookupByLibrary.simpleMessage( "Mit Nicht-Ente-Benutzern teilen"), - "shareWithPeopleSectionTitle": m52, + "shareWithPeopleSectionTitle": m51, "shareYourFirstAlbum": MessageLookupByLibrary.simpleMessage("Teile dein erstes Album"), "sharedAlbumSectionDescription": MessageLookupByLibrary.simpleMessage( @@ -1313,7 +1309,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Neue geteilte Fotos"), "sharedPhotoNotificationsExplanation": MessageLookupByLibrary.simpleMessage( "Erhalte Benachrichtigungen, wenn jemand ein Foto zu einem gemeinsam genutzten Album hinzufügt, dem du angehörst"), - "sharedWith": m53, + "sharedWith": m52, "sharedWithMe": MessageLookupByLibrary.simpleMessage("Mit mir geteilt"), "sharedWithYou": MessageLookupByLibrary.simpleMessage("Mit dir geteilt"), @@ -1328,11 +1324,11 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Andere Geräte abmelden"), "signUpTerms": MessageLookupByLibrary.simpleMessage( "Ich stimme den Nutzungsbedingungen und der Datenschutzerklärung zu"), - "singleFileDeleteFromDevice": m54, + "singleFileDeleteFromDevice": m53, "singleFileDeleteHighlight": MessageLookupByLibrary.simpleMessage( "Es wird aus allen Alben gelöscht."), - "singleFileInBothLocalAndRemote": m55, - "singleFileInRemoteOnly": m56, + "singleFileInBothLocalAndRemote": m54, + "singleFileInRemoteOnly": m55, "skip": MessageLookupByLibrary.simpleMessage("Überspringen"), "social": MessageLookupByLibrary.simpleMessage("Social Media"), "someItemsAreInBothEnteAndYourDevice": @@ -1374,13 +1370,13 @@ class MessageLookup extends MessageLookupByLibrary { "storage": MessageLookupByLibrary.simpleMessage("Speicherplatz"), "storageBreakupFamily": MessageLookupByLibrary.simpleMessage("Familie"), "storageBreakupYou": MessageLookupByLibrary.simpleMessage("Sie"), - "storageInGB": m57, + "storageInGB": m56, "storageLimitExceeded": MessageLookupByLibrary.simpleMessage( "Speichergrenze überschritten"), - "storageUsageInfo": m58, + "storageUsageInfo": m57, "strongStrength": MessageLookupByLibrary.simpleMessage("Stark"), - "subAlreadyLinkedErrMessage": m59, - "subWillBeCancelledOn": m60, + "subAlreadyLinkedErrMessage": m58, + "subWillBeCancelledOn": m59, "subscribe": MessageLookupByLibrary.simpleMessage("Abonnieren"), "subscribeToEnableSharing": MessageLookupByLibrary.simpleMessage( "Sieht aus, als sei dein Abonnement abgelaufen. Bitte abonniere, um das Teilen zu aktivieren."), @@ -1397,7 +1393,7 @@ class MessageLookup extends MessageLookupByLibrary { "suggestFeatures": MessageLookupByLibrary.simpleMessage("Verbesserung vorschlagen"), "support": MessageLookupByLibrary.simpleMessage("Support"), - "syncProgress": m61, + "syncProgress": m60, "syncStopped": MessageLookupByLibrary.simpleMessage("Synchronisierung angehalten"), "syncing": MessageLookupByLibrary.simpleMessage("Synchronisiere …"), @@ -1426,7 +1422,7 @@ class MessageLookup extends MessageLookupByLibrary { "theseItemsWillBeDeletedFromYourDevice": MessageLookupByLibrary.simpleMessage( "Diese Elemente werden von deinem Gerät gelöscht."), - "theyAlsoGetXGb": m62, + "theyAlsoGetXGb": m61, "theyWillBeDeletedFromAllAlbums": MessageLookupByLibrary.simpleMessage( "Sie werden aus allen Alben gelöscht."), "thisActionCannotBeUndone": MessageLookupByLibrary.simpleMessage( @@ -1442,7 +1438,7 @@ class MessageLookup extends MessageLookupByLibrary { "Diese E-Mail-Adresse wird bereits verwendet"), "thisImageHasNoExifData": MessageLookupByLibrary.simpleMessage( "Dieses Bild hat keine Exif-Daten"), - "thisIsPersonVerificationId": m63, + "thisIsPersonVerificationId": m62, "thisIsYourVerificationId": MessageLookupByLibrary.simpleMessage( "Dies ist deine Verifizierungs-ID"), "thisWillLogYouOutOfTheFollowingDevice": @@ -1459,7 +1455,7 @@ class MessageLookup extends MessageLookupByLibrary { "total": MessageLookupByLibrary.simpleMessage("Gesamt"), "totalSize": MessageLookupByLibrary.simpleMessage("Gesamtgröße"), "trash": MessageLookupByLibrary.simpleMessage("Papierkorb"), - "trashDaysLeft": m64, + "trashDaysLeft": m63, "tryAgain": MessageLookupByLibrary.simpleMessage("Erneut versuchen"), "turnOnBackupForAutoUpload": MessageLookupByLibrary.simpleMessage( "Aktiviere die Sicherung, um automatisch neu hinzugefügte Dateien dieses Ordners auf ente hochzuladen."), @@ -1514,7 +1510,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Ausgewähltes Foto verwenden"), "usedSpace": MessageLookupByLibrary.simpleMessage("Belegter Speicherplatz"), - "validTill": m65, + "validTill": m64, "verificationFailedPleaseTryAgain": MessageLookupByLibrary.simpleMessage( "Verifizierung fehlgeschlagen, bitte versuchen Sie es erneut"), @@ -1523,7 +1519,7 @@ class MessageLookup extends MessageLookupByLibrary { "verify": MessageLookupByLibrary.simpleMessage("Überprüfen"), "verifyEmail": MessageLookupByLibrary.simpleMessage("E-Mail-Adresse verifizieren"), - "verifyEmailID": m66, + "verifyEmailID": m65, "verifyIDLabel": MessageLookupByLibrary.simpleMessage("Überprüfen"), "verifyPasskey": MessageLookupByLibrary.simpleMessage("Passkey verifizieren"), @@ -1556,12 +1552,12 @@ class MessageLookup extends MessageLookupByLibrary { "weDontSupportEditingPhotosAndAlbumsThatYouDont": MessageLookupByLibrary.simpleMessage( "Wir unterstützen keine Bearbeitung von Fotos und Alben, die du noch nicht besitzt"), - "weHaveSendEmailTo": m67, + "weHaveSendEmailTo": m66, "weakStrength": MessageLookupByLibrary.simpleMessage("Schwach"), "welcomeBack": MessageLookupByLibrary.simpleMessage("Willkommen zurück!"), "yearly": MessageLookupByLibrary.simpleMessage("Jährlich"), - "yearsAgo": m68, + "yearsAgo": m67, "yes": MessageLookupByLibrary.simpleMessage("Ja"), "yesCancel": MessageLookupByLibrary.simpleMessage("Ja, kündigen"), "yesConvertToViewer": MessageLookupByLibrary.simpleMessage( @@ -1591,7 +1587,7 @@ class MessageLookup extends MessageLookupByLibrary { "Du kannst nicht mit dir selbst teilen"), "youDontHaveAnyArchivedItems": MessageLookupByLibrary.simpleMessage( "Du hast keine archivierten Elemente."), - "youHaveSuccessfullyFreedUp": m69, + "youHaveSuccessfullyFreedUp": m68, "yourAccountHasBeenDeleted": MessageLookupByLibrary.simpleMessage( "Dein Benutzerkonto wurde gelöscht"), "yourMap": MessageLookupByLibrary.simpleMessage("Deine Karte"), diff --git a/mobile/lib/generated/intl/messages_en.dart b/mobile/lib/generated/intl/messages_en.dart index 624a7cfb24..b208717fa4 100644 --- a/mobile/lib/generated/intl/messages_en.dart +++ b/mobile/lib/generated/intl/messages_en.dart @@ -129,89 +129,86 @@ class MessageLookup extends MessageLookupByLibrary { static String m37(providerName) => "Please talk to ${providerName} support if you were charged"; - static String m38(reason) => - "Unfortunately your payment failed due to ${reason}"; - - static String m39(endDate) => + static String m38(endDate) => "Free trial valid till ${endDate}.\nYou can choose a paid plan afterwards."; - static String m40(toEmail) => "Please email us at ${toEmail}"; + static String m39(toEmail) => "Please email us at ${toEmail}"; - static String m41(toEmail) => "Please send the logs to \n${toEmail}"; + static String m40(toEmail) => "Please send the logs to \n${toEmail}"; - static String m42(storeName) => "Rate us on ${storeName}"; + static String m41(storeName) => "Rate us on ${storeName}"; - static String m43(storageInGB) => + static String m42(storageInGB) => "3. Both of you get ${storageInGB} GB* free"; - static String m44(userEmail) => + static String m43(userEmail) => "${userEmail} will be removed from this shared album\n\nAny photos added by them will also be removed from the album"; - static String m45(endDate) => "Subscription renews on ${endDate}"; + static String m44(endDate) => "Subscription renews on ${endDate}"; - static String m46(count) => + static String m45(count) => "${Intl.plural(count, one: '${count} result found', other: '${count} results found')}"; - static String m47(count) => "${count} selected"; + static String m46(count) => "${count} selected"; - static String m48(count, yourCount) => + static String m47(count, yourCount) => "${count} selected (${yourCount} yours)"; - static String m49(verificationID) => + static String m48(verificationID) => "Here\'s my verification ID: ${verificationID} for ente.io."; - static String m50(verificationID) => + static String m49(verificationID) => "Hey, can you confirm that this is your ente.io verification ID: ${verificationID}"; - static String m51(referralCode, referralStorageInGB) => + static String m50(referralCode, referralStorageInGB) => "ente referral code: ${referralCode} \n\nApply it in Settings → General → Referrals to get ${referralStorageInGB} GB free after you signup for a paid plan\n\nhttps://ente.io"; - static String m52(numberOfPeople) => + static String m51(numberOfPeople) => "${Intl.plural(numberOfPeople, zero: 'Share with specific people', one: 'Shared with 1 person', other: 'Shared with ${numberOfPeople} people')}"; - static String m53(emailIDs) => "Shared with ${emailIDs}"; + static String m52(emailIDs) => "Shared with ${emailIDs}"; - static String m54(fileType) => + static String m53(fileType) => "This ${fileType} will be deleted from your device."; - static String m55(fileType) => + static String m54(fileType) => "This ${fileType} is in both ente and your device."; - static String m56(fileType) => "This ${fileType} will be deleted from ente."; + static String m55(fileType) => "This ${fileType} will be deleted from ente."; - static String m57(storageAmountInGB) => "${storageAmountInGB} GB"; + static String m56(storageAmountInGB) => "${storageAmountInGB} GB"; - static String m58( + static String m57( usedAmount, usedStorageUnit, totalAmount, totalStorageUnit) => "${usedAmount} ${usedStorageUnit} of ${totalAmount} ${totalStorageUnit} used"; - static String m59(id) => + static String m58(id) => "Your ${id} is already linked to another ente account.\nIf you would like to use your ${id} with this account, please contact our support\'\'"; - static String m60(endDate) => + static String m59(endDate) => "Your subscription will be cancelled on ${endDate}"; - static String m61(completed, total) => + static String m60(completed, total) => "${completed}/${total} memories preserved"; - static String m62(storageAmountInGB) => + static String m61(storageAmountInGB) => "They also get ${storageAmountInGB} GB"; - static String m63(email) => "This is ${email}\'s Verification ID"; + static String m62(email) => "This is ${email}\'s Verification ID"; - static String m64(count) => + static String m63(count) => "${Intl.plural(count, zero: '', one: '1 day', other: '${count} days')}"; - static String m65(endDate) => "Valid till ${endDate}"; + static String m64(endDate) => "Valid till ${endDate}"; - static String m66(email) => "Verify ${email}"; + static String m65(email) => "Verify ${email}"; - static String m67(email) => "We have sent a mail to ${email}"; + static String m66(email) => "We have sent a mail to ${email}"; - static String m68(count) => + static String m67(count) => "${Intl.plural(count, one: '${count} year ago', other: '${count} years ago')}"; - static String m69(storageSaved) => + static String m68(storageSaved) => "You have successfully freed up ${storageSaved}!"; final messages = _notInlinedMessages(_notInlinedMessages); @@ -750,6 +747,7 @@ class MessageLookup extends MessageLookupByLibrary { "We don\'t track app installs. It\'d help if you told us where you found us!"), "hearUsWhereTitle": MessageLookupByLibrary.simpleMessage( "How did you hear about Ente? (optional)"), + "help": MessageLookupByLibrary.simpleMessage("Help"), "hidden": MessageLookupByLibrary.simpleMessage("Hidden"), "hide": MessageLookupByLibrary.simpleMessage("Hide"), "hiding": MessageLookupByLibrary.simpleMessage("Hiding..."), @@ -982,8 +980,9 @@ class MessageLookup extends MessageLookupByLibrary { "paymentDetails": MessageLookupByLibrary.simpleMessage("Payment details"), "paymentFailed": MessageLookupByLibrary.simpleMessage("Payment failed"), + "paymentFailedMessage": MessageLookupByLibrary.simpleMessage( + "Unfortunately your payment failed. Please contact support and we\'ll help you out!"), "paymentFailedTalkToProvider": m37, - "paymentFailedWithReason": m38, "pendingItems": MessageLookupByLibrary.simpleMessage("Pending items"), "pendingSync": MessageLookupByLibrary.simpleMessage("Pending sync"), "peopleUsingYourCode": @@ -1007,7 +1006,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Pick center point"), "pinAlbum": MessageLookupByLibrary.simpleMessage("Pin album"), "playOnTv": MessageLookupByLibrary.simpleMessage("Play album on TV"), - "playStoreFreeTrialValidTill": m39, + "playStoreFreeTrialValidTill": m38, "playstoreSubscription": MessageLookupByLibrary.simpleMessage("PlayStore subscription"), "pleaseCheckYourInternetConnectionAndTryAgain": @@ -1019,12 +1018,12 @@ class MessageLookup extends MessageLookupByLibrary { "pleaseContactSupportIfTheProblemPersists": MessageLookupByLibrary.simpleMessage( "Please contact support if the problem persists"), - "pleaseEmailUsAt": m40, + "pleaseEmailUsAt": m39, "pleaseGrantPermissions": MessageLookupByLibrary.simpleMessage("Please grant permissions"), "pleaseLoginAgain": MessageLookupByLibrary.simpleMessage("Please login again"), - "pleaseSendTheLogsTo": m41, + "pleaseSendTheLogsTo": m40, "pleaseTryAgain": MessageLookupByLibrary.simpleMessage("Please try again"), "pleaseVerifyTheCodeYouHaveEntered": @@ -1059,7 +1058,7 @@ class MessageLookup extends MessageLookupByLibrary { "raiseTicket": MessageLookupByLibrary.simpleMessage("Raise ticket"), "rateTheApp": MessageLookupByLibrary.simpleMessage("Rate the app"), "rateUs": MessageLookupByLibrary.simpleMessage("Rate us"), - "rateUsOnStore": m42, + "rateUsOnStore": m41, "recover": MessageLookupByLibrary.simpleMessage("Recover"), "recoverAccount": MessageLookupByLibrary.simpleMessage("Recover account"), @@ -1090,7 +1089,7 @@ class MessageLookup extends MessageLookupByLibrary { "1. Give this code to your friends"), "referralStep2": MessageLookupByLibrary.simpleMessage( "2. They sign up for a paid plan"), - "referralStep3": m43, + "referralStep3": m42, "referrals": MessageLookupByLibrary.simpleMessage("Referrals"), "referralsAreCurrentlyPaused": MessageLookupByLibrary.simpleMessage( "Referrals are currently paused"), @@ -1114,7 +1113,7 @@ class MessageLookup extends MessageLookupByLibrary { "removeLink": MessageLookupByLibrary.simpleMessage("Remove link"), "removeParticipant": MessageLookupByLibrary.simpleMessage("Remove participant"), - "removeParticipantBody": m44, + "removeParticipantBody": m43, "removePublicLink": MessageLookupByLibrary.simpleMessage("Remove public link"), "removeShareItemsWarning": MessageLookupByLibrary.simpleMessage( @@ -1128,7 +1127,7 @@ class MessageLookup extends MessageLookupByLibrary { "renameFile": MessageLookupByLibrary.simpleMessage("Rename file"), "renewSubscription": MessageLookupByLibrary.simpleMessage("Renew subscription"), - "renewsOn": m45, + "renewsOn": m44, "reportABug": MessageLookupByLibrary.simpleMessage("Report a bug"), "reportBug": MessageLookupByLibrary.simpleMessage("Report bug"), "resendEmail": MessageLookupByLibrary.simpleMessage("Resend email"), @@ -1188,7 +1187,7 @@ class MessageLookup extends MessageLookupByLibrary { "Group photos that are taken within some radius of a photo"), "searchPeopleEmptySection": MessageLookupByLibrary.simpleMessage( "Invite people, and you\'ll see all photos shared by them here"), - "searchResultCount": m46, + "searchResultCount": m45, "security": MessageLookupByLibrary.simpleMessage("Security"), "selectALocation": MessageLookupByLibrary.simpleMessage("Select a location"), @@ -1215,8 +1214,8 @@ class MessageLookup extends MessageLookupByLibrary { "selectedItemsWillBeDeletedFromAllAlbumsAndMoved": MessageLookupByLibrary.simpleMessage( "Selected items will be deleted from all albums and moved to trash."), - "selectedPhotos": m47, - "selectedPhotosWithYours": m48, + "selectedPhotos": m46, + "selectedPhotosWithYours": m47, "send": MessageLookupByLibrary.simpleMessage("Send"), "sendEmail": MessageLookupByLibrary.simpleMessage("Send email"), "sendInvite": MessageLookupByLibrary.simpleMessage("Send invite"), @@ -1238,16 +1237,16 @@ class MessageLookup extends MessageLookupByLibrary { "shareAnAlbumNow": MessageLookupByLibrary.simpleMessage("Share an album now"), "shareLink": MessageLookupByLibrary.simpleMessage("Share link"), - "shareMyVerificationID": m49, + "shareMyVerificationID": m48, "shareOnlyWithThePeopleYouWant": MessageLookupByLibrary.simpleMessage( "Share only with the people you want"), - "shareTextConfirmOthersVerificationID": m50, + "shareTextConfirmOthersVerificationID": m49, "shareTextRecommendUsingEnte": MessageLookupByLibrary.simpleMessage( "Download ente so we can easily share original quality photos and videos\n\nhttps://ente.io"), - "shareTextReferralCode": m51, + "shareTextReferralCode": m50, "shareWithNonenteUsers": MessageLookupByLibrary.simpleMessage("Share with non-ente users"), - "shareWithPeopleSectionTitle": m52, + "shareWithPeopleSectionTitle": m51, "shareYourFirstAlbum": MessageLookupByLibrary.simpleMessage("Share your first album"), "sharedAlbumSectionDescription": MessageLookupByLibrary.simpleMessage( @@ -1258,7 +1257,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("New shared photos"), "sharedPhotoNotificationsExplanation": MessageLookupByLibrary.simpleMessage( "Receive notifications when someone adds a photo to a shared album that you\'re a part of"), - "sharedWith": m53, + "sharedWith": m52, "sharedWithMe": MessageLookupByLibrary.simpleMessage("Shared with me"), "sharedWithYou": MessageLookupByLibrary.simpleMessage("Shared with you"), @@ -1272,11 +1271,11 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Sign out other devices"), "signUpTerms": MessageLookupByLibrary.simpleMessage( "I agree to the terms of service and privacy policy"), - "singleFileDeleteFromDevice": m54, + "singleFileDeleteFromDevice": m53, "singleFileDeleteHighlight": MessageLookupByLibrary.simpleMessage( "It will be deleted from all albums."), - "singleFileInBothLocalAndRemote": m55, - "singleFileInRemoteOnly": m56, + "singleFileInBothLocalAndRemote": m54, + "singleFileInRemoteOnly": m55, "skip": MessageLookupByLibrary.simpleMessage("Skip"), "social": MessageLookupByLibrary.simpleMessage("Social"), "someItemsAreInBothEnteAndYourDevice": @@ -1314,13 +1313,13 @@ class MessageLookup extends MessageLookupByLibrary { "storage": MessageLookupByLibrary.simpleMessage("Storage"), "storageBreakupFamily": MessageLookupByLibrary.simpleMessage("Family"), "storageBreakupYou": MessageLookupByLibrary.simpleMessage("You"), - "storageInGB": m57, + "storageInGB": m56, "storageLimitExceeded": MessageLookupByLibrary.simpleMessage("Storage limit exceeded"), - "storageUsageInfo": m58, + "storageUsageInfo": m57, "strongStrength": MessageLookupByLibrary.simpleMessage("Strong"), - "subAlreadyLinkedErrMessage": m59, - "subWillBeCancelledOn": m60, + "subAlreadyLinkedErrMessage": m58, + "subWillBeCancelledOn": m59, "subscribe": MessageLookupByLibrary.simpleMessage("Subscribe"), "subscribeToEnableSharing": MessageLookupByLibrary.simpleMessage( "Looks like your subscription has expired. Please subscribe to enable sharing."), @@ -1337,7 +1336,7 @@ class MessageLookup extends MessageLookupByLibrary { "suggestFeatures": MessageLookupByLibrary.simpleMessage("Suggest features"), "support": MessageLookupByLibrary.simpleMessage("Support"), - "syncProgress": m61, + "syncProgress": m60, "syncStopped": MessageLookupByLibrary.simpleMessage("Sync stopped"), "syncing": MessageLookupByLibrary.simpleMessage("Syncing..."), "systemTheme": MessageLookupByLibrary.simpleMessage("System"), @@ -1363,7 +1362,7 @@ class MessageLookup extends MessageLookupByLibrary { "theseItemsWillBeDeletedFromYourDevice": MessageLookupByLibrary.simpleMessage( "These items will be deleted from your device."), - "theyAlsoGetXGb": m62, + "theyAlsoGetXGb": m61, "theyWillBeDeletedFromAllAlbums": MessageLookupByLibrary.simpleMessage( "They will be deleted from all albums."), "thisActionCannotBeUndone": MessageLookupByLibrary.simpleMessage( @@ -1379,7 +1378,7 @@ class MessageLookup extends MessageLookupByLibrary { "This email is already in use"), "thisImageHasNoExifData": MessageLookupByLibrary.simpleMessage("This image has no exif data"), - "thisIsPersonVerificationId": m63, + "thisIsPersonVerificationId": m62, "thisIsYourVerificationId": MessageLookupByLibrary.simpleMessage( "This is your Verification ID"), "thisWillLogYouOutOfTheFollowingDevice": @@ -1395,7 +1394,7 @@ class MessageLookup extends MessageLookupByLibrary { "total": MessageLookupByLibrary.simpleMessage("total"), "totalSize": MessageLookupByLibrary.simpleMessage("Total size"), "trash": MessageLookupByLibrary.simpleMessage("Trash"), - "trashDaysLeft": m64, + "trashDaysLeft": m63, "tryAgain": MessageLookupByLibrary.simpleMessage("Try again"), "turnOnBackupForAutoUpload": MessageLookupByLibrary.simpleMessage( "Turn on backup to automatically upload files added to this device folder to ente."), @@ -1447,7 +1446,7 @@ class MessageLookup extends MessageLookupByLibrary { "useSelectedPhoto": MessageLookupByLibrary.simpleMessage("Use selected photo"), "usedSpace": MessageLookupByLibrary.simpleMessage("Used space"), - "validTill": m65, + "validTill": m64, "verificationFailedPleaseTryAgain": MessageLookupByLibrary.simpleMessage( "Verification failed, please try again"), @@ -1455,7 +1454,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Verification ID"), "verify": MessageLookupByLibrary.simpleMessage("Verify"), "verifyEmail": MessageLookupByLibrary.simpleMessage("Verify email"), - "verifyEmailID": m66, + "verifyEmailID": m65, "verifyIDLabel": MessageLookupByLibrary.simpleMessage("Verify"), "verifyPasskey": MessageLookupByLibrary.simpleMessage("Verify passkey"), "verifyPassword": @@ -1486,11 +1485,11 @@ class MessageLookup extends MessageLookupByLibrary { "weDontSupportEditingPhotosAndAlbumsThatYouDont": MessageLookupByLibrary.simpleMessage( "We don\'t support editing photos and albums that you don\'t own yet"), - "weHaveSendEmailTo": m67, + "weHaveSendEmailTo": m66, "weakStrength": MessageLookupByLibrary.simpleMessage("Weak"), "welcomeBack": MessageLookupByLibrary.simpleMessage("Welcome back!"), "yearly": MessageLookupByLibrary.simpleMessage("Yearly"), - "yearsAgo": m68, + "yearsAgo": m67, "yes": MessageLookupByLibrary.simpleMessage("Yes"), "yesCancel": MessageLookupByLibrary.simpleMessage("Yes, cancel"), "yesConvertToViewer": @@ -1520,7 +1519,7 @@ class MessageLookup extends MessageLookupByLibrary { "You cannot share with yourself"), "youDontHaveAnyArchivedItems": MessageLookupByLibrary.simpleMessage( "You don\'t have any archived items."), - "youHaveSuccessfullyFreedUp": m69, + "youHaveSuccessfullyFreedUp": m68, "yourAccountHasBeenDeleted": MessageLookupByLibrary.simpleMessage( "Your account has been deleted"), "yourMap": MessageLookupByLibrary.simpleMessage("Your map"), diff --git a/mobile/lib/generated/intl/messages_es.dart b/mobile/lib/generated/intl/messages_es.dart index 0c2673d464..8ceb97ad31 100644 --- a/mobile/lib/generated/intl/messages_es.dart +++ b/mobile/lib/generated/intl/messages_es.dart @@ -121,79 +121,76 @@ class MessageLookup extends MessageLookupByLibrary { static String m37(providerName) => "Por favor hable con el soporte de ${providerName} si se le cobró"; - static String m38(reason) => - "Lamentablemente tu pago falló debido a ${reason}"; - - static String m40(toEmail) => + static String m39(toEmail) => "Por favor, envíanos un correo electrónico a ${toEmail}"; - static String m41(toEmail) => "Por favor, envíe los registros a ${toEmail}"; + static String m40(toEmail) => "Por favor, envíe los registros a ${toEmail}"; - static String m42(storeName) => "Califícanos en ${storeName}"; + static String m41(storeName) => "Califícanos en ${storeName}"; - static String m43(storageInGB) => + static String m42(storageInGB) => "3. Ambos obtienen ${storageInGB} GB* gratis"; - static String m44(userEmail) => + static String m43(userEmail) => "${userEmail} será eliminado de este álbum compartido\n\nCualquier foto añadida por ellos también será eliminada del álbum"; - static String m45(endDate) => "Se renueva el ${endDate}"; + static String m44(endDate) => "Se renueva el ${endDate}"; - static String m47(count) => "${count} seleccionados"; + static String m46(count) => "${count} seleccionados"; - static String m48(count, yourCount) => + static String m47(count, yourCount) => "${count} seleccionados (${yourCount} tuyos)"; - static String m49(verificationID) => + static String m48(verificationID) => "Aquí está mi ID de verificación: ${verificationID} para ente.io."; - static String m50(verificationID) => + static String m49(verificationID) => "Hola, ¿puedes confirmar que esta es tu ID de verificación ente.io: ${verificationID}?"; - static String m51(referralCode, referralStorageInGB) => + static String m50(referralCode, referralStorageInGB) => "ente código de referencia: ${referralCode} \n\nAplicarlo en Ajustes → General → Referencias para obtener ${referralStorageInGB} GB gratis después de registrarse en un plan de pago\n\nhttps://ente.io"; - static String m52(numberOfPeople) => + static String m51(numberOfPeople) => "${Intl.plural(numberOfPeople, zero: 'Compartir con personas específicas', one: 'Compartido con 1 persona', other: 'Compartido con ${numberOfPeople} personas')}"; - static String m53(emailIDs) => "Compartido con ${emailIDs}"; + static String m52(emailIDs) => "Compartido con ${emailIDs}"; - static String m54(fileType) => + static String m53(fileType) => "Este ${fileType} se eliminará de tu dispositivo."; - static String m55(fileType) => + static String m54(fileType) => "Este ${fileType} está tanto en ente como en tu dispositivo."; - static String m56(fileType) => "Este ${fileType} se eliminará de ente."; + static String m55(fileType) => "Este ${fileType} se eliminará de ente."; - static String m57(storageAmountInGB) => "${storageAmountInGB} GB"; + static String m56(storageAmountInGB) => "${storageAmountInGB} GB"; - static String m58( + static String m57( usedAmount, usedStorageUnit, totalAmount, totalStorageUnit) => "${usedAmount} ${usedStorageUnit} de ${totalAmount} ${totalStorageUnit} usados"; - static String m59(id) => + static String m58(id) => "Su ${id} ya está vinculado a otra cuenta ente.\nSi desea utilizar su ${id} con esta cuenta, póngase en contacto con nuestro servicio de asistencia\'\'"; - static String m60(endDate) => "Tu suscripción se cancelará el ${endDate}"; + static String m59(endDate) => "Tu suscripción se cancelará el ${endDate}"; - static String m61(completed, total) => + static String m60(completed, total) => "${completed}/${total} recuerdos conservados"; - static String m62(storageAmountInGB) => + static String m61(storageAmountInGB) => "También obtienen ${storageAmountInGB} GB"; - static String m63(email) => "Este es el ID de verificación de ${email}"; + static String m62(email) => "Este es el ID de verificación de ${email}"; - static String m66(email) => "Verificar ${email}"; + static String m65(email) => "Verificar ${email}"; - static String m67(email) => + static String m66(email) => "Hemos enviado un correo a ${email}"; - static String m68(count) => + static String m67(count) => "${Intl.plural(count, one: '${count} hace un año', other: '${count} hace años')}"; - static String m69(storageSaved) => "¡Has liberado ${storageSaved} con éxito!"; + static String m68(storageSaved) => "¡Has liberado ${storageSaved} con éxito!"; final messages = _notInlinedMessages(_notInlinedMessages); static Map _notInlinedMessages(_) => { @@ -875,7 +872,6 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Detalles de pago"), "paymentFailed": MessageLookupByLibrary.simpleMessage("Pago fallido"), "paymentFailedTalkToProvider": m37, - "paymentFailedWithReason": m38, "pendingSync": MessageLookupByLibrary.simpleMessage("Sincronización pendiente"), "peopleUsingYourCode": @@ -902,12 +898,12 @@ class MessageLookup extends MessageLookupByLibrary { "pleaseContactSupportIfTheProblemPersists": MessageLookupByLibrary.simpleMessage( "Por favor contacte a soporte técnico si el problema persiste"), - "pleaseEmailUsAt": m40, + "pleaseEmailUsAt": m39, "pleaseGrantPermissions": MessageLookupByLibrary.simpleMessage("Por favor, concede permiso"), "pleaseLoginAgain": MessageLookupByLibrary.simpleMessage( "Por favor, vuelva a iniciar sesión"), - "pleaseSendTheLogsTo": m41, + "pleaseSendTheLogsTo": m40, "pleaseTryAgain": MessageLookupByLibrary.simpleMessage( "Por favor, inténtalo nuevamente"), "pleaseVerifyTheCodeYouHaveEntered": @@ -941,7 +937,7 @@ class MessageLookup extends MessageLookupByLibrary { "rateTheApp": MessageLookupByLibrary.simpleMessage("Evalúa la aplicación"), "rateUs": MessageLookupByLibrary.simpleMessage("Califícanos"), - "rateUsOnStore": m42, + "rateUsOnStore": m41, "recover": MessageLookupByLibrary.simpleMessage("Recuperar"), "recoverAccount": MessageLookupByLibrary.simpleMessage("Recuperar cuenta"), @@ -973,7 +969,7 @@ class MessageLookup extends MessageLookupByLibrary { "1. Dale este código a tus amigos"), "referralStep2": MessageLookupByLibrary.simpleMessage( "2. Se inscriben a un plan pagado"), - "referralStep3": m43, + "referralStep3": m42, "referrals": MessageLookupByLibrary.simpleMessage("Referidos"), "referralsAreCurrentlyPaused": MessageLookupByLibrary.simpleMessage( "Las referencias están actualmente en pausa"), @@ -998,7 +994,7 @@ class MessageLookup extends MessageLookupByLibrary { "removeLink": MessageLookupByLibrary.simpleMessage("Eliminar enlace"), "removeParticipant": MessageLookupByLibrary.simpleMessage("Quitar participante"), - "removeParticipantBody": m44, + "removeParticipantBody": m43, "removePublicLink": MessageLookupByLibrary.simpleMessage("Quitar enlace público"), "removeShareItemsWarning": MessageLookupByLibrary.simpleMessage( @@ -1012,7 +1008,7 @@ class MessageLookup extends MessageLookupByLibrary { "renameFile": MessageLookupByLibrary.simpleMessage("Renombrar archivo"), "renewSubscription": MessageLookupByLibrary.simpleMessage("Renovar suscripción"), - "renewsOn": m45, + "renewsOn": m44, "reportABug": MessageLookupByLibrary.simpleMessage("Reportar un error"), "reportBug": MessageLookupByLibrary.simpleMessage("Reportar error"), "resendEmail": @@ -1074,8 +1070,8 @@ class MessageLookup extends MessageLookupByLibrary { "selectedItemsWillBeDeletedFromAllAlbumsAndMoved": MessageLookupByLibrary.simpleMessage( "Los archivos seleccionados serán eliminados de todos los álbumes y movidos a la papelera."), - "selectedPhotos": m47, - "selectedPhotosWithYours": m48, + "selectedPhotos": m46, + "selectedPhotosWithYours": m47, "send": MessageLookupByLibrary.simpleMessage("Enviar"), "sendEmail": MessageLookupByLibrary.simpleMessage("Enviar correo electrónico"), @@ -1100,32 +1096,32 @@ class MessageLookup extends MessageLookupByLibrary { "shareAnAlbumNow": MessageLookupByLibrary.simpleMessage("Compartir un álbum ahora"), "shareLink": MessageLookupByLibrary.simpleMessage("Compartir enlace"), - "shareMyVerificationID": m49, + "shareMyVerificationID": m48, "shareOnlyWithThePeopleYouWant": MessageLookupByLibrary.simpleMessage( "Comparte sólo con la gente que quieres"), - "shareTextConfirmOthersVerificationID": m50, + "shareTextConfirmOthersVerificationID": m49, "shareTextRecommendUsingEnte": MessageLookupByLibrary.simpleMessage( "Descarga ente para que podamos compartir fácilmente fotos y videos en su calidad original\n\nhttps://ente.io"), - "shareTextReferralCode": m51, + "shareTextReferralCode": m50, "shareWithNonenteUsers": MessageLookupByLibrary.simpleMessage( "Compartir con usuarios no ente"), - "shareWithPeopleSectionTitle": m52, + "shareWithPeopleSectionTitle": m51, "shareYourFirstAlbum": MessageLookupByLibrary.simpleMessage("Comparte tu primer álbum"), "sharedAlbumSectionDescription": MessageLookupByLibrary.simpleMessage( "Crear álbumes compartidos y colaborativos con otros usuarios ente, incluyendo usuarios en planes gratuitos."), "sharedByMe": MessageLookupByLibrary.simpleMessage("Compartido por mí"), - "sharedWith": m53, + "sharedWith": m52, "sharedWithMe": MessageLookupByLibrary.simpleMessage("Compartido conmigo"), "sharing": MessageLookupByLibrary.simpleMessage("Compartiendo..."), "signUpTerms": MessageLookupByLibrary.simpleMessage( "Estoy de acuerdo con los términos del servicio y la política de privacidad"), - "singleFileDeleteFromDevice": m54, + "singleFileDeleteFromDevice": m53, "singleFileDeleteHighlight": MessageLookupByLibrary.simpleMessage( "Se borrará de todos los álbumes."), - "singleFileInBothLocalAndRemote": m55, - "singleFileInRemoteOnly": m56, + "singleFileInBothLocalAndRemote": m54, + "singleFileInRemoteOnly": m55, "skip": MessageLookupByLibrary.simpleMessage("Omitir"), "social": MessageLookupByLibrary.simpleMessage("Social"), "someItemsAreInBothEnteAndYourDevice": @@ -1160,13 +1156,13 @@ class MessageLookup extends MessageLookupByLibrary { "storage": MessageLookupByLibrary.simpleMessage("Almacenamiento"), "storageBreakupFamily": MessageLookupByLibrary.simpleMessage("Familia"), "storageBreakupYou": MessageLookupByLibrary.simpleMessage("Usted"), - "storageInGB": m57, + "storageInGB": m56, "storageLimitExceeded": MessageLookupByLibrary.simpleMessage("Límite de datos excedido"), - "storageUsageInfo": m58, + "storageUsageInfo": m57, "strongStrength": MessageLookupByLibrary.simpleMessage("Segura"), - "subAlreadyLinkedErrMessage": m59, - "subWillBeCancelledOn": m60, + "subAlreadyLinkedErrMessage": m58, + "subWillBeCancelledOn": m59, "subscribe": MessageLookupByLibrary.simpleMessage("Suscribirse"), "subscribeToEnableSharing": MessageLookupByLibrary.simpleMessage( "Parece que su suscripción ha caducado. Por favor, suscríbase para habilitar el compartir."), @@ -1179,7 +1175,7 @@ class MessageLookup extends MessageLookupByLibrary { "suggestFeatures": MessageLookupByLibrary.simpleMessage("Sugerir una característica"), "support": MessageLookupByLibrary.simpleMessage("Soporte"), - "syncProgress": m61, + "syncProgress": m60, "syncStopped": MessageLookupByLibrary.simpleMessage("Sincronización detenida"), "syncing": MessageLookupByLibrary.simpleMessage("Sincronizando..."), @@ -1207,7 +1203,7 @@ class MessageLookup extends MessageLookupByLibrary { "theseItemsWillBeDeletedFromYourDevice": MessageLookupByLibrary.simpleMessage( "Estos elementos se eliminarán de tu dispositivo."), - "theyAlsoGetXGb": m62, + "theyAlsoGetXGb": m61, "theyWillBeDeletedFromAllAlbums": MessageLookupByLibrary.simpleMessage( "Se borrarán de todos los álbumes."), "thisActionCannotBeUndone": MessageLookupByLibrary.simpleMessage( @@ -1223,7 +1219,7 @@ class MessageLookup extends MessageLookupByLibrary { "Este correo electrónico ya está en uso"), "thisImageHasNoExifData": MessageLookupByLibrary.simpleMessage( "Esta imagen no tiene datos exif"), - "thisIsPersonVerificationId": m63, + "thisIsPersonVerificationId": m62, "thisIsYourVerificationId": MessageLookupByLibrary.simpleMessage( "Esta es tu ID de verificación"), "thisWillLogYouOutOfTheFollowingDevice": @@ -1293,7 +1289,7 @@ class MessageLookup extends MessageLookupByLibrary { "verify": MessageLookupByLibrary.simpleMessage("Verificar"), "verifyEmail": MessageLookupByLibrary.simpleMessage( "Verificar correo electrónico"), - "verifyEmailID": m66, + "verifyEmailID": m65, "verifyIDLabel": MessageLookupByLibrary.simpleMessage("Verificar"), "verifyPassword": MessageLookupByLibrary.simpleMessage("Verificar contraseña"), @@ -1316,12 +1312,12 @@ class MessageLookup extends MessageLookupByLibrary { "weDontSupportEditingPhotosAndAlbumsThatYouDont": MessageLookupByLibrary.simpleMessage( "No admitimos la edición de fotos y álbunes que aún no son tuyos"), - "weHaveSendEmailTo": m67, + "weHaveSendEmailTo": m66, "weakStrength": MessageLookupByLibrary.simpleMessage("Poco segura"), "welcomeBack": MessageLookupByLibrary.simpleMessage("¡Bienvenido de nuevo!"), "yearly": MessageLookupByLibrary.simpleMessage("Anualmente"), - "yearsAgo": m68, + "yearsAgo": m67, "yes": MessageLookupByLibrary.simpleMessage("Sí"), "yesCancel": MessageLookupByLibrary.simpleMessage("Sí, cancelar"), "yesConvertToViewer": @@ -1351,7 +1347,7 @@ class MessageLookup extends MessageLookupByLibrary { "No puedes compartir contigo mismo"), "youDontHaveAnyArchivedItems": MessageLookupByLibrary.simpleMessage( "No tienes nada de elementos archivados."), - "youHaveSuccessfullyFreedUp": m69, + "youHaveSuccessfullyFreedUp": m68, "yourAccountHasBeenDeleted": MessageLookupByLibrary.simpleMessage("Su cuenta ha sido eliminada"), "yourMap": MessageLookupByLibrary.simpleMessage("Your map"), diff --git a/mobile/lib/generated/intl/messages_fr.dart b/mobile/lib/generated/intl/messages_fr.dart index 6bef1659c6..035d6c2322 100644 --- a/mobile/lib/generated/intl/messages_fr.dart +++ b/mobile/lib/generated/intl/messages_fr.dart @@ -126,89 +126,86 @@ class MessageLookup extends MessageLookupByLibrary { static String m37(providerName) => "Veuillez contacter le support ${providerName} si vous avez été facturé"; - static String m38(reason) => - "Malheureusement, votre paiement a échoué pour ${reason}"; - - static String m39(endDate) => + static String m38(endDate) => "Essai gratuit valable jusqu\'à ${endDate}.\nVous pouvez choisir un plan payant par la suite."; - static String m40(toEmail) => "Merci de nous envoyer un e-mail à ${toEmail}"; + static String m39(toEmail) => "Merci de nous envoyer un e-mail à ${toEmail}"; - static String m41(toEmail) => "Envoyez les logs à ${toEmail}"; + static String m40(toEmail) => "Envoyez les logs à ${toEmail}"; - static String m42(storeName) => "Notez-nous sur ${storeName}"; + static String m41(storeName) => "Notez-nous sur ${storeName}"; - static String m43(storageInGB) => + static String m42(storageInGB) => "3. Vous recevez tous les deux ${storageInGB} GB* gratuits"; - static String m44(userEmail) => + static String m43(userEmail) => "${userEmail} sera retiré de cet album partagé\n\nToutes les photos ajoutées par eux seront également retirées de l\'album"; - static String m45(endDate) => "Renouvellement le ${endDate}"; + static String m44(endDate) => "Renouvellement le ${endDate}"; - static String m46(count) => + static String m45(count) => "${Intl.plural(count, one: '${count} résultat trouvé', other: '${count} résultats trouvés')}"; - static String m47(count) => "${count} sélectionné(s)"; + static String m46(count) => "${count} sélectionné(s)"; - static String m48(count, yourCount) => + static String m47(count, yourCount) => "${count} sélectionné(s) (${yourCount} à vous)"; - static String m49(verificationID) => + static String m48(verificationID) => "Voici mon ID de vérification : ${verificationID} pour ente.io."; - static String m50(verificationID) => + static String m49(verificationID) => "Hé, pouvez-vous confirmer qu\'il s\'agit de votre ID de vérification ente.io : ${verificationID}"; - static String m51(referralCode, referralStorageInGB) => + static String m50(referralCode, referralStorageInGB) => "code de parrainage ente : ${referralCode} \n\nAppliquez le dans Paramètres → Général → Références pour obtenir ${referralStorageInGB} Go gratuitement après votre inscription à un plan payant\n\nhttps://ente.io"; - static String m52(numberOfPeople) => + static String m51(numberOfPeople) => "${Intl.plural(numberOfPeople, zero: 'Partagez avec des personnes spécifiques', one: 'Partagé avec 1 personne', other: 'Partagé avec ${numberOfPeople} des gens')}"; - static String m53(emailIDs) => "Partagé avec ${emailIDs}"; + static String m52(emailIDs) => "Partagé avec ${emailIDs}"; - static String m54(fileType) => + static String m53(fileType) => "Elle ${fileType} sera supprimée de votre appareil."; - static String m55(fileType) => + static String m54(fileType) => "Cette ${fileType} est à la fois sur ente et sur votre appareil."; - static String m56(fileType) => "Ce ${fileType} sera supprimé de ente."; + static String m55(fileType) => "Ce ${fileType} sera supprimé de ente."; - static String m57(storageAmountInGB) => "${storageAmountInGB} Go"; + static String m56(storageAmountInGB) => "${storageAmountInGB} Go"; - static String m58( + static String m57( usedAmount, usedStorageUnit, totalAmount, totalStorageUnit) => "${usedAmount} ${usedStorageUnit} sur ${totalAmount} ${totalStorageUnit} utilisé"; - static String m59(id) => + static String m58(id) => "Votre ${id} est déjà lié à un autre compte ente.\nSi vous souhaitez utiliser votre ${id} avec ce compte, veuillez contacter notre support"; - static String m60(endDate) => "Votre abonnement sera annulé le ${endDate}"; + static String m59(endDate) => "Votre abonnement sera annulé le ${endDate}"; - static String m61(completed, total) => + static String m60(completed, total) => "${completed}/${total} souvenirs préservés"; - static String m62(storageAmountInGB) => + static String m61(storageAmountInGB) => "Ils obtiennent aussi ${storageAmountInGB} Go"; - static String m63(email) => "Ceci est l\'ID de vérification de ${email}"; + static String m62(email) => "Ceci est l\'ID de vérification de ${email}"; - static String m64(count) => + static String m63(count) => "${Intl.plural(count, zero: '0 jour', one: '1 jour', other: '${count} jours')}"; - static String m65(endDate) => "Valable jusqu\'au ${endDate}"; + static String m64(endDate) => "Valable jusqu\'au ${endDate}"; - static String m66(email) => "Vérifier ${email}"; + static String m65(email) => "Vérifier ${email}"; - static String m67(email) => + static String m66(email) => "Nous avons envoyé un e-mail à ${email}"; - static String m68(count) => + static String m67(count) => "${Intl.plural(count, one: 'il y a ${count} an', other: 'il y a ${count} ans')}"; - static String m69(storageSaved) => + static String m68(storageSaved) => "Vous avez libéré ${storageSaved} avec succès !"; final messages = _notInlinedMessages(_notInlinedMessages); @@ -1000,7 +997,6 @@ class MessageLookup extends MessageLookupByLibrary { "paymentFailed": MessageLookupByLibrary.simpleMessage("Échec du paiement"), "paymentFailedTalkToProvider": m37, - "paymentFailedWithReason": m38, "pendingSync": MessageLookupByLibrary.simpleMessage("Synchronisation en attente"), "peopleUsingYourCode": MessageLookupByLibrary.simpleMessage( @@ -1023,7 +1019,7 @@ class MessageLookup extends MessageLookupByLibrary { "pickCenterPoint": MessageLookupByLibrary.simpleMessage( "Sélectionner le point central"), "pinAlbum": MessageLookupByLibrary.simpleMessage("Épingler l\'album"), - "playStoreFreeTrialValidTill": m39, + "playStoreFreeTrialValidTill": m38, "playstoreSubscription": MessageLookupByLibrary.simpleMessage("Abonnement au PlayStore"), "pleaseContactSupportAndWeWillBeHappyToHelp": @@ -1032,12 +1028,12 @@ class MessageLookup extends MessageLookupByLibrary { "pleaseContactSupportIfTheProblemPersists": MessageLookupByLibrary.simpleMessage( "Merci de contacter l\'assistance si cette erreur persiste"), - "pleaseEmailUsAt": m40, + "pleaseEmailUsAt": m39, "pleaseGrantPermissions": MessageLookupByLibrary.simpleMessage( "Veuillez accorder la permission"), "pleaseLoginAgain": MessageLookupByLibrary.simpleMessage("Veuillez vous reconnecter"), - "pleaseSendTheLogsTo": m41, + "pleaseSendTheLogsTo": m40, "pleaseTryAgain": MessageLookupByLibrary.simpleMessage("Veuillez réessayer"), "pleaseVerifyTheCodeYouHaveEntered": @@ -1073,7 +1069,7 @@ class MessageLookup extends MessageLookupByLibrary { "rateTheApp": MessageLookupByLibrary.simpleMessage("Évaluer l\'application"), "rateUs": MessageLookupByLibrary.simpleMessage("Évaluez-nous"), - "rateUsOnStore": m42, + "rateUsOnStore": m41, "recover": MessageLookupByLibrary.simpleMessage("Récupérer"), "recoverAccount": MessageLookupByLibrary.simpleMessage("Récupérer un compte"), @@ -1104,7 +1100,7 @@ class MessageLookup extends MessageLookupByLibrary { "1. Donnez ce code à vos amis"), "referralStep2": MessageLookupByLibrary.simpleMessage( "2. Ils s\'inscrivent à une offre payante"), - "referralStep3": m43, + "referralStep3": m42, "referrals": MessageLookupByLibrary.simpleMessage("Parrainages"), "referralsAreCurrentlyPaused": MessageLookupByLibrary.simpleMessage( "Les recommandations sont actuellement en pause"), @@ -1130,7 +1126,7 @@ class MessageLookup extends MessageLookupByLibrary { "removeLink": MessageLookupByLibrary.simpleMessage("Supprimer le lien"), "removeParticipant": MessageLookupByLibrary.simpleMessage("Supprimer le participant"), - "removeParticipantBody": m44, + "removeParticipantBody": m43, "removePublicLink": MessageLookupByLibrary.simpleMessage("Supprimer le lien public"), "removeShareItemsWarning": MessageLookupByLibrary.simpleMessage( @@ -1146,7 +1142,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Renommer le fichier"), "renewSubscription": MessageLookupByLibrary.simpleMessage("Renouveler l’abonnement"), - "renewsOn": m45, + "renewsOn": m44, "reportABug": MessageLookupByLibrary.simpleMessage("Signaler un bug"), "reportBug": MessageLookupByLibrary.simpleMessage("Signaler un bug"), "resendEmail": @@ -1211,7 +1207,7 @@ class MessageLookup extends MessageLookupByLibrary { "Grouper les photos qui sont prises dans un certain angle d\'une photo"), "searchPeopleEmptySection": MessageLookupByLibrary.simpleMessage( "Invitez des gens, et vous verrez ici toutes les photos qu\'ils partagent"), - "searchResultCount": m46, + "searchResultCount": m45, "security": MessageLookupByLibrary.simpleMessage("Sécurité"), "selectALocation": MessageLookupByLibrary.simpleMessage("Select a location"), @@ -1240,8 +1236,8 @@ class MessageLookup extends MessageLookupByLibrary { "selectedItemsWillBeDeletedFromAllAlbumsAndMoved": MessageLookupByLibrary.simpleMessage( "Les éléments sélectionnés seront supprimés de tous les albums et déplacés dans la corbeille."), - "selectedPhotos": m47, - "selectedPhotosWithYours": m48, + "selectedPhotos": m46, + "selectedPhotosWithYours": m47, "send": MessageLookupByLibrary.simpleMessage("Envoyer"), "sendEmail": MessageLookupByLibrary.simpleMessage("Envoyer un e-mail"), "sendInvite": @@ -1267,16 +1263,16 @@ class MessageLookup extends MessageLookupByLibrary { "shareAnAlbumNow": MessageLookupByLibrary.simpleMessage( "Partagez un album maintenant"), "shareLink": MessageLookupByLibrary.simpleMessage("Partager le lien"), - "shareMyVerificationID": m49, + "shareMyVerificationID": m48, "shareOnlyWithThePeopleYouWant": MessageLookupByLibrary.simpleMessage( "Partager uniquement avec les personnes que vous voulez"), - "shareTextConfirmOthersVerificationID": m50, + "shareTextConfirmOthersVerificationID": m49, "shareTextRecommendUsingEnte": MessageLookupByLibrary.simpleMessage( "Téléchargez ente pour que nous puissions facilement partager des photos et des vidéos de qualité originale\n\nhttps://ente.io"), - "shareTextReferralCode": m51, + "shareTextReferralCode": m50, "shareWithNonenteUsers": MessageLookupByLibrary.simpleMessage( "Partager avec des utilisateurs non-ente"), - "shareWithPeopleSectionTitle": m52, + "shareWithPeopleSectionTitle": m51, "shareYourFirstAlbum": MessageLookupByLibrary.simpleMessage( "Partagez votre premier album"), "sharedAlbumSectionDescription": MessageLookupByLibrary.simpleMessage( @@ -1287,7 +1283,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Nouvelles photos partagées"), "sharedPhotoNotificationsExplanation": MessageLookupByLibrary.simpleMessage( "Recevoir des notifications quand quelqu\'un ajoute une photo à un album partagé dont vous faites partie"), - "sharedWith": m53, + "sharedWith": m52, "sharedWithMe": MessageLookupByLibrary.simpleMessage("Partagés avec moi"), "sharedWithYou": @@ -1297,11 +1293,11 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Montrer les souvenirs"), "signUpTerms": MessageLookupByLibrary.simpleMessage( "J\'accepte les conditions d\'utilisation et la politique de confidentialité"), - "singleFileDeleteFromDevice": m54, + "singleFileDeleteFromDevice": m53, "singleFileDeleteHighlight": MessageLookupByLibrary.simpleMessage( "Elle sera supprimée de tous les albums."), - "singleFileInBothLocalAndRemote": m55, - "singleFileInRemoteOnly": m56, + "singleFileInBothLocalAndRemote": m54, + "singleFileInRemoteOnly": m55, "skip": MessageLookupByLibrary.simpleMessage("Ignorer"), "social": MessageLookupByLibrary.simpleMessage("Réseaux Sociaux"), "someItemsAreInBothEnteAndYourDevice": @@ -1341,14 +1337,14 @@ class MessageLookup extends MessageLookupByLibrary { "storage": MessageLookupByLibrary.simpleMessage("Stockage"), "storageBreakupFamily": MessageLookupByLibrary.simpleMessage("Famille"), "storageBreakupYou": MessageLookupByLibrary.simpleMessage("Vous"), - "storageInGB": m57, + "storageInGB": m56, "storageLimitExceeded": MessageLookupByLibrary.simpleMessage("Limite de stockage atteinte"), - "storageUsageInfo": m58, + "storageUsageInfo": m57, "strongStrength": MessageLookupByLibrary.simpleMessage("Securité forte"), - "subAlreadyLinkedErrMessage": m59, - "subWillBeCancelledOn": m60, + "subAlreadyLinkedErrMessage": m58, + "subWillBeCancelledOn": m59, "subscribe": MessageLookupByLibrary.simpleMessage("S\'abonner"), "subscribeToEnableSharing": MessageLookupByLibrary.simpleMessage( "Il semble que votre abonnement ait expiré. Veuillez vous abonner pour activer le partage."), @@ -1365,7 +1361,7 @@ class MessageLookup extends MessageLookupByLibrary { "suggestFeatures": MessageLookupByLibrary.simpleMessage( "Suggérer des fonctionnalités"), "support": MessageLookupByLibrary.simpleMessage("Support"), - "syncProgress": m61, + "syncProgress": m60, "syncStopped": MessageLookupByLibrary.simpleMessage("Synchronisation arrêtée ?"), "syncing": MessageLookupByLibrary.simpleMessage( @@ -1394,7 +1390,7 @@ class MessageLookup extends MessageLookupByLibrary { "theseItemsWillBeDeletedFromYourDevice": MessageLookupByLibrary.simpleMessage( "Ces éléments seront supprimés de votre appareil."), - "theyAlsoGetXGb": m62, + "theyAlsoGetXGb": m61, "theyWillBeDeletedFromAllAlbums": MessageLookupByLibrary.simpleMessage( "Ils seront supprimés de tous les albums."), "thisActionCannotBeUndone": MessageLookupByLibrary.simpleMessage( @@ -1410,7 +1406,7 @@ class MessageLookup extends MessageLookupByLibrary { "Cette adresse mail est déjà utilisé"), "thisImageHasNoExifData": MessageLookupByLibrary.simpleMessage( "Cette image n\'a pas de données exif"), - "thisIsPersonVerificationId": m63, + "thisIsPersonVerificationId": m62, "thisIsYourVerificationId": MessageLookupByLibrary.simpleMessage( "Ceci est votre ID de vérification"), "thisWillLogYouOutOfTheFollowingDevice": @@ -1426,7 +1422,7 @@ class MessageLookup extends MessageLookupByLibrary { "total": MessageLookupByLibrary.simpleMessage("total"), "totalSize": MessageLookupByLibrary.simpleMessage("Taille totale"), "trash": MessageLookupByLibrary.simpleMessage("Corbeille"), - "trashDaysLeft": m64, + "trashDaysLeft": m63, "tryAgain": MessageLookupByLibrary.simpleMessage("Réessayer"), "turnOnBackupForAutoUpload": MessageLookupByLibrary.simpleMessage( "Activez la sauvegarde pour télécharger automatiquement les fichiers ajoutés à ce dossier de l\'appareil sur ente."), @@ -1484,7 +1480,7 @@ class MessageLookup extends MessageLookupByLibrary { "useSelectedPhoto": MessageLookupByLibrary.simpleMessage( "Utiliser la photo sélectionnée"), "usedSpace": MessageLookupByLibrary.simpleMessage("Mémoire utilisée"), - "validTill": m65, + "validTill": m64, "verificationFailedPleaseTryAgain": MessageLookupByLibrary.simpleMessage( "La vérification a échouée, veuillez réessayer"), @@ -1493,7 +1489,7 @@ class MessageLookup extends MessageLookupByLibrary { "verify": MessageLookupByLibrary.simpleMessage("Vérifier"), "verifyEmail": MessageLookupByLibrary.simpleMessage("Vérifier l\'email"), - "verifyEmailID": m66, + "verifyEmailID": m65, "verifyIDLabel": MessageLookupByLibrary.simpleMessage("Vérifier"), "verifyPassword": MessageLookupByLibrary.simpleMessage("Vérifier le mot de passe"), @@ -1522,11 +1518,11 @@ class MessageLookup extends MessageLookupByLibrary { "weDontSupportEditingPhotosAndAlbumsThatYouDont": MessageLookupByLibrary.simpleMessage( "Nous ne prenons pas en charge l\'édition des photos et des albums que vous ne possédez pas encore"), - "weHaveSendEmailTo": m67, + "weHaveSendEmailTo": m66, "weakStrength": MessageLookupByLibrary.simpleMessage("Securité Faible"), "welcomeBack": MessageLookupByLibrary.simpleMessage("Bienvenue !"), "yearly": MessageLookupByLibrary.simpleMessage("Annuel"), - "yearsAgo": m68, + "yearsAgo": m67, "yes": MessageLookupByLibrary.simpleMessage("Oui"), "yesCancel": MessageLookupByLibrary.simpleMessage("Oui, annuler"), "yesConvertToViewer": MessageLookupByLibrary.simpleMessage( @@ -1557,7 +1553,7 @@ class MessageLookup extends MessageLookupByLibrary { "Vous ne pouvez pas partager avec vous-même"), "youDontHaveAnyArchivedItems": MessageLookupByLibrary.simpleMessage( "Vous n\'avez aucun élément archivé."), - "youHaveSuccessfullyFreedUp": m69, + "youHaveSuccessfullyFreedUp": m68, "yourAccountHasBeenDeleted": MessageLookupByLibrary.simpleMessage("Votre compte a été supprimé"), "yourMap": MessageLookupByLibrary.simpleMessage("Votre carte"), diff --git a/mobile/lib/generated/intl/messages_it.dart b/mobile/lib/generated/intl/messages_it.dart index 8b9b28b35b..c3cbb0b740 100644 --- a/mobile/lib/generated/intl/messages_it.dart +++ b/mobile/lib/generated/intl/messages_it.dart @@ -124,86 +124,83 @@ class MessageLookup extends MessageLookupByLibrary { static String m37(providerName) => "Si prega di parlare con il supporto di ${providerName} se ti è stato addebitato qualcosa"; - static String m38(reason) => - "Purtroppo il tuo pagamento non è riuscito a causa di ${reason}"; - - static String m39(endDate) => + static String m38(endDate) => "Prova gratuita valida fino al ${endDate}.\nPuoi scegliere un piano a pagamento in seguito."; - static String m40(toEmail) => "Per favore invia un\'email a ${toEmail}"; + static String m39(toEmail) => "Per favore invia un\'email a ${toEmail}"; - static String m41(toEmail) => "Invia i log a \n${toEmail}"; + static String m40(toEmail) => "Invia i log a \n${toEmail}"; - static String m42(storeName) => "Valutaci su ${storeName}"; + static String m41(storeName) => "Valutaci su ${storeName}"; - static String m43(storageInGB) => + static String m42(storageInGB) => "3. Ottenete entrambi ${storageInGB} GB* gratis"; - static String m44(userEmail) => + static String m43(userEmail) => "${userEmail} verrà rimosso da questo album condiviso\n\nQualsiasi foto aggiunta dall\'utente verrà rimossa dall\'album"; - static String m45(endDate) => "Si rinnova il ${endDate}"; + static String m44(endDate) => "Si rinnova il ${endDate}"; - static String m47(count) => "${count} selezionati"; + static String m46(count) => "${count} selezionati"; - static String m48(count, yourCount) => + static String m47(count, yourCount) => "${count} selezionato (${yourCount} tuoi)"; - static String m49(verificationID) => + static String m48(verificationID) => "Ecco il mio ID di verifica: ${verificationID} per ente.io."; - static String m50(verificationID) => + static String m49(verificationID) => "Hey, puoi confermare che questo è il tuo ID di verifica: ${verificationID} su ente.io"; - static String m51(referralCode, referralStorageInGB) => + static String m50(referralCode, referralStorageInGB) => "ente referral code: ${referralCode} \n\nApplicalo in Impostazioni → Generale → Referral per ottenere ${referralStorageInGB} GB gratis dopo la registrazione di un piano a pagamento\n\nhttps://ente.io"; - static String m52(numberOfPeople) => + static String m51(numberOfPeople) => "${Intl.plural(numberOfPeople, zero: 'Condividi con persone specifiche', one: 'Condividi con una persona', other: 'Condividi con ${numberOfPeople} persone')}"; - static String m53(emailIDs) => "Condiviso con ${emailIDs}"; + static String m52(emailIDs) => "Condiviso con ${emailIDs}"; - static String m54(fileType) => + static String m53(fileType) => "Questo ${fileType} verrà eliminato dal tuo dispositivo."; - static String m55(fileType) => + static String m54(fileType) => "Questo ${fileType} è sia su ente che sul tuo dispositivo."; - static String m56(fileType) => "Questo ${fileType} verrà eliminato su ente."; + static String m55(fileType) => "Questo ${fileType} verrà eliminato su ente."; - static String m57(storageAmountInGB) => "${storageAmountInGB} GB"; + static String m56(storageAmountInGB) => "${storageAmountInGB} GB"; - static String m58( + static String m57( usedAmount, usedStorageUnit, totalAmount, totalStorageUnit) => "${usedAmount} ${usedStorageUnit} di ${totalAmount} ${totalStorageUnit} utilizzati"; - static String m59(id) => + static String m58(id) => "Il tuo ${id} è già collegato ad un altro account ente.\nSe desideri utilizzare il tuo ${id} con questo account, contatta il nostro supporto\'\'"; - static String m60(endDate) => "L\'abbonamento verrà cancellato il ${endDate}"; + static String m59(endDate) => "L\'abbonamento verrà cancellato il ${endDate}"; - static String m61(completed, total) => + static String m60(completed, total) => "${completed}/${total} ricordi conservati"; - static String m62(storageAmountInGB) => + static String m61(storageAmountInGB) => "Anche loro riceveranno ${storageAmountInGB} GB"; - static String m63(email) => "Questo è l\'ID di verifica di ${email}"; + static String m62(email) => "Questo è l\'ID di verifica di ${email}"; - static String m64(count) => + static String m63(count) => "${Intl.plural(count, zero: '', one: '1 giorno', other: '${count} giorni')}"; - static String m65(endDate) => "Valido fino al ${endDate}"; + static String m64(endDate) => "Valido fino al ${endDate}"; - static String m66(email) => "Verifica ${email}"; + static String m65(email) => "Verifica ${email}"; - static String m67(email) => + static String m66(email) => "Abbiamo inviato una mail a ${email}"; - static String m68(count) => + static String m67(count) => "${Intl.plural(count, one: '${count} anno fa', other: '${count} anni fa')}"; - static String m69(storageSaved) => + static String m68(storageSaved) => "Hai liberato con successo ${storageSaved}!"; final messages = _notInlinedMessages(_notInlinedMessages); @@ -967,7 +964,6 @@ class MessageLookup extends MessageLookupByLibrary { "paymentFailed": MessageLookupByLibrary.simpleMessage("Pagamento non riuscito"), "paymentFailedTalkToProvider": m37, - "paymentFailedWithReason": m38, "pendingSync": MessageLookupByLibrary.simpleMessage("Sincronizzazione in sospeso"), "peopleUsingYourCode": MessageLookupByLibrary.simpleMessage( @@ -987,7 +983,7 @@ class MessageLookup extends MessageLookupByLibrary { "pickCenterPoint": MessageLookupByLibrary.simpleMessage( "Selezionare il punto centrale"), "pinAlbum": MessageLookupByLibrary.simpleMessage("Fissa l\'album"), - "playStoreFreeTrialValidTill": m39, + "playStoreFreeTrialValidTill": m38, "playstoreSubscription": MessageLookupByLibrary.simpleMessage("Abbonamento su PlayStore"), "pleaseContactSupportAndWeWillBeHappyToHelp": @@ -996,12 +992,12 @@ class MessageLookup extends MessageLookupByLibrary { "pleaseContactSupportIfTheProblemPersists": MessageLookupByLibrary.simpleMessage( "Riprova. Se il problema persiste, ti invitiamo a contattare l\'assistenza"), - "pleaseEmailUsAt": m40, + "pleaseEmailUsAt": m39, "pleaseGrantPermissions": MessageLookupByLibrary.simpleMessage("Concedi i permessi"), "pleaseLoginAgain": MessageLookupByLibrary.simpleMessage( "Effettua nuovamente l\'accesso"), - "pleaseSendTheLogsTo": m41, + "pleaseSendTheLogsTo": m40, "pleaseTryAgain": MessageLookupByLibrary.simpleMessage("Riprova"), "pleaseVerifyTheCodeYouHaveEntered": MessageLookupByLibrary.simpleMessage( @@ -1035,7 +1031,7 @@ class MessageLookup extends MessageLookupByLibrary { "raiseTicket": MessageLookupByLibrary.simpleMessage("Invia ticket"), "rateTheApp": MessageLookupByLibrary.simpleMessage("Valuta l\'app"), "rateUs": MessageLookupByLibrary.simpleMessage("Lascia una recensione"), - "rateUsOnStore": m42, + "rateUsOnStore": m41, "recover": MessageLookupByLibrary.simpleMessage("Recupera"), "recoverAccount": MessageLookupByLibrary.simpleMessage("Recupera account"), @@ -1067,7 +1063,7 @@ class MessageLookup extends MessageLookupByLibrary { "1. Condividi questo codice con i tuoi amici"), "referralStep2": MessageLookupByLibrary.simpleMessage( "2. Si iscrivono per un piano a pagamento"), - "referralStep3": m43, + "referralStep3": m42, "referrals": MessageLookupByLibrary.simpleMessage("Invita un Amico"), "referralsAreCurrentlyPaused": MessageLookupByLibrary.simpleMessage( "I referral code sono attualmente in pausa"), @@ -1091,7 +1087,7 @@ class MessageLookup extends MessageLookupByLibrary { "removeLink": MessageLookupByLibrary.simpleMessage("Elimina link"), "removeParticipant": MessageLookupByLibrary.simpleMessage("Rimuovi partecipante"), - "removeParticipantBody": m44, + "removeParticipantBody": m43, "removePublicLink": MessageLookupByLibrary.simpleMessage("Rimuovi link pubblico"), "removeShareItemsWarning": MessageLookupByLibrary.simpleMessage( @@ -1105,7 +1101,7 @@ class MessageLookup extends MessageLookupByLibrary { "renameFile": MessageLookupByLibrary.simpleMessage("Rinomina file"), "renewSubscription": MessageLookupByLibrary.simpleMessage("Rinnova abbonamento"), - "renewsOn": m45, + "renewsOn": m44, "reportABug": MessageLookupByLibrary.simpleMessage("Segnala un bug"), "reportBug": MessageLookupByLibrary.simpleMessage("Segnala un bug"), "resendEmail": MessageLookupByLibrary.simpleMessage("Rinvia email"), @@ -1170,8 +1166,8 @@ class MessageLookup extends MessageLookupByLibrary { "selectedItemsWillBeDeletedFromAllAlbumsAndMoved": MessageLookupByLibrary.simpleMessage( "Gli elementi selezionati verranno eliminati da tutti gli album e spostati nel cestino."), - "selectedPhotos": m47, - "selectedPhotosWithYours": m48, + "selectedPhotos": m46, + "selectedPhotosWithYours": m47, "send": MessageLookupByLibrary.simpleMessage("Invia"), "sendEmail": MessageLookupByLibrary.simpleMessage("Invia email"), "sendInvite": MessageLookupByLibrary.simpleMessage("Invita"), @@ -1195,16 +1191,16 @@ class MessageLookup extends MessageLookupByLibrary { "shareAnAlbumNow": MessageLookupByLibrary.simpleMessage("Condividi un album"), "shareLink": MessageLookupByLibrary.simpleMessage("Condividi link"), - "shareMyVerificationID": m49, + "shareMyVerificationID": m48, "shareOnlyWithThePeopleYouWant": MessageLookupByLibrary.simpleMessage( "Condividi solo con le persone che vuoi"), - "shareTextConfirmOthersVerificationID": m50, + "shareTextConfirmOthersVerificationID": m49, "shareTextRecommendUsingEnte": MessageLookupByLibrary.simpleMessage( "Scarica ente in modo da poter facilmente condividere foto e video senza perdita di qualità\n\nhttps://ente.io"), - "shareTextReferralCode": m51, + "shareTextReferralCode": m50, "shareWithNonenteUsers": MessageLookupByLibrary.simpleMessage( "Condividi con utenti che non hanno un account ente"), - "shareWithPeopleSectionTitle": m52, + "shareWithPeopleSectionTitle": m51, "shareYourFirstAlbum": MessageLookupByLibrary.simpleMessage( "Condividi il tuo primo album"), "sharedAlbumSectionDescription": MessageLookupByLibrary.simpleMessage( @@ -1215,7 +1211,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Nuove foto condivise"), "sharedPhotoNotificationsExplanation": MessageLookupByLibrary.simpleMessage( "Ricevi notifiche quando qualcuno aggiunge una foto a un album condiviso, di cui fai parte"), - "sharedWith": m53, + "sharedWith": m52, "sharedWithMe": MessageLookupByLibrary.simpleMessage("Condivisi con me"), "sharedWithYou": @@ -1225,11 +1221,11 @@ class MessageLookup extends MessageLookupByLibrary { "showMemories": MessageLookupByLibrary.simpleMessage("Mostra ricordi"), "signUpTerms": MessageLookupByLibrary.simpleMessage( "Accetto i termini di servizio e la politica sulla privacy"), - "singleFileDeleteFromDevice": m54, + "singleFileDeleteFromDevice": m53, "singleFileDeleteHighlight": MessageLookupByLibrary.simpleMessage( "Verrà eliminato da tutti gli album."), - "singleFileInBothLocalAndRemote": m55, - "singleFileInRemoteOnly": m56, + "singleFileInBothLocalAndRemote": m54, + "singleFileInRemoteOnly": m55, "skip": MessageLookupByLibrary.simpleMessage("Salta"), "social": MessageLookupByLibrary.simpleMessage("Social"), "someItemsAreInBothEnteAndYourDevice": @@ -1270,13 +1266,13 @@ class MessageLookup extends MessageLookupByLibrary { "storageBreakupFamily": MessageLookupByLibrary.simpleMessage("Famiglia"), "storageBreakupYou": MessageLookupByLibrary.simpleMessage("Tu"), - "storageInGB": m57, + "storageInGB": m56, "storageLimitExceeded": MessageLookupByLibrary.simpleMessage( "Limite d\'archiviazione superato"), - "storageUsageInfo": m58, + "storageUsageInfo": m57, "strongStrength": MessageLookupByLibrary.simpleMessage("Forte"), - "subAlreadyLinkedErrMessage": m59, - "subWillBeCancelledOn": m60, + "subAlreadyLinkedErrMessage": m58, + "subWillBeCancelledOn": m59, "subscribe": MessageLookupByLibrary.simpleMessage("Iscriviti"), "subscribeToEnableSharing": MessageLookupByLibrary.simpleMessage( "Sembra che il tuo abbonamento sia scaduto. Iscriviti per abilitare la condivisione."), @@ -1293,7 +1289,7 @@ class MessageLookup extends MessageLookupByLibrary { "suggestFeatures": MessageLookupByLibrary.simpleMessage("Suggerisci una funzionalità"), "support": MessageLookupByLibrary.simpleMessage("Assistenza"), - "syncProgress": m61, + "syncProgress": m60, "syncStopped": MessageLookupByLibrary.simpleMessage("Sincronizzazione interrotta"), "syncing": MessageLookupByLibrary.simpleMessage( @@ -1322,7 +1318,7 @@ class MessageLookup extends MessageLookupByLibrary { "theseItemsWillBeDeletedFromYourDevice": MessageLookupByLibrary.simpleMessage( "Questi file verranno eliminati dal tuo dispositivo."), - "theyAlsoGetXGb": m62, + "theyAlsoGetXGb": m61, "theyWillBeDeletedFromAllAlbums": MessageLookupByLibrary.simpleMessage( "Verranno eliminati da tutti gli album."), "thisActionCannotBeUndone": MessageLookupByLibrary.simpleMessage( @@ -1339,7 +1335,7 @@ class MessageLookup extends MessageLookupByLibrary { "Questo indirizzo email è già registrato"), "thisImageHasNoExifData": MessageLookupByLibrary.simpleMessage( "Questa immagine non ha dati EXIF"), - "thisIsPersonVerificationId": m63, + "thisIsPersonVerificationId": m62, "thisIsYourVerificationId": MessageLookupByLibrary.simpleMessage( "Questo è il tuo ID di verifica"), "thisWillLogYouOutOfTheFollowingDevice": @@ -1355,7 +1351,7 @@ class MessageLookup extends MessageLookupByLibrary { "total": MessageLookupByLibrary.simpleMessage("totale"), "totalSize": MessageLookupByLibrary.simpleMessage("Dimensioni totali"), "trash": MessageLookupByLibrary.simpleMessage("Cestino"), - "trashDaysLeft": m64, + "trashDaysLeft": m63, "tryAgain": MessageLookupByLibrary.simpleMessage("Riprova"), "turnOnBackupForAutoUpload": MessageLookupByLibrary.simpleMessage( "Attiva il backup per caricare automaticamente i file aggiunti in questa cartella del dispositivo su ente."), @@ -1412,7 +1408,7 @@ class MessageLookup extends MessageLookupByLibrary { "useSelectedPhoto": MessageLookupByLibrary.simpleMessage("Usa la foto selezionata"), "usedSpace": MessageLookupByLibrary.simpleMessage("Spazio utilizzato"), - "validTill": m65, + "validTill": m64, "verificationFailedPleaseTryAgain": MessageLookupByLibrary.simpleMessage( "Verifica fallita, per favore prova di nuovo"), @@ -1420,7 +1416,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("ID di verifica"), "verify": MessageLookupByLibrary.simpleMessage("Verifica"), "verifyEmail": MessageLookupByLibrary.simpleMessage("Verifica email"), - "verifyEmailID": m66, + "verifyEmailID": m65, "verifyIDLabel": MessageLookupByLibrary.simpleMessage("Verifica"), "verifyPassword": MessageLookupByLibrary.simpleMessage("Verifica password"), @@ -1447,11 +1443,11 @@ class MessageLookup extends MessageLookupByLibrary { "weDontSupportEditingPhotosAndAlbumsThatYouDont": MessageLookupByLibrary.simpleMessage( "Non puoi modificare foto e album che non possiedi"), - "weHaveSendEmailTo": m67, + "weHaveSendEmailTo": m66, "weakStrength": MessageLookupByLibrary.simpleMessage("Debole"), "welcomeBack": MessageLookupByLibrary.simpleMessage("Bentornato/a!"), "yearly": MessageLookupByLibrary.simpleMessage("Annuale"), - "yearsAgo": m68, + "yearsAgo": m67, "yes": MessageLookupByLibrary.simpleMessage("Si"), "yesCancel": MessageLookupByLibrary.simpleMessage("Sì, cancella"), "yesConvertToViewer": MessageLookupByLibrary.simpleMessage( @@ -1481,7 +1477,7 @@ class MessageLookup extends MessageLookupByLibrary { "Non puoi condividere con te stesso"), "youDontHaveAnyArchivedItems": MessageLookupByLibrary.simpleMessage( "Non hai nulla di archiviato."), - "youHaveSuccessfullyFreedUp": m69, + "youHaveSuccessfullyFreedUp": m68, "yourAccountHasBeenDeleted": MessageLookupByLibrary.simpleMessage( "Il tuo account è stato eliminato"), "yourMap": MessageLookupByLibrary.simpleMessage("Your map"), diff --git a/mobile/lib/generated/intl/messages_nl.dart b/mobile/lib/generated/intl/messages_nl.dart index 1f53a6ffe4..fe49550d66 100644 --- a/mobile/lib/generated/intl/messages_nl.dart +++ b/mobile/lib/generated/intl/messages_nl.dart @@ -131,91 +131,88 @@ class MessageLookup extends MessageLookupByLibrary { static String m37(providerName) => "Praat met ${providerName} klantenservice als u in rekening bent gebracht"; - static String m38(reason) => - "Helaas is uw betaling mislukt vanwege ${reason}"; - - static String m39(endDate) => + static String m38(endDate) => "Gratis proefperiode geldig tot ${endDate}.\nU kunt naderhand een betaald abonnement kiezen."; - static String m40(toEmail) => "Stuur ons een e-mail op ${toEmail}"; + static String m39(toEmail) => "Stuur ons een e-mail op ${toEmail}"; - static String m41(toEmail) => + static String m40(toEmail) => "Verstuur de logboeken alstublieft naar ${toEmail}"; - static String m42(storeName) => "Beoordeel ons op ${storeName}"; + static String m41(storeName) => "Beoordeel ons op ${storeName}"; - static String m43(storageInGB) => + static String m42(storageInGB) => "Jullie krijgen allebei ${storageInGB} GB* gratis"; - static String m44(userEmail) => + static String m43(userEmail) => "${userEmail} zal worden verwijderd uit dit gedeelde album\n\nAlle door hen toegevoegde foto\'s worden ook uit het album verwijderd"; - static String m45(endDate) => "Wordt verlengd op ${endDate}"; + static String m44(endDate) => "Wordt verlengd op ${endDate}"; - static String m46(count) => + static String m45(count) => "${Intl.plural(count, one: '${count} resultaat gevonden', other: '${count} resultaten gevonden')}"; - static String m47(count) => "${count} geselecteerd"; + static String m46(count) => "${count} geselecteerd"; - static String m48(count, yourCount) => + static String m47(count, yourCount) => "${count} geselecteerd (${yourCount} van jou)"; - static String m49(verificationID) => + static String m48(verificationID) => "Hier is mijn verificatie-ID: ${verificationID} voor ente.io."; - static String m50(verificationID) => + static String m49(verificationID) => "Hey, kunt u bevestigen dat dit uw ente.io verificatie-ID is: ${verificationID}"; - static String m51(referralCode, referralStorageInGB) => + static String m50(referralCode, referralStorageInGB) => "ente verwijzingscode: ${referralCode} \n\nPas het toe bij Instellingen → Algemeen → Verwijzingen om ${referralStorageInGB} GB gratis te krijgen nadat je je hebt aangemeld voor een betaald abonnement\n\nhttps://ente.io"; - static String m52(numberOfPeople) => + static String m51(numberOfPeople) => "${Intl.plural(numberOfPeople, zero: 'Deel met specifieke mensen', one: 'Gedeeld met 1 persoon', other: 'Gedeeld met ${numberOfPeople} mensen')}"; - static String m53(emailIDs) => "Gedeeld met ${emailIDs}"; + static String m52(emailIDs) => "Gedeeld met ${emailIDs}"; - static String m54(fileType) => + static String m53(fileType) => "Deze ${fileType} zal worden verwijderd van jouw apparaat."; - static String m55(fileType) => + static String m54(fileType) => "Deze ${fileType} staat zowel in ente als op jouw apparaat."; - static String m56(fileType) => + static String m55(fileType) => "Deze ${fileType} zal worden verwijderd uit ente."; - static String m57(storageAmountInGB) => "${storageAmountInGB} GB"; + static String m56(storageAmountInGB) => "${storageAmountInGB} GB"; - static String m58( + static String m57( usedAmount, usedStorageUnit, totalAmount, totalStorageUnit) => "${usedAmount} ${usedStorageUnit} van ${totalAmount} ${totalStorageUnit} gebruikt"; - static String m59(id) => + static String m58(id) => "Uw ${id} is al aan een ander ente account gekoppeld.\nAls u uw ${id} wilt gebruiken met dit account, neem dan contact op met onze klantenservice"; - static String m60(endDate) => "Uw abonnement loopt af op ${endDate}"; + static String m59(endDate) => "Uw abonnement loopt af op ${endDate}"; - static String m61(completed, total) => + static String m60(completed, total) => "${completed}/${total} herinneringen bewaard"; - static String m62(storageAmountInGB) => + static String m61(storageAmountInGB) => "Zij krijgen ook ${storageAmountInGB} GB"; - static String m63(email) => "Dit is de verificatie-ID van ${email}"; + static String m62(email) => "Dit is de verificatie-ID van ${email}"; - static String m64(count) => + static String m63(count) => "${Intl.plural(count, zero: '', one: '1 dag', other: '${count} dagen')}"; - static String m65(endDate) => "Geldig tot ${endDate}"; + static String m64(endDate) => "Geldig tot ${endDate}"; - static String m66(email) => "Verifieer ${email}"; + static String m65(email) => "Verifieer ${email}"; - static String m67(email) => + static String m66(email) => "We hebben een e-mail gestuurd naar ${email}"; - static String m68(count) => + static String m67(count) => "${Intl.plural(count, one: '${count} jaar geleden', other: '${count} jaar geleden')}"; - static String m69(storageSaved) => + static String m68(storageSaved) => "Je hebt ${storageSaved} succesvol vrijgemaakt!"; final messages = _notInlinedMessages(_notInlinedMessages); @@ -1020,7 +1017,6 @@ class MessageLookup extends MessageLookupByLibrary { "paymentFailed": MessageLookupByLibrary.simpleMessage("Betaling mislukt"), "paymentFailedTalkToProvider": m37, - "paymentFailedWithReason": m38, "pendingItems": MessageLookupByLibrary.simpleMessage("Bestanden in behandeling"), "pendingSync": MessageLookupByLibrary.simpleMessage( @@ -1048,7 +1044,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Album bovenaan vastzetten"), "playOnTv": MessageLookupByLibrary.simpleMessage("Album afspelen op TV"), - "playStoreFreeTrialValidTill": m39, + "playStoreFreeTrialValidTill": m38, "playstoreSubscription": MessageLookupByLibrary.simpleMessage("PlayStore abonnement"), "pleaseCheckYourInternetConnectionAndTryAgain": @@ -1060,12 +1056,12 @@ class MessageLookup extends MessageLookupByLibrary { "pleaseContactSupportIfTheProblemPersists": MessageLookupByLibrary.simpleMessage( "Neem contact op met klantenservice als het probleem aanhoudt"), - "pleaseEmailUsAt": m40, + "pleaseEmailUsAt": m39, "pleaseGrantPermissions": MessageLookupByLibrary.simpleMessage( "Geef alstublieft toestemming"), "pleaseLoginAgain": MessageLookupByLibrary.simpleMessage("Log opnieuw in"), - "pleaseSendTheLogsTo": m41, + "pleaseSendTheLogsTo": m40, "pleaseTryAgain": MessageLookupByLibrary.simpleMessage("Probeer het nog eens"), "pleaseVerifyTheCodeYouHaveEntered": @@ -1100,7 +1096,7 @@ class MessageLookup extends MessageLookupByLibrary { "raiseTicket": MessageLookupByLibrary.simpleMessage("Meld probleem"), "rateTheApp": MessageLookupByLibrary.simpleMessage("Beoordeel de app"), "rateUs": MessageLookupByLibrary.simpleMessage("Beoordeel ons"), - "rateUsOnStore": m42, + "rateUsOnStore": m41, "recover": MessageLookupByLibrary.simpleMessage("Herstellen"), "recoverAccount": MessageLookupByLibrary.simpleMessage("Account herstellen"), @@ -1131,7 +1127,7 @@ class MessageLookup extends MessageLookupByLibrary { "1. Geef deze code aan je vrienden"), "referralStep2": MessageLookupByLibrary.simpleMessage( "2. Ze registreren voor een betaald plan"), - "referralStep3": m43, + "referralStep3": m42, "referrals": MessageLookupByLibrary.simpleMessage("Referenties"), "referralsAreCurrentlyPaused": MessageLookupByLibrary.simpleMessage( "Verwijzingen zijn momenteel gepauzeerd"), @@ -1157,7 +1153,7 @@ class MessageLookup extends MessageLookupByLibrary { "removeLink": MessageLookupByLibrary.simpleMessage("Verwijder link"), "removeParticipant": MessageLookupByLibrary.simpleMessage("Deelnemer verwijderen"), - "removeParticipantBody": m44, + "removeParticipantBody": m43, "removePublicLink": MessageLookupByLibrary.simpleMessage("Verwijder publieke link"), "removeShareItemsWarning": MessageLookupByLibrary.simpleMessage( @@ -1173,7 +1169,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Bestandsnaam wijzigen"), "renewSubscription": MessageLookupByLibrary.simpleMessage("Abonnement verlengen"), - "renewsOn": m45, + "renewsOn": m44, "reportABug": MessageLookupByLibrary.simpleMessage("Een fout melden"), "reportBug": MessageLookupByLibrary.simpleMessage("Fout melden"), "resendEmail": @@ -1235,7 +1231,7 @@ class MessageLookup extends MessageLookupByLibrary { "Foto\'s groeperen die in een bepaalde straal van een foto worden genomen"), "searchPeopleEmptySection": MessageLookupByLibrary.simpleMessage( "Nodig mensen uit, en je ziet alle foto\'s die door hen worden gedeeld hier"), - "searchResultCount": m46, + "searchResultCount": m45, "security": MessageLookupByLibrary.simpleMessage("Beveiliging"), "selectALocation": MessageLookupByLibrary.simpleMessage("Selecteer een locatie"), @@ -1262,8 +1258,8 @@ class MessageLookup extends MessageLookupByLibrary { "selectedItemsWillBeDeletedFromAllAlbumsAndMoved": MessageLookupByLibrary.simpleMessage( "Geselecteerde bestanden worden verwijderd uit alle albums en verplaatst naar de prullenbak."), - "selectedPhotos": m47, - "selectedPhotosWithYours": m48, + "selectedPhotos": m46, + "selectedPhotosWithYours": m47, "send": MessageLookupByLibrary.simpleMessage("Verzenden"), "sendEmail": MessageLookupByLibrary.simpleMessage("E-mail versturen"), "sendInvite": @@ -1287,16 +1283,16 @@ class MessageLookup extends MessageLookupByLibrary { "shareAnAlbumNow": MessageLookupByLibrary.simpleMessage("Deel nu een album"), "shareLink": MessageLookupByLibrary.simpleMessage("Link delen"), - "shareMyVerificationID": m49, + "shareMyVerificationID": m48, "shareOnlyWithThePeopleYouWant": MessageLookupByLibrary.simpleMessage( "Deel alleen met de mensen die u wilt"), - "shareTextConfirmOthersVerificationID": m50, + "shareTextConfirmOthersVerificationID": m49, "shareTextRecommendUsingEnte": MessageLookupByLibrary.simpleMessage( "Download ente zodat we gemakkelijk foto\'s en video\'s van originele kwaliteit kunnen delen\n\nhttps://ente.io"), - "shareTextReferralCode": m51, + "shareTextReferralCode": m50, "shareWithNonenteUsers": MessageLookupByLibrary.simpleMessage( "Delen met niet-ente gebruikers"), - "shareWithPeopleSectionTitle": m52, + "shareWithPeopleSectionTitle": m51, "shareYourFirstAlbum": MessageLookupByLibrary.simpleMessage("Deel jouw eerste album"), "sharedAlbumSectionDescription": MessageLookupByLibrary.simpleMessage( @@ -1307,7 +1303,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Nieuwe gedeelde foto\'s"), "sharedPhotoNotificationsExplanation": MessageLookupByLibrary.simpleMessage( "Ontvang meldingen wanneer iemand een foto toevoegt aan een gedeeld album waar je deel van uitmaakt"), - "sharedWith": m53, + "sharedWith": m52, "sharedWithMe": MessageLookupByLibrary.simpleMessage("Gedeeld met mij"), "sharedWithYou": MessageLookupByLibrary.simpleMessage("Gedeeld met jou"), @@ -1322,11 +1318,11 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Log uit op andere apparaten"), "signUpTerms": MessageLookupByLibrary.simpleMessage( "Ik ga akkoord met de gebruiksvoorwaarden en privacybeleid"), - "singleFileDeleteFromDevice": m54, + "singleFileDeleteFromDevice": m53, "singleFileDeleteHighlight": MessageLookupByLibrary.simpleMessage( "Het wordt uit alle albums verwijderd."), - "singleFileInBothLocalAndRemote": m55, - "singleFileInRemoteOnly": m56, + "singleFileInBothLocalAndRemote": m54, + "singleFileInRemoteOnly": m55, "skip": MessageLookupByLibrary.simpleMessage("Overslaan"), "social": MessageLookupByLibrary.simpleMessage("Sociale media"), "someItemsAreInBothEnteAndYourDevice": MessageLookupByLibrary.simpleMessage( @@ -1364,13 +1360,13 @@ class MessageLookup extends MessageLookupByLibrary { "storage": MessageLookupByLibrary.simpleMessage("Opslagruimte"), "storageBreakupFamily": MessageLookupByLibrary.simpleMessage("Familie"), "storageBreakupYou": MessageLookupByLibrary.simpleMessage("Jij"), - "storageInGB": m57, + "storageInGB": m56, "storageLimitExceeded": MessageLookupByLibrary.simpleMessage("Opslaglimiet overschreden"), - "storageUsageInfo": m58, + "storageUsageInfo": m57, "strongStrength": MessageLookupByLibrary.simpleMessage("Sterk"), - "subAlreadyLinkedErrMessage": m59, - "subWillBeCancelledOn": m60, + "subAlreadyLinkedErrMessage": m58, + "subWillBeCancelledOn": m59, "subscribe": MessageLookupByLibrary.simpleMessage("Abonneer"), "subscribeToEnableSharing": MessageLookupByLibrary.simpleMessage( "Het lijkt erop dat je abonnement is verlopen. Abonneer om delen mogelijk te maken."), @@ -1387,7 +1383,7 @@ class MessageLookup extends MessageLookupByLibrary { "suggestFeatures": MessageLookupByLibrary.simpleMessage("Features voorstellen"), "support": MessageLookupByLibrary.simpleMessage("Ondersteuning"), - "syncProgress": m61, + "syncProgress": m60, "syncStopped": MessageLookupByLibrary.simpleMessage("Synchronisatie gestopt"), "syncing": MessageLookupByLibrary.simpleMessage("Synchroniseren..."), @@ -1415,7 +1411,7 @@ class MessageLookup extends MessageLookupByLibrary { "theseItemsWillBeDeletedFromYourDevice": MessageLookupByLibrary.simpleMessage( "Deze bestanden zullen worden verwijderd van uw apparaat."), - "theyAlsoGetXGb": m62, + "theyAlsoGetXGb": m61, "theyWillBeDeletedFromAllAlbums": MessageLookupByLibrary.simpleMessage( "Ze zullen uit alle albums worden verwijderd."), "thisActionCannotBeUndone": MessageLookupByLibrary.simpleMessage( @@ -1431,7 +1427,7 @@ class MessageLookup extends MessageLookupByLibrary { "Dit e-mailadres is al in gebruik"), "thisImageHasNoExifData": MessageLookupByLibrary.simpleMessage( "Deze foto heeft geen exif gegevens"), - "thisIsPersonVerificationId": m63, + "thisIsPersonVerificationId": m62, "thisIsYourVerificationId": MessageLookupByLibrary.simpleMessage("Dit is uw verificatie-ID"), "thisWillLogYouOutOfTheFollowingDevice": @@ -1448,7 +1444,7 @@ class MessageLookup extends MessageLookupByLibrary { "total": MessageLookupByLibrary.simpleMessage("totaal"), "totalSize": MessageLookupByLibrary.simpleMessage("Totale grootte"), "trash": MessageLookupByLibrary.simpleMessage("Prullenbak"), - "trashDaysLeft": m64, + "trashDaysLeft": m63, "tryAgain": MessageLookupByLibrary.simpleMessage("Probeer opnieuw"), "turnOnBackupForAutoUpload": MessageLookupByLibrary.simpleMessage( "Schakel back-up in om bestanden die toegevoegd zijn aan deze map op dit apparaat automatisch te uploaden."), @@ -1504,7 +1500,7 @@ class MessageLookup extends MessageLookupByLibrary { "useSelectedPhoto": MessageLookupByLibrary.simpleMessage("Gebruik geselecteerde foto"), "usedSpace": MessageLookupByLibrary.simpleMessage("Gebruikte ruimte"), - "validTill": m65, + "validTill": m64, "verificationFailedPleaseTryAgain": MessageLookupByLibrary.simpleMessage( "Verificatie mislukt, probeer het opnieuw"), @@ -1512,7 +1508,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Verificatie ID"), "verify": MessageLookupByLibrary.simpleMessage("Verifiëren"), "verifyEmail": MessageLookupByLibrary.simpleMessage("Bevestig e-mail"), - "verifyEmailID": m66, + "verifyEmailID": m65, "verifyIDLabel": MessageLookupByLibrary.simpleMessage("Verifiëren"), "verifyPassword": MessageLookupByLibrary.simpleMessage("Bevestig wachtwoord"), @@ -1541,11 +1537,11 @@ class MessageLookup extends MessageLookupByLibrary { "weDontSupportEditingPhotosAndAlbumsThatYouDont": MessageLookupByLibrary.simpleMessage( "We ondersteunen het bewerken van foto\'s en albums waar je niet de eigenaar van bent nog niet"), - "weHaveSendEmailTo": m67, + "weHaveSendEmailTo": m66, "weakStrength": MessageLookupByLibrary.simpleMessage("Zwak"), "welcomeBack": MessageLookupByLibrary.simpleMessage("Welkom terug!"), "yearly": MessageLookupByLibrary.simpleMessage("Jaarlijks"), - "yearsAgo": m68, + "yearsAgo": m67, "yes": MessageLookupByLibrary.simpleMessage("Ja"), "yesCancel": MessageLookupByLibrary.simpleMessage("Ja, opzeggen"), "yesConvertToViewer": @@ -1575,7 +1571,7 @@ class MessageLookup extends MessageLookupByLibrary { "Je kunt niet met jezelf delen"), "youDontHaveAnyArchivedItems": MessageLookupByLibrary.simpleMessage( "U heeft geen gearchiveerde bestanden."), - "youHaveSuccessfullyFreedUp": m69, + "youHaveSuccessfullyFreedUp": m68, "yourAccountHasBeenDeleted": MessageLookupByLibrary.simpleMessage("Je account is verwijderd"), "yourMap": MessageLookupByLibrary.simpleMessage("Jouw kaart"), diff --git a/mobile/lib/generated/intl/messages_pt.dart b/mobile/lib/generated/intl/messages_pt.dart index ffdc0d2573..71de452972 100644 --- a/mobile/lib/generated/intl/messages_pt.dart +++ b/mobile/lib/generated/intl/messages_pt.dart @@ -21,7 +21,7 @@ class MessageLookup extends MessageLookupByLibrary { String get localeName => 'pt'; static String m0(count) => - "${Intl.plural(count, zero: 'Add collaborator', one: 'Add collaborator', other: 'Add collaborators')}"; + "${Intl.plural(count, zero: 'Adicionar colaborador', one: 'Adicionar coloborador', other: 'Adicionar colaboradores')}"; static String m2(count) => "${Intl.plural(count, one: 'Adicionar item', other: 'Adicionar itens')}"; @@ -30,7 +30,7 @@ class MessageLookup extends MessageLookupByLibrary { "Seu complemento ${storageAmount} é válido até o dia ${endDate}"; static String m1(count) => - "${Intl.plural(count, zero: 'Add viewer', one: 'Add viewer', other: 'Add viewers')}"; + "${Intl.plural(count, zero: 'Adicionar visualizador', one: 'Adicionar visualizador', other: 'Adicionar Visualizadores')}"; static String m4(emailOrName) => "Adicionado por ${emailOrName}"; @@ -130,88 +130,85 @@ class MessageLookup extends MessageLookupByLibrary { static String m37(providerName) => "Por favor, fale com o suporte ${providerName} se você foi cobrado"; - static String m38(reason) => - "Infelizmente o seu pagamento falhou devido a ${reason}"; - - static String m39(endDate) => + static String m38(endDate) => "Teste gratuito válido até ${endDate}.\nVocê pode escolher um plano pago depois."; - static String m40(toEmail) => + static String m39(toEmail) => "Por favor, envie-nos um e-mail para ${toEmail}"; - static String m41(toEmail) => "Por favor, envie os logs para \n${toEmail}"; + static String m40(toEmail) => "Por favor, envie os logs para \n${toEmail}"; - static String m42(storeName) => "Avalie-nos em ${storeName}"; + static String m41(storeName) => "Avalie-nos em ${storeName}"; - static String m43(storageInGB) => "3. Ambos ganham ${storageInGB} GB* grátis"; + static String m42(storageInGB) => "3. Ambos ganham ${storageInGB} GB* grátis"; - static String m44(userEmail) => + static String m43(userEmail) => "${userEmail} será removido deste álbum compartilhado\n\nQuaisquer fotos adicionadas por eles também serão removidas do álbum"; - static String m45(endDate) => "Renovação de assinatura em ${endDate}"; + static String m44(endDate) => "Renovação de assinatura em ${endDate}"; - static String m46(count) => + static String m45(count) => "${Intl.plural(count, one: '${count} resultado encontrado', other: '${count} resultado encontrado')}"; - static String m47(count) => "${count} Selecionados"; + static String m46(count) => "${count} Selecionados"; - static String m48(count, yourCount) => + static String m47(count, yourCount) => "${count} Selecionado (${yourCount} seus)"; - static String m49(verificationID) => + static String m48(verificationID) => "Aqui está meu ID de verificação para o Ente.io: ${verificationID}"; - static String m50(verificationID) => + static String m49(verificationID) => "Ei, você pode confirmar que este é seu ID de verificação do Ente.io? ${verificationID}"; - static String m51(referralCode, referralStorageInGB) => + static String m50(referralCode, referralStorageInGB) => "Código de referência do ente: ${referralCode} \n\nAplique em Configurações → Geral → Indicações para obter ${referralStorageInGB} GB gratuitamente após a sua inscrição em um plano pago\n\nhttps://ente.io"; - static String m52(numberOfPeople) => + static String m51(numberOfPeople) => "${Intl.plural(numberOfPeople, zero: 'Compartilhe com pessoas específicas', one: 'Compartilhado com 1 pessoa', other: 'Compartilhado com ${numberOfPeople} pessoas')}"; - static String m53(emailIDs) => "Compartilhado com ${emailIDs}"; + static String m52(emailIDs) => "Compartilhado com ${emailIDs}"; - static String m54(fileType) => + static String m53(fileType) => "Este ${fileType} será excluído do seu dispositivo."; - static String m55(fileType) => + static String m54(fileType) => "Este ${fileType} está em ente e no seu dispositivo."; - static String m56(fileType) => "Este ${fileType} será excluído do ente."; + static String m55(fileType) => "Este ${fileType} será excluído do ente."; - static String m57(storageAmountInGB) => "${storageAmountInGB} GB"; + static String m56(storageAmountInGB) => "${storageAmountInGB} GB"; - static String m58( + static String m57( usedAmount, usedStorageUnit, totalAmount, totalStorageUnit) => "${usedAmount} ${usedStorageUnit} de ${totalAmount} ${totalStorageUnit} usado"; - static String m59(id) => + static String m58(id) => "Seu ${id} já está vinculado a outra conta ente.\nSe você gostaria de usar seu ${id} com esta conta, por favor contate nosso suporte\'\'"; - static String m60(endDate) => "Sua assinatura será cancelada em ${endDate}"; + static String m59(endDate) => "Sua assinatura será cancelada em ${endDate}"; - static String m61(completed, total) => + static String m60(completed, total) => "${completed}/${total} memórias preservadas"; - static String m62(storageAmountInGB) => + static String m61(storageAmountInGB) => "Eles também recebem ${storageAmountInGB} GB"; - static String m63(email) => "Este é o ID de verificação de ${email}"; + static String m62(email) => "Este é o ID de verificação de ${email}"; - static String m64(count) => + static String m63(count) => "${Intl.plural(count, zero: '', one: '1 dia', other: '${count} dias')}"; - static String m65(endDate) => "Válido até ${endDate}"; + static String m64(endDate) => "Válido até ${endDate}"; - static String m66(email) => "Verificar ${email}"; + static String m65(email) => "Verificar ${email}"; - static String m67(email) => "Enviamos um e-mail à ${email}"; + static String m66(email) => "Enviamos um e-mail à ${email}"; - static String m68(count) => + static String m67(count) => "${Intl.plural(count, one: '${count} anos atrás', other: '${count} anos atrás')}"; - static String m69(storageSaved) => + static String m68(storageSaved) => "Você liberou ${storageSaved} com sucesso!"; final messages = _notInlinedMessages(_notInlinedMessages); @@ -312,7 +309,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Aplicar código"), "appstoreSubscription": MessageLookupByLibrary.simpleMessage("Assinatura da AppStore"), - "archive": MessageLookupByLibrary.simpleMessage("Arquivo"), + "archive": MessageLookupByLibrary.simpleMessage("Arquivado"), "archiveAlbum": MessageLookupByLibrary.simpleMessage("Arquivar álbum"), "archiving": MessageLookupByLibrary.simpleMessage("Arquivando..."), "areYouSureThatYouWantToLeaveTheFamily": @@ -523,7 +520,7 @@ class MessageLookup extends MessageLookupByLibrary { "decryptingVideo": MessageLookupByLibrary.simpleMessage("Descriptografando vídeo..."), "deduplicateFiles": - MessageLookupByLibrary.simpleMessage("Arquivos Deduplicados"), + MessageLookupByLibrary.simpleMessage("Arquivos duplicados"), "delete": MessageLookupByLibrary.simpleMessage("Apagar"), "deleteAccount": MessageLookupByLibrary.simpleMessage("Excluir conta"), "deleteAccountFeedbackPrompt": MessageLookupByLibrary.simpleMessage( @@ -769,7 +766,8 @@ class MessageLookup extends MessageLookupByLibrary { "Não rastreamos instalações do aplicativo. Seria útil se você nos contasse onde nos encontrou!"), "hearUsWhereTitle": MessageLookupByLibrary.simpleMessage( "Como você ouviu sobre o Ente? (opcional)"), - "hidden": MessageLookupByLibrary.simpleMessage("Escondido"), + "help": MessageLookupByLibrary.simpleMessage("Ajuda"), + "hidden": MessageLookupByLibrary.simpleMessage("Oculto"), "hide": MessageLookupByLibrary.simpleMessage("Ocultar"), "hiding": MessageLookupByLibrary.simpleMessage("Ocultando..."), "hostedAtOsmFrance": @@ -896,7 +894,7 @@ class MessageLookup extends MessageLookupByLibrary { "Isso enviará através dos logs para nos ajudar a depurar o seu problema. Por favor, note que nomes de arquivos serão incluídos para ajudar a rastrear problemas com arquivos específicos."), "longPressAnEmailToVerifyEndToEndEncryption": MessageLookupByLibrary.simpleMessage( - "Long press an email to verify end to end encryption."), + "Pressione e segure um e-mail para verificar a criptografia de ponta a ponta."), "longpressOnAnItemToViewInFullscreen": MessageLookupByLibrary.simpleMessage( "Pressione e segure em um item para exibir em tela cheia"), @@ -1014,8 +1012,9 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Detalhes de pagamento"), "paymentFailed": MessageLookupByLibrary.simpleMessage("Falha no pagamento"), + "paymentFailedMessage": MessageLookupByLibrary.simpleMessage( + "Infelizmente o seu pagamento falhou. Entre em contato com o suporte e nós ajudaremos você!"), "paymentFailedTalkToProvider": m37, - "paymentFailedWithReason": m38, "pendingItems": MessageLookupByLibrary.simpleMessage("Itens pendentes"), "pendingSync": MessageLookupByLibrary.simpleMessage("Sincronização pendente"), @@ -1041,7 +1040,7 @@ class MessageLookup extends MessageLookupByLibrary { "pinAlbum": MessageLookupByLibrary.simpleMessage("Fixar álbum"), "playOnTv": MessageLookupByLibrary.simpleMessage("Reproduzir álbum na TV"), - "playStoreFreeTrialValidTill": m39, + "playStoreFreeTrialValidTill": m38, "playstoreSubscription": MessageLookupByLibrary.simpleMessage("Assinatura da PlayStore"), "pleaseCheckYourInternetConnectionAndTryAgain": @@ -1053,12 +1052,12 @@ class MessageLookup extends MessageLookupByLibrary { "pleaseContactSupportIfTheProblemPersists": MessageLookupByLibrary.simpleMessage( "Por favor, contate o suporte se o problema persistir"), - "pleaseEmailUsAt": m40, + "pleaseEmailUsAt": m39, "pleaseGrantPermissions": MessageLookupByLibrary.simpleMessage( "Por favor, conceda as permissões"), "pleaseLoginAgain": MessageLookupByLibrary.simpleMessage( "Por favor, faça login novamente"), - "pleaseSendTheLogsTo": m41, + "pleaseSendTheLogsTo": m40, "pleaseTryAgain": MessageLookupByLibrary.simpleMessage("Por favor, tente novamente"), "pleaseVerifyTheCodeYouHaveEntered": @@ -1095,7 +1094,7 @@ class MessageLookup extends MessageLookupByLibrary { "rateTheApp": MessageLookupByLibrary.simpleMessage("Avalie o aplicativo"), "rateUs": MessageLookupByLibrary.simpleMessage("Avalie-nos"), - "rateUsOnStore": m42, + "rateUsOnStore": m41, "recover": MessageLookupByLibrary.simpleMessage("Recuperar"), "recoverAccount": MessageLookupByLibrary.simpleMessage("Recuperar conta"), @@ -1127,7 +1126,7 @@ class MessageLookup extends MessageLookupByLibrary { "Envie esse código aos seus amigos"), "referralStep2": MessageLookupByLibrary.simpleMessage( "2. Eles se inscreveram para um plano pago"), - "referralStep3": m43, + "referralStep3": m42, "referrals": MessageLookupByLibrary.simpleMessage("Indicações"), "referralsAreCurrentlyPaused": MessageLookupByLibrary.simpleMessage( "Referências estão atualmente pausadas"), @@ -1151,7 +1150,7 @@ class MessageLookup extends MessageLookupByLibrary { "removeLink": MessageLookupByLibrary.simpleMessage("Remover link"), "removeParticipant": MessageLookupByLibrary.simpleMessage("Remover participante"), - "removeParticipantBody": m44, + "removeParticipantBody": m43, "removePublicLink": MessageLookupByLibrary.simpleMessage("Remover link público"), "removeShareItemsWarning": MessageLookupByLibrary.simpleMessage( @@ -1165,7 +1164,7 @@ class MessageLookup extends MessageLookupByLibrary { "renameFile": MessageLookupByLibrary.simpleMessage("Renomear arquivo"), "renewSubscription": MessageLookupByLibrary.simpleMessage("Renovar assinatura"), - "renewsOn": m45, + "renewsOn": m44, "reportABug": MessageLookupByLibrary.simpleMessage("Reportar um problema"), "reportBug": @@ -1230,7 +1229,7 @@ class MessageLookup extends MessageLookupByLibrary { "Fotos de grupo que estão sendo tiradas em algum raio da foto"), "searchPeopleEmptySection": MessageLookupByLibrary.simpleMessage( "Convide pessoas e você verá todas as fotos compartilhadas por elas aqui"), - "searchResultCount": m46, + "searchResultCount": m45, "security": MessageLookupByLibrary.simpleMessage("Segurança"), "selectALocation": MessageLookupByLibrary.simpleMessage("Selecionar um local"), @@ -1258,8 +1257,8 @@ class MessageLookup extends MessageLookupByLibrary { "selectedItemsWillBeDeletedFromAllAlbumsAndMoved": MessageLookupByLibrary.simpleMessage( "Os itens selecionados serão excluídos de todos os álbuns e movidos para o lixo."), - "selectedPhotos": m47, - "selectedPhotosWithYours": m48, + "selectedPhotos": m46, + "selectedPhotosWithYours": m47, "send": MessageLookupByLibrary.simpleMessage("Enviar"), "sendEmail": MessageLookupByLibrary.simpleMessage("Enviar e-mail"), "sendInvite": MessageLookupByLibrary.simpleMessage("Enviar convite"), @@ -1283,16 +1282,16 @@ class MessageLookup extends MessageLookupByLibrary { "shareAnAlbumNow": MessageLookupByLibrary.simpleMessage("Compartilhar um álbum agora"), "shareLink": MessageLookupByLibrary.simpleMessage("Compartilhar link"), - "shareMyVerificationID": m49, + "shareMyVerificationID": m48, "shareOnlyWithThePeopleYouWant": MessageLookupByLibrary.simpleMessage( "Compartilhar apenas com as pessoas que você quiser"), - "shareTextConfirmOthersVerificationID": m50, + "shareTextConfirmOthersVerificationID": m49, "shareTextRecommendUsingEnte": MessageLookupByLibrary.simpleMessage( "Baixe o Ente para podermos compartilhar facilmente fotos e vídeos de alta qualidade\n\nhttps://ente.io"), - "shareTextReferralCode": m51, + "shareTextReferralCode": m50, "shareWithNonenteUsers": MessageLookupByLibrary.simpleMessage( "Compartilhar com usuários não-Ente"), - "shareWithPeopleSectionTitle": m52, + "shareWithPeopleSectionTitle": m51, "shareYourFirstAlbum": MessageLookupByLibrary.simpleMessage( "Compartilhar seu primeiro álbum"), "sharedAlbumSectionDescription": MessageLookupByLibrary.simpleMessage( @@ -1305,7 +1304,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Novas fotos compartilhadas"), "sharedPhotoNotificationsExplanation": MessageLookupByLibrary.simpleMessage( "Receber notificações quando alguém adicionar uma foto em um álbum compartilhado que você faz parte"), - "sharedWith": m53, + "sharedWith": m52, "sharedWithMe": MessageLookupByLibrary.simpleMessage("Compartilhado comigo"), "sharedWithYou": @@ -1321,11 +1320,11 @@ class MessageLookup extends MessageLookupByLibrary { "Terminar sessão em outros dispositivos"), "signUpTerms": MessageLookupByLibrary.simpleMessage( "Eu concordo com os termos de serviço e a política de privacidade"), - "singleFileDeleteFromDevice": m54, + "singleFileDeleteFromDevice": m53, "singleFileDeleteHighlight": MessageLookupByLibrary.simpleMessage( "Ele será excluído de todos os álbuns."), - "singleFileInBothLocalAndRemote": m55, - "singleFileInRemoteOnly": m56, + "singleFileInBothLocalAndRemote": m54, + "singleFileInRemoteOnly": m55, "skip": MessageLookupByLibrary.simpleMessage("Pular"), "social": MessageLookupByLibrary.simpleMessage("Redes sociais"), "someItemsAreInBothEnteAndYourDevice": @@ -1366,13 +1365,13 @@ class MessageLookup extends MessageLookupByLibrary { "storage": MessageLookupByLibrary.simpleMessage("Armazenamento"), "storageBreakupFamily": MessageLookupByLibrary.simpleMessage("Família"), "storageBreakupYou": MessageLookupByLibrary.simpleMessage("Você"), - "storageInGB": m57, + "storageInGB": m56, "storageLimitExceeded": MessageLookupByLibrary.simpleMessage( "Limite de armazenamento excedido"), - "storageUsageInfo": m58, + "storageUsageInfo": m57, "strongStrength": MessageLookupByLibrary.simpleMessage("Forte"), - "subAlreadyLinkedErrMessage": m59, - "subWillBeCancelledOn": m60, + "subAlreadyLinkedErrMessage": m58, + "subWillBeCancelledOn": m59, "subscribe": MessageLookupByLibrary.simpleMessage("Assinar"), "subscribeToEnableSharing": MessageLookupByLibrary.simpleMessage( "Parece que sua assinatura expirou. Por favor inscreva-se para ativar o compartilhamento."), @@ -1387,9 +1386,9 @@ class MessageLookup extends MessageLookupByLibrary { "successfullyUnhid": MessageLookupByLibrary.simpleMessage("Desocultado com sucesso"), "suggestFeatures": - MessageLookupByLibrary.simpleMessage("Sugerir funcionalidades"), + MessageLookupByLibrary.simpleMessage("Sugerir recurso"), "support": MessageLookupByLibrary.simpleMessage("Suporte"), - "syncProgress": m61, + "syncProgress": m60, "syncStopped": MessageLookupByLibrary.simpleMessage("Sincronização interrompida"), "syncing": MessageLookupByLibrary.simpleMessage("Sincronizando..."), @@ -1416,7 +1415,7 @@ class MessageLookup extends MessageLookupByLibrary { "theseItemsWillBeDeletedFromYourDevice": MessageLookupByLibrary.simpleMessage( "Estes itens serão excluídos do seu dispositivo."), - "theyAlsoGetXGb": m62, + "theyAlsoGetXGb": m61, "theyWillBeDeletedFromAllAlbums": MessageLookupByLibrary.simpleMessage( "Ele será excluído de todos os álbuns."), "thisActionCannotBeUndone": MessageLookupByLibrary.simpleMessage( @@ -1432,7 +1431,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Este e-mail já está em uso"), "thisImageHasNoExifData": MessageLookupByLibrary.simpleMessage( "Esta imagem não tem dados exif"), - "thisIsPersonVerificationId": m63, + "thisIsPersonVerificationId": m62, "thisIsYourVerificationId": MessageLookupByLibrary.simpleMessage( "Este é o seu ID de verificação"), "thisWillLogYouOutOfTheFollowingDevice": @@ -1448,7 +1447,7 @@ class MessageLookup extends MessageLookupByLibrary { "total": MessageLookupByLibrary.simpleMessage("total"), "totalSize": MessageLookupByLibrary.simpleMessage("Tamanho total"), "trash": MessageLookupByLibrary.simpleMessage("Lixeira"), - "trashDaysLeft": m64, + "trashDaysLeft": m63, "tryAgain": MessageLookupByLibrary.simpleMessage("Tente novamente"), "turnOnBackupForAutoUpload": MessageLookupByLibrary.simpleMessage( "Ative o backup para enviar automaticamente arquivos adicionados a esta pasta do dispositivo para o ente."), @@ -1501,7 +1500,7 @@ class MessageLookup extends MessageLookupByLibrary { "useSelectedPhoto": MessageLookupByLibrary.simpleMessage("Utilizar foto selecionada"), "usedSpace": MessageLookupByLibrary.simpleMessage("Espaço em uso"), - "validTill": m65, + "validTill": m64, "verificationFailedPleaseTryAgain": MessageLookupByLibrary.simpleMessage( "Falha na verificação, por favor, tente novamente"), @@ -1509,7 +1508,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("ID de Verificação"), "verify": MessageLookupByLibrary.simpleMessage("Verificar"), "verifyEmail": MessageLookupByLibrary.simpleMessage("Verificar email"), - "verifyEmailID": m66, + "verifyEmailID": m65, "verifyIDLabel": MessageLookupByLibrary.simpleMessage("Verificar"), "verifyPasskey": MessageLookupByLibrary.simpleMessage("Verificar chave de acesso"), @@ -1542,12 +1541,12 @@ class MessageLookup extends MessageLookupByLibrary { "weDontSupportEditingPhotosAndAlbumsThatYouDont": MessageLookupByLibrary.simpleMessage( "Não suportamos a edição de fotos e álbuns que você ainda não possui"), - "weHaveSendEmailTo": m67, + "weHaveSendEmailTo": m66, "weakStrength": MessageLookupByLibrary.simpleMessage("Fraca"), "welcomeBack": MessageLookupByLibrary.simpleMessage("Bem-vindo de volta!"), "yearly": MessageLookupByLibrary.simpleMessage("Anual"), - "yearsAgo": m68, + "yearsAgo": m67, "yes": MessageLookupByLibrary.simpleMessage("Sim"), "yesCancel": MessageLookupByLibrary.simpleMessage("Sim, cancelar"), "yesConvertToViewer": MessageLookupByLibrary.simpleMessage( @@ -1578,7 +1577,7 @@ class MessageLookup extends MessageLookupByLibrary { "Você não pode compartilhar consigo mesmo"), "youDontHaveAnyArchivedItems": MessageLookupByLibrary.simpleMessage( "Você não tem nenhum item arquivado."), - "youHaveSuccessfullyFreedUp": m69, + "youHaveSuccessfullyFreedUp": m68, "yourAccountHasBeenDeleted": MessageLookupByLibrary.simpleMessage("Sua conta foi excluída"), "yourMap": MessageLookupByLibrary.simpleMessage("Seu mapa"), diff --git a/mobile/lib/generated/intl/messages_zh.dart b/mobile/lib/generated/intl/messages_zh.dart index ca420142bb..c611a7352d 100644 --- a/mobile/lib/generated/intl/messages_zh.dart +++ b/mobile/lib/generated/intl/messages_zh.dart @@ -21,7 +21,7 @@ class MessageLookup extends MessageLookupByLibrary { String get localeName => 'zh'; static String m0(count) => - "${Intl.plural(count, zero: 'Add collaborator', one: 'Add collaborator', other: 'Add collaborators')}"; + "${Intl.plural(count, zero: '添加协作者', one: '添加协作者', other: '添加协作者')}"; static String m2(count) => "${Intl.plural(count, one: '添加一个项目', other: '添加一些项目')}"; @@ -30,7 +30,7 @@ class MessageLookup extends MessageLookupByLibrary { "您的 ${storageAmount} 插件有效期至 ${endDate}"; static String m1(count) => - "${Intl.plural(count, zero: 'Add viewer', one: 'Add viewer', other: 'Add viewers')}"; + "${Intl.plural(count, zero: '添加查看者', one: '添加查看者', other: '添加查看者')}"; static String m4(emailOrName) => "由 ${emailOrName} 添加"; @@ -122,79 +122,77 @@ class MessageLookup extends MessageLookupByLibrary { static String m37(providerName) => "如果您被收取费用,请用英语与 ${providerName} 的客服聊天"; - static String m38(reason) => "很抱歉,您的支付因 ${reason} 而失败"; + static String m38(endDate) => "免费试用有效期至 ${endDate}。\n之后您可以选择付费计划。"; - static String m39(endDate) => "免费试用有效期至 ${endDate}。\n之后您可以选择付费计划。"; + static String m39(toEmail) => "请给我们发送电子邮件至 ${toEmail}"; - static String m40(toEmail) => "请给我们发送电子邮件至 ${toEmail}"; + static String m40(toEmail) => "请将日志发送至 \n${toEmail}"; - static String m41(toEmail) => "请将日志发送至 \n${toEmail}"; + static String m41(storeName) => "在 ${storeName} 上给我们评分"; - static String m42(storeName) => "在 ${storeName} 上给我们评分"; + static String m42(storageInGB) => "3. 你和朋友都将免费获得 ${storageInGB} GB*"; - static String m43(storageInGB) => "3. 你和朋友都将免费获得 ${storageInGB} GB*"; - - static String m44(userEmail) => + static String m43(userEmail) => "${userEmail} 将从这个共享相册中删除\n\nTA们添加的任何照片也将从相册中删除"; - static String m45(endDate) => "在 ${endDate} 前续费"; + static String m44(endDate) => "在 ${endDate} 前续费"; - static String m46(count) => + static String m45(count) => "${Intl.plural(count, other: '已找到 ${count} 个结果')}"; - static String m47(count) => "已选择 ${count} 个"; + static String m46(count) => "已选择 ${count} 个"; - static String m48(count, yourCount) => "选择了 ${count} 个 (您的 ${yourCount} 个)"; + static String m47(count, yourCount) => "选择了 ${count} 个 (您的 ${yourCount} 个)"; - static String m49(verificationID) => "这是我的ente.io 的验证 ID: ${verificationID}。"; + static String m48(verificationID) => "这是我的ente.io 的验证 ID: ${verificationID}。"; - static String m50(verificationID) => + static String m49(verificationID) => "嘿,你能确认这是你的 ente.io 验证 ID吗:${verificationID}"; - static String m51(referralCode, referralStorageInGB) => + static String m50(referralCode, referralStorageInGB) => "ente推荐码: ${referralCode} \n\n注册付费计划后在设置 → 常规 → 推荐中应用它以免费获得 ${referralStorageInGB} GB空间\n\nhttps://ente.io"; - static String m52(numberOfPeople) => + static String m51(numberOfPeople) => "${Intl.plural(numberOfPeople, zero: '与特定人员共享', one: '与 1 人共享', other: '与 ${numberOfPeople} 人共享')}"; - static String m53(emailIDs) => "与 ${emailIDs} 共享"; + static String m52(emailIDs) => "与 ${emailIDs} 共享"; - static String m54(fileType) => "此 ${fileType} 将从您的设备中删除。"; + static String m53(fileType) => "此 ${fileType} 将从您的设备中删除。"; - static String m55(fileType) => "此 ${fileType} 同时在ente和您的设备中。"; + static String m54(fileType) => "此 ${fileType} 同时在ente和您的设备中。"; - static String m56(fileType) => "此 ${fileType} 将从ente中删除。"; + static String m55(fileType) => "此 ${fileType} 将从ente中删除。"; - static String m57(storageAmountInGB) => "${storageAmountInGB} GB"; + static String m56(storageAmountInGB) => "${storageAmountInGB} GB"; - static String m58( + static String m57( usedAmount, usedStorageUnit, totalAmount, totalStorageUnit) => "已使用 ${usedAmount} ${usedStorageUnit} / ${totalAmount} ${totalStorageUnit}"; - static String m59(id) => + static String m58(id) => "您的 ${id} 已经链接到另一个ente账户。\n如果您想要通过此账户使用您的 ${id} ,请联系我们的客服\'\'"; - static String m60(endDate) => "您的订阅将于 ${endDate} 取消"; + static String m59(endDate) => "您的订阅将于 ${endDate} 取消"; - static String m61(completed, total) => "已保存的回忆 ${completed}/共 ${total}"; + static String m60(completed, total) => "已保存的回忆 ${completed}/共 ${total}"; - static String m62(storageAmountInGB) => "他们也会获得 ${storageAmountInGB} GB"; + static String m61(storageAmountInGB) => "他们也会获得 ${storageAmountInGB} GB"; - static String m63(email) => "这是 ${email} 的验证ID"; + static String m62(email) => "这是 ${email} 的验证ID"; - static String m64(count) => + static String m63(count) => "${Intl.plural(count, zero: '', one: '1天', other: '${count} 天')}"; - static String m65(endDate) => "有效期至 ${endDate}"; + static String m64(endDate) => "有效期至 ${endDate}"; - static String m66(email) => "验证 ${email}"; + static String m65(email) => "验证 ${email}"; - static String m67(email) => "我们已经发送邮件到 ${email}"; + static String m66(email) => "我们已经发送邮件到 ${email}"; - static String m68(count) => + static String m67(count) => "${Intl.plural(count, one: '${count} 年前', other: '${count} 年前')}"; - static String m69(storageSaved) => "您已成功释放了 ${storageSaved}!"; + static String m68(storageSaved) => "您已成功释放了 ${storageSaved}!"; final messages = _notInlinedMessages(_notInlinedMessages); static Map _notInlinedMessages(_) => { @@ -632,6 +630,7 @@ class MessageLookup extends MessageLookupByLibrary { "我们不跟踪应用程序安装情况。如果您告诉我们您是在哪里找到我们的,将会有所帮助!"), "hearUsWhereTitle": MessageLookupByLibrary.simpleMessage("您是如何知道Ente的? (可选的)"), + "help": MessageLookupByLibrary.simpleMessage("帮助"), "hidden": MessageLookupByLibrary.simpleMessage("已隐藏"), "hide": MessageLookupByLibrary.simpleMessage("隐藏"), "hiding": MessageLookupByLibrary.simpleMessage("正在隐藏..."), @@ -739,8 +738,7 @@ class MessageLookup extends MessageLookupByLibrary { "logsDialogBody": MessageLookupByLibrary.simpleMessage( "这将跨日志发送以帮助我们调试您的问题。 请注意,将包含文件名以帮助跟踪特定文件的问题。"), "longPressAnEmailToVerifyEndToEndEncryption": - MessageLookupByLibrary.simpleMessage( - "Long press an email to verify end to end encryption."), + MessageLookupByLibrary.simpleMessage("长按电子邮件以验证端到端加密。"), "longpressOnAnItemToViewInFullscreen": MessageLookupByLibrary.simpleMessage("长按一个项目来全屏查看"), "lostDevice": MessageLookupByLibrary.simpleMessage("丢失了设备吗?"), @@ -837,8 +835,9 @@ class MessageLookup extends MessageLookupByLibrary { "我们不储存这个密码,所以如果忘记, 我们将无法解密您的数据"), "paymentDetails": MessageLookupByLibrary.simpleMessage("付款明细"), "paymentFailed": MessageLookupByLibrary.simpleMessage("支付失败"), + "paymentFailedMessage": MessageLookupByLibrary.simpleMessage( + "不幸的是,您的付款失败。请联系支持人员,我们将为您提供帮助!"), "paymentFailedTalkToProvider": m37, - "paymentFailedWithReason": m38, "pendingItems": MessageLookupByLibrary.simpleMessage("待处理项目"), "pendingSync": MessageLookupByLibrary.simpleMessage("正在等待同步"), "peopleUsingYourCode": MessageLookupByLibrary.simpleMessage("使用您的代码的人"), @@ -856,7 +855,7 @@ class MessageLookup extends MessageLookupByLibrary { "pickCenterPoint": MessageLookupByLibrary.simpleMessage("选择中心点"), "pinAlbum": MessageLookupByLibrary.simpleMessage("置顶相册"), "playOnTv": MessageLookupByLibrary.simpleMessage("在电视上播放相册"), - "playStoreFreeTrialValidTill": m39, + "playStoreFreeTrialValidTill": m38, "playstoreSubscription": MessageLookupByLibrary.simpleMessage("PlayStore 订阅"), "pleaseCheckYourInternetConnectionAndTryAgain": @@ -866,10 +865,10 @@ class MessageLookup extends MessageLookupByLibrary { "请用英语联系 support@ente.io ,我们将乐意提供帮助!"), "pleaseContactSupportIfTheProblemPersists": MessageLookupByLibrary.simpleMessage("如果问题仍然存在,请联系支持"), - "pleaseEmailUsAt": m40, + "pleaseEmailUsAt": m39, "pleaseGrantPermissions": MessageLookupByLibrary.simpleMessage("请授予权限"), "pleaseLoginAgain": MessageLookupByLibrary.simpleMessage("请重新登录"), - "pleaseSendTheLogsTo": m41, + "pleaseSendTheLogsTo": m40, "pleaseTryAgain": MessageLookupByLibrary.simpleMessage("请重试"), "pleaseVerifyTheCodeYouHaveEntered": MessageLookupByLibrary.simpleMessage("请验证您输入的代码"), @@ -895,7 +894,7 @@ class MessageLookup extends MessageLookupByLibrary { "raiseTicket": MessageLookupByLibrary.simpleMessage("提升工单"), "rateTheApp": MessageLookupByLibrary.simpleMessage("为此应用评分"), "rateUs": MessageLookupByLibrary.simpleMessage("给我们评分"), - "rateUsOnStore": m42, + "rateUsOnStore": m41, "recover": MessageLookupByLibrary.simpleMessage("恢复"), "recoverAccount": MessageLookupByLibrary.simpleMessage("恢复账户"), "recoverButton": MessageLookupByLibrary.simpleMessage("恢复"), @@ -920,7 +919,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("把我们推荐给你的朋友然后获得延长一倍的订阅计划"), "referralStep1": MessageLookupByLibrary.simpleMessage("1. 将此代码提供给您的朋友"), "referralStep2": MessageLookupByLibrary.simpleMessage("2. 他们注册一个付费计划"), - "referralStep3": m43, + "referralStep3": m42, "referrals": MessageLookupByLibrary.simpleMessage("推荐人"), "referralsAreCurrentlyPaused": MessageLookupByLibrary.simpleMessage("推荐已暂停"), @@ -939,7 +938,7 @@ class MessageLookup extends MessageLookupByLibrary { "removeFromFavorite": MessageLookupByLibrary.simpleMessage("从收藏中移除"), "removeLink": MessageLookupByLibrary.simpleMessage("移除链接"), "removeParticipant": MessageLookupByLibrary.simpleMessage("移除参与者"), - "removeParticipantBody": m44, + "removeParticipantBody": m43, "removePublicLink": MessageLookupByLibrary.simpleMessage("删除公开链接"), "removeShareItemsWarning": MessageLookupByLibrary.simpleMessage("您要删除的某些项目是由其他人添加的,您将无法访问它们"), @@ -950,7 +949,7 @@ class MessageLookup extends MessageLookupByLibrary { "renameAlbum": MessageLookupByLibrary.simpleMessage("重命名相册"), "renameFile": MessageLookupByLibrary.simpleMessage("重命名文件"), "renewSubscription": MessageLookupByLibrary.simpleMessage("续费订阅"), - "renewsOn": m45, + "renewsOn": m44, "reportABug": MessageLookupByLibrary.simpleMessage("报告错误"), "reportBug": MessageLookupByLibrary.simpleMessage("报告错误"), "resendEmail": MessageLookupByLibrary.simpleMessage("重新发送电子邮件"), @@ -997,7 +996,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("在照片的一定半径内拍摄的几组照片"), "searchPeopleEmptySection": MessageLookupByLibrary.simpleMessage("邀请他人,您将在此看到他们分享的所有照片"), - "searchResultCount": m46, + "searchResultCount": m45, "security": MessageLookupByLibrary.simpleMessage("安全"), "selectALocation": MessageLookupByLibrary.simpleMessage("选择一个位置"), "selectALocationFirst": @@ -1017,8 +1016,8 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("所选文件夹将被加密和备份"), "selectedItemsWillBeDeletedFromAllAlbumsAndMoved": MessageLookupByLibrary.simpleMessage("所选项目将从所有相册中删除并移动到回收站。"), - "selectedPhotos": m47, - "selectedPhotosWithYours": m48, + "selectedPhotos": m46, + "selectedPhotosWithYours": m47, "send": MessageLookupByLibrary.simpleMessage("发送"), "sendEmail": MessageLookupByLibrary.simpleMessage("发送电子邮件"), "sendInvite": MessageLookupByLibrary.simpleMessage("发送邀请"), @@ -1037,16 +1036,16 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("打开相册并点击右上角的分享按钮进行分享"), "shareAnAlbumNow": MessageLookupByLibrary.simpleMessage("立即分享相册"), "shareLink": MessageLookupByLibrary.simpleMessage("分享链接"), - "shareMyVerificationID": m49, + "shareMyVerificationID": m48, "shareOnlyWithThePeopleYouWant": MessageLookupByLibrary.simpleMessage("仅与您想要的人分享"), - "shareTextConfirmOthersVerificationID": m50, + "shareTextConfirmOthersVerificationID": m49, "shareTextRecommendUsingEnte": MessageLookupByLibrary.simpleMessage( "下载 ente,以便我们轻松分享原始质量的照片和视频\n\nhttps://ente.io"), - "shareTextReferralCode": m51, + "shareTextReferralCode": m50, "shareWithNonenteUsers": MessageLookupByLibrary.simpleMessage("与非ente 用户分享"), - "shareWithPeopleSectionTitle": m52, + "shareWithPeopleSectionTitle": m51, "shareYourFirstAlbum": MessageLookupByLibrary.simpleMessage("分享您的第一个相册"), "sharedAlbumSectionDescription": MessageLookupByLibrary.simpleMessage( @@ -1057,7 +1056,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("新共享的照片"), "sharedPhotoNotificationsExplanation": MessageLookupByLibrary.simpleMessage("当有人将照片添加到您所属的共享相册时收到通知"), - "sharedWith": m53, + "sharedWith": m52, "sharedWithMe": MessageLookupByLibrary.simpleMessage("与我共享"), "sharedWithYou": MessageLookupByLibrary.simpleMessage("已与您共享"), "sharing": MessageLookupByLibrary.simpleMessage("正在分享..."), @@ -1069,11 +1068,11 @@ class MessageLookup extends MessageLookupByLibrary { "signOutOtherDevices": MessageLookupByLibrary.simpleMessage("登出其他设备"), "signUpTerms": MessageLookupByLibrary.simpleMessage( "我同意 服务条款隐私政策"), - "singleFileDeleteFromDevice": m54, + "singleFileDeleteFromDevice": m53, "singleFileDeleteHighlight": MessageLookupByLibrary.simpleMessage("它将从所有相册中删除。"), - "singleFileInBothLocalAndRemote": m55, - "singleFileInRemoteOnly": m56, + "singleFileInBothLocalAndRemote": m54, + "singleFileInRemoteOnly": m55, "skip": MessageLookupByLibrary.simpleMessage("跳过"), "social": MessageLookupByLibrary.simpleMessage("社交"), "someItemsAreInBothEnteAndYourDevice": @@ -1104,12 +1103,12 @@ class MessageLookup extends MessageLookupByLibrary { "storage": MessageLookupByLibrary.simpleMessage("存储空间"), "storageBreakupFamily": MessageLookupByLibrary.simpleMessage("家庭"), "storageBreakupYou": MessageLookupByLibrary.simpleMessage("您"), - "storageInGB": m57, + "storageInGB": m56, "storageLimitExceeded": MessageLookupByLibrary.simpleMessage("已超出存储限制"), - "storageUsageInfo": m58, + "storageUsageInfo": m57, "strongStrength": MessageLookupByLibrary.simpleMessage("强"), - "subAlreadyLinkedErrMessage": m59, - "subWillBeCancelledOn": m60, + "subAlreadyLinkedErrMessage": m58, + "subWillBeCancelledOn": m59, "subscribe": MessageLookupByLibrary.simpleMessage("订阅"), "subscribeToEnableSharing": MessageLookupByLibrary.simpleMessage("您的订阅似乎已过期。请订阅以启用分享。"), @@ -1122,7 +1121,7 @@ class MessageLookup extends MessageLookupByLibrary { "successfullyUnhid": MessageLookupByLibrary.simpleMessage("已成功取消隐藏"), "suggestFeatures": MessageLookupByLibrary.simpleMessage("建议新功能"), "support": MessageLookupByLibrary.simpleMessage("支持"), - "syncProgress": m61, + "syncProgress": m60, "syncStopped": MessageLookupByLibrary.simpleMessage("同步已停止"), "syncing": MessageLookupByLibrary.simpleMessage("正在同步···"), "systemTheme": MessageLookupByLibrary.simpleMessage("适应系统"), @@ -1145,7 +1144,7 @@ class MessageLookup extends MessageLookupByLibrary { "theme": MessageLookupByLibrary.simpleMessage("主题"), "theseItemsWillBeDeletedFromYourDevice": MessageLookupByLibrary.simpleMessage("这些项目将从您的设备中删除。"), - "theyAlsoGetXGb": m62, + "theyAlsoGetXGb": m61, "theyWillBeDeletedFromAllAlbums": MessageLookupByLibrary.simpleMessage("他们将从所有相册中删除。"), "thisActionCannotBeUndone": @@ -1159,7 +1158,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("这个邮箱地址已经被使用"), "thisImageHasNoExifData": MessageLookupByLibrary.simpleMessage("此图像没有Exif 数据"), - "thisIsPersonVerificationId": m63, + "thisIsPersonVerificationId": m62, "thisIsYourVerificationId": MessageLookupByLibrary.simpleMessage("这是您的验证 ID"), "thisWillLogYouOutOfTheFollowingDevice": @@ -1173,7 +1172,7 @@ class MessageLookup extends MessageLookupByLibrary { "total": MessageLookupByLibrary.simpleMessage("总计"), "totalSize": MessageLookupByLibrary.simpleMessage("总大小"), "trash": MessageLookupByLibrary.simpleMessage("回收站"), - "trashDaysLeft": m64, + "trashDaysLeft": m63, "tryAgain": MessageLookupByLibrary.simpleMessage("请再试一次"), "turnOnBackupForAutoUpload": MessageLookupByLibrary.simpleMessage("打开备份以自动上传添加到此设备文件夹的文件。"), @@ -1216,13 +1215,13 @@ class MessageLookup extends MessageLookupByLibrary { "useRecoveryKey": MessageLookupByLibrary.simpleMessage("使用恢复密钥"), "useSelectedPhoto": MessageLookupByLibrary.simpleMessage("使用所选照片"), "usedSpace": MessageLookupByLibrary.simpleMessage("已用空间"), - "validTill": m65, + "validTill": m64, "verificationFailedPleaseTryAgain": MessageLookupByLibrary.simpleMessage("验证失败,请重试"), "verificationId": MessageLookupByLibrary.simpleMessage("验证 ID"), "verify": MessageLookupByLibrary.simpleMessage("验证"), "verifyEmail": MessageLookupByLibrary.simpleMessage("验证电子邮件"), - "verifyEmailID": m66, + "verifyEmailID": m65, "verifyIDLabel": MessageLookupByLibrary.simpleMessage("验证"), "verifyPasskey": MessageLookupByLibrary.simpleMessage("验证通行密钥"), "verifyPassword": MessageLookupByLibrary.simpleMessage("验证密码"), @@ -1246,11 +1245,11 @@ class MessageLookup extends MessageLookupByLibrary { "weAreOpenSource": MessageLookupByLibrary.simpleMessage("我们是开源的 !"), "weDontSupportEditingPhotosAndAlbumsThatYouDont": MessageLookupByLibrary.simpleMessage("我们不支持编辑您尚未拥有的照片和相册"), - "weHaveSendEmailTo": m67, + "weHaveSendEmailTo": m66, "weakStrength": MessageLookupByLibrary.simpleMessage("弱"), "welcomeBack": MessageLookupByLibrary.simpleMessage("欢迎回来!"), "yearly": MessageLookupByLibrary.simpleMessage("每年"), - "yearsAgo": m68, + "yearsAgo": m67, "yes": MessageLookupByLibrary.simpleMessage("是"), "yesCancel": MessageLookupByLibrary.simpleMessage("是的,取消"), "yesConvertToViewer": MessageLookupByLibrary.simpleMessage("是的,转换为查看者"), @@ -1276,7 +1275,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("莫开玩笑,您不能与自己分享"), "youDontHaveAnyArchivedItems": MessageLookupByLibrary.simpleMessage("您没有任何存档的项目。"), - "youHaveSuccessfullyFreedUp": m69, + "youHaveSuccessfullyFreedUp": m68, "yourAccountHasBeenDeleted": MessageLookupByLibrary.simpleMessage("您的账户已删除"), "yourMap": MessageLookupByLibrary.simpleMessage("您的地图"), diff --git a/mobile/lib/generated/l10n.dart b/mobile/lib/generated/l10n.dart index 78e376d486..8d8853ae24 100644 --- a/mobile/lib/generated/l10n.dart +++ b/mobile/lib/generated/l10n.dart @@ -2232,6 +2232,16 @@ class S { ); } + /// `Help` + String get help { + return Intl.message( + 'Help', + name: 'help', + desc: '', + args: [], + ); + } + /// `Oops, something went wrong` String get oopsSomethingWentWrong { return Intl.message( @@ -4465,13 +4475,13 @@ class S { ); } - /// `Unfortunately your payment failed due to {reason}` - String paymentFailedWithReason(Object reason) { + /// `Unfortunately your payment failed. Please contact support and we'll help you out!` + String get paymentFailedMessage { return Intl.message( - 'Unfortunately your payment failed due to $reason', - name: 'paymentFailedWithReason', + 'Unfortunately your payment failed. Please contact support and we\'ll help you out!', + name: 'paymentFailedMessage', desc: '', - args: [reason], + args: [], ); } diff --git a/mobile/lib/l10n/intl_en.arb b/mobile/lib/l10n/intl_en.arb index eff0f89408..eee9524878 100644 --- a/mobile/lib/l10n/intl_en.arb +++ b/mobile/lib/l10n/intl_en.arb @@ -304,6 +304,7 @@ } }, "faq": "FAQ", + "help": "Help", "oopsSomethingWentWrong": "Oops, something went wrong", "peopleUsingYourCode": "People using your code", "eligible": "eligible", @@ -640,7 +641,7 @@ "thankYou": "Thank you", "failedToVerifyPaymentStatus": "Failed to verify payment status", "pleaseWaitForSometimeBeforeRetrying": "Please wait for sometime before retrying", - "paymentFailedWithReason": "Unfortunately your payment failed due to {reason}", + "paymentFailedMessage": "Unfortunately your payment failed. Please contact support and we'll help you out!", "youAreOnAFamilyPlan": "You are on a family plan!", "contactFamilyAdmin": "Please contact {familyAdminEmail} to manage your subscription", "leaveFamily": "Leave family", diff --git a/mobile/lib/l10n/intl_pt.arb b/mobile/lib/l10n/intl_pt.arb index 9c47deb671..66c895e4c1 100644 --- a/mobile/lib/l10n/intl_pt.arb +++ b/mobile/lib/l10n/intl_pt.arb @@ -304,6 +304,7 @@ } }, "faq": "Perguntas frequentes", + "help": "Ajuda", "oopsSomethingWentWrong": "Ops! Algo deu errado", "peopleUsingYourCode": "Pessoas que usam seu código", "eligible": "elegível", @@ -338,11 +339,11 @@ "disableLinkMessage": "Isso removerá o link público para acessar \"{albumName}\".", "sharing": "Compartilhando...", "youCannotShareWithYourself": "Você não pode compartilhar consigo mesmo", - "archive": "Arquivo", + "archive": "Arquivado", "createAlbumActionHint": "Pressione e segure para selecionar fotos e clique em + para criar um álbum", "importing": "Importando....", "failedToLoadAlbums": "Falha ao carregar álbuns", - "hidden": "Escondido", + "hidden": "Oculto", "authToViewYourHiddenFiles": "Autentique-se para visualizar seus arquivos ocultos", "trash": "Lixeira", "uncategorized": "Sem categoria", @@ -545,7 +546,7 @@ "yourStorageDetailsCouldNotBeFetched": "Seus detalhes de armazenamento não puderam ser obtidos", "reportABug": "Reportar um problema", "reportBug": "Reportar um problema", - "suggestFeatures": "Sugerir funcionalidades", + "suggestFeatures": "Sugerir recurso", "support": "Suporte", "theme": "Tema", "lightTheme": "Claro", @@ -640,7 +641,7 @@ "thankYou": "Obrigado", "failedToVerifyPaymentStatus": "Falha ao verificar status do pagamento", "pleaseWaitForSometimeBeforeRetrying": "Por favor, aguarde algum tempo antes de tentar novamente", - "paymentFailedWithReason": "Infelizmente o seu pagamento falhou devido a {reason}", + "paymentFailedMessage": "Infelizmente o seu pagamento falhou. Entre em contato com o suporte e nós ajudaremos você!", "youAreOnAFamilyPlan": "Você está em um plano familiar!", "contactFamilyAdmin": "Entre em contato com {familyAdminEmail} para gerenciar sua assinatura", "leaveFamily": "Sair da família", @@ -839,7 +840,7 @@ "pressAndHoldToPlayVideo": "Pressione e segure para reproduzir o vídeo", "pressAndHoldToPlayVideoDetailed": "Pressione e segure na imagem para reproduzir o vídeo", "downloadFailed": "Falha ao baixar", - "deduplicateFiles": "Arquivos Deduplicados", + "deduplicateFiles": "Arquivos duplicados", "deselectAll": "Desmarcar todos", "reviewDeduplicateItems": "Por favor, reveja e exclua os itens que você acredita serem duplicados.", "clubByCaptureTime": "Agrupar por tempo de captura", @@ -1200,7 +1201,7 @@ "joinDiscord": "Junte-se ao Discord", "locations": "Locais", "descriptions": "Descrições", - "addViewers": "{count, plural, zero {Add viewer} one {Add viewer} other {Add viewers}}", - "addCollaborators": "{count, plural, zero {Add collaborator} one {Add collaborator} other {Add collaborators}}", - "longPressAnEmailToVerifyEndToEndEncryption": "Long press an email to verify end to end encryption." + "addViewers": "{count, plural, zero {Adicionar visualizador} one {Adicionar visualizador} other {Adicionar Visualizadores}}", + "addCollaborators": "{count, plural, zero {Adicionar colaborador} one {Adicionar coloborador} other {Adicionar colaboradores}}", + "longPressAnEmailToVerifyEndToEndEncryption": "Pressione e segure um e-mail para verificar a criptografia de ponta a ponta." } \ No newline at end of file diff --git a/mobile/lib/l10n/intl_zh.arb b/mobile/lib/l10n/intl_zh.arb index dd9b9f160e..982ff40f78 100644 --- a/mobile/lib/l10n/intl_zh.arb +++ b/mobile/lib/l10n/intl_zh.arb @@ -304,6 +304,7 @@ } }, "faq": "常见问题", + "help": "帮助", "oopsSomethingWentWrong": "哎呀,似乎出了点问题", "peopleUsingYourCode": "使用您的代码的人", "eligible": "符合资格", @@ -640,7 +641,7 @@ "thankYou": "非常感谢您", "failedToVerifyPaymentStatus": "验证支付状态失败", "pleaseWaitForSometimeBeforeRetrying": "请稍等片刻后再重试", - "paymentFailedWithReason": "很抱歉,您的支付因 {reason} 而失败", + "paymentFailedMessage": "不幸的是,您的付款失败。请联系支持人员,我们将为您提供帮助!", "youAreOnAFamilyPlan": "你在一个家庭计划中!", "contactFamilyAdmin": "请联系 {familyAdminEmail} 来管理您的订阅", "leaveFamily": "离开家庭计划", @@ -1200,7 +1201,7 @@ "joinDiscord": "加入 Discord", "locations": "位置", "descriptions": "描述", - "addViewers": "{count, plural, zero {Add viewer} one {Add viewer} other {Add viewers}}", - "addCollaborators": "{count, plural, zero {Add collaborator} one {Add collaborator} other {Add collaborators}}", - "longPressAnEmailToVerifyEndToEndEncryption": "Long press an email to verify end to end encryption." + "addViewers": "{count, plural, zero {添加查看者} one {添加查看者} other {添加查看者}}", + "addCollaborators": "{count, plural, zero {添加协作者} one {添加协作者} other {添加协作者}}", + "longPressAnEmailToVerifyEndToEndEncryption": "长按电子邮件以验证端到端加密。" } \ No newline at end of file diff --git a/mobile/lib/models/subscription.dart b/mobile/lib/models/subscription.dart index 4779424211..51fca19e3a 100644 --- a/mobile/lib/models/subscription.dart +++ b/mobile/lib/models/subscription.dart @@ -30,6 +30,19 @@ class Subscription { return expiryTime > DateTime.now().microsecondsSinceEpoch; } + bool isCancelled() { + return attributes?.isCancelled ?? false; + } + + bool isPastDue() { + return !isCancelled() && + expiryTime < DateTime.now().microsecondsSinceEpoch && + expiryTime >= + DateTime.now() + .subtract(const Duration(days: 30)) + .microsecondsSinceEpoch; + } + bool isYearlyPlan() { return 'year' == period; } diff --git a/mobile/lib/services/home_widget_service.dart b/mobile/lib/services/home_widget_service.dart index c8082b448a..33ef5d2bbc 100644 --- a/mobile/lib/services/home_widget_service.dart +++ b/mobile/lib/services/home_widget_service.dart @@ -145,13 +145,7 @@ class HomeWidgetService { } Future countHomeWidgets() async { - return await hw.HomeWidget.getWidgetCount( - name: 'SlideshowWidgetProvider', - androidName: 'SlideshowWidgetProvider', - qualifiedAndroidName: 'io.ente.photos.SlideshowWidgetProvider', - iOSName: 'SlideshowWidget', - ) ?? - 0; + return (await hw.HomeWidget.getInstalledWidgets()).length; } Future clearHomeWidget() async { diff --git a/mobile/lib/services/ignored_files_service.dart b/mobile/lib/services/ignored_files_service.dart index 759acd157d..d4a4af9f9b 100644 --- a/mobile/lib/services/ignored_files_service.dart +++ b/mobile/lib/services/ignored_files_service.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:io'; import 'package:logging/logging.dart'; +import "package:photos/core/errors.dart"; import 'package:photos/db/ignored_files_db.dart'; import 'package:photos/models/file/file.dart'; import 'package:photos/models/ignored_file.dart'; @@ -47,7 +48,10 @@ class IgnoredFilesService { return false; } - String? getUploadSkipReason(Map idToReasonMap, EnteFile file) { + String? getUploadSkipReason( + Map idToReasonMap, + EnteFile file, + ) { final id = _getIgnoreID(file.localID, file.deviceFolder, file.title); if (id != null && id.isNotEmpty) { return idToReasonMap[id]; @@ -100,6 +104,12 @@ class IgnoredFilesService { for (IgnoredFile iFile in dbResult) { final id = _idForIgnoredFile(iFile); if (id != null) { + if (Platform.isIOS && + iFile.reason == InvalidReason.sourceFileMissing.name) { + // ignoreSourceFileMissing error on iOS as the file fetch from iCloud might have failed, + // but the file might be available later + continue; + } result[id] = iFile.reason; } } diff --git a/mobile/lib/services/local_file_update_service.dart b/mobile/lib/services/local_file_update_service.dart index 591cade675..e00ac6c459 100644 --- a/mobile/lib/services/local_file_update_service.dart +++ b/mobile/lib/services/local_file_update_service.dart @@ -3,6 +3,7 @@ import 'dart:core'; import 'dart:io'; import 'package:logging/logging.dart'; +import "package:photo_manager/photo_manager.dart"; import "package:photos/core/configuration.dart"; import 'package:photos/core/errors.dart'; import 'package:photos/db/file_updation_db.dart'; @@ -25,6 +26,10 @@ class LocalFileUpdateService { late Logger _logger; final String _iosLivePhotoSizeMigrationDone = 'fm_ios_live_photo_check'; final String _doneLivePhotoImport = 'fm_import_ios_live_photo_check'; + final String _androidMissingGPSImportDone = + 'fm_android_missing_gps_import_done'; + final String _androidMissingGPSCheckDone = + 'fm_android_missing_gps_check_done'; static int twoHundredKb = 200 * 1024; final List _oldMigrationKeys = [ 'fm_badCreationTime', @@ -63,6 +68,9 @@ class LocalFileUpdateService { if (!Platform.isAndroid) { await _handleLivePhotosSizedCheck(); } + if (Platform.isAndroid) { + await _androidMissingGPSCheck(); + } } catch (e, s) { _logger.severe('failed to perform migration', e, s); } finally { @@ -385,6 +393,131 @@ class LocalFileUpdateService { await _prefs.setBool(_doneLivePhotoImport, true); } + //#region Android Missing GPS specific methods ### + + Future _androidMissingGPSCheck() async { + if (_prefs.containsKey(_androidMissingGPSCheckDone)) { + return; + } + await _importAndroidBadGPSCandidate(); + // singleRunLimit indicates number of files to check during single + // invocation of this method. The limit act as a crude way to limit the + // resource consumed by the method + const int singleRunLimit = 500; + final localIDsToProcess = + await _fileUpdationDB.getLocalIDsForPotentialReUpload( + singleRunLimit, + FileUpdationDB.androidMissingGPS, + ); + if (localIDsToProcess.isNotEmpty) { + final chunksOf50 = localIDsToProcess.chunks(50); + for (final chunk in chunksOf50) { + final sTime = DateTime.now().microsecondsSinceEpoch; + final List futures = []; + final chunkOf10 = chunk.chunks(10); + for (final smallChunk in chunkOf10) { + futures.add(_checkForMissingGPS(smallChunk)); + } + await Future.wait(futures); + final eTime = DateTime.now().microsecondsSinceEpoch; + final d = Duration(microseconds: eTime - sTime); + _logger.info( + 'Performed missing GPS Location check for ${chunk.length} files ' + 'completed in ${d.inSeconds.toString()} secs', + ); + } + } else { + _logger.info('Completed android missing GPS check'); + await _prefs.setBool(_androidMissingGPSCheckDone, true); + } + } + + Future _checkForMissingGPS(List localIDs) async { + try { + final List localFiles = + await FilesDB.instance.getLocalFiles(localIDs); + final ownerID = Configuration.instance.getUserID()!; + final Set localIDsWithFile = {}; + final Set reuploadCandidate = {}; + final Set processedIDs = {}; + for (EnteFile file in localFiles) { + if (file.localID == null) continue; + // ignore files that are not uploaded or have different owner + if (!file.isUploaded || file.ownerID! != ownerID) { + processedIDs.add(file.localID!); + } + if (file.hasLocation) { + processedIDs.add(file.localID!); + } + } + for (EnteFile enteFile in localFiles) { + try { + if (enteFile.localID == null || + processedIDs.contains(enteFile.localID!)) { + continue; + } + + final localID = enteFile.localID!; + localIDsWithFile.add(localID); + final AssetEntity? entity = await AssetEntity.fromId(localID); + if (entity == null) { + processedIDs.add(localID); + } else { + final latLng = await entity.latlngAsync(); + if ((latLng.longitude ?? 0) == 0 || (latLng.latitude ?? 0) == 0) { + processedIDs.add(localID); + } else { + reuploadCandidate.add(localID); + processedIDs.add(localID); + } + } + } catch (e, s) { + processedIDs.add(enteFile.localID!); + _logger.severe('lat/long check file ${enteFile.toString()}', e, s); + } + } + for (String id in localIDs) { + // if the file with given localID doesn't exist, consider it as done. + if (!localIDsWithFile.contains(id)) { + processedIDs.add(id); + } + } + await FileUpdationDB.instance.insertMultiple( + reuploadCandidate.toList(), + FileUpdationDB.modificationTimeUpdated, + ); + await FileUpdationDB.instance.deleteByLocalIDs( + processedIDs.toList(), + FileUpdationDB.androidMissingGPS, + ); + } catch (e, s) { + _logger.severe('error while checking missing GPS', e, s); + } + } + + Future _importAndroidBadGPSCandidate() async { + if (_prefs.containsKey(_androidMissingGPSImportDone)) { + return; + } + final sTime = DateTime.now().microsecondsSinceEpoch; + _logger.info('importing files without missing GPS'); + final int ownerID = Configuration.instance.getUserID()!; + final fileLocalIDs = + await FilesDB.instance.getLocalFilesBackedUpWithoutLocation(ownerID); + await _fileUpdationDB.insertMultiple( + fileLocalIDs, + FileUpdationDB.androidMissingGPS, + ); + final eTime = DateTime.now().microsecondsSinceEpoch; + final d = Duration(microseconds: eTime - sTime); + _logger.info( + 'importing completed, total files count ${fileLocalIDs.length} and took ${d.inSeconds.toString()} seconds', + ); + await _prefs.setBool(_androidMissingGPSImportDone, true); + } + + //#endregion Android Missing GPS specific methods ### + Future getUploadData(EnteFile file) async { final mediaUploadData = await getUploadDataFromEnteFile(file); // delete the file from app's internal cache if it was copied to app diff --git a/mobile/lib/services/local_sync_service.dart b/mobile/lib/services/local_sync_service.dart index b480661cbf..93b3c94373 100644 --- a/mobile/lib/services/local_sync_service.dart +++ b/mobile/lib/services/local_sync_service.dart @@ -20,6 +20,7 @@ import 'package:photos/services/app_lifecycle_service.dart'; import "package:photos/services/ignored_files_service.dart"; import 'package:photos/services/local/local_sync_util.dart'; import "package:photos/utils/debouncer.dart"; +import "package:photos/utils/photo_manager_util.dart"; import 'package:shared_preferences/shared_preferences.dart'; import 'package:sqflite/sqflite.dart'; import 'package:tuple/tuple.dart'; @@ -61,7 +62,7 @@ class LocalSyncService { return; } if (Platform.isAndroid && AppLifecycleService.instance.isForeground) { - final permissionState = await PhotoManager.requestPermissionExtend(); + final permissionState = await requestPhotoMangerPermissions(); if (permissionState != PermissionState.authorized) { _logger.severe( "sync requested with invalid permission", @@ -213,6 +214,11 @@ class LocalSyncService { _logger.warning('Invalid file received for ignoring: $file'); return; } + if (Platform.isIOS && error.reason == InvalidReason.sourceFileMissing) { + // ignoreSourceFileMissing error on iOS as the file fetch from iCloud might have failed, + // but the file might be available later + return; + } final ignored = IgnoredFile( file.localID, file.title, diff --git a/mobile/lib/services/machine_learning/semantic_search/frameworks/onnx/onnx_image_encoder.dart b/mobile/lib/services/machine_learning/semantic_search/frameworks/onnx/onnx_image_encoder.dart index 5b91eaaee2..5464507700 100644 --- a/mobile/lib/services/machine_learning/semantic_search/frameworks/onnx/onnx_image_encoder.dart +++ b/mobile/lib/services/machine_learning/semantic_search/frameworks/onnx/onnx_image_encoder.dart @@ -15,9 +15,10 @@ class OnnxImageEncoder { ..setIntraOpNumThreads(1) ..setSessionGraphOptimizationLevel(GraphOptimizationLevel.ortEnableAll); try { - final bytes = await File(args["imageModelPath"]).readAsBytes(); - final session = OrtSession.fromBuffer(bytes, sessionOptions); + final session = + OrtSession.fromFile(File(args["imageModelPath"]), sessionOptions); _logger.info('image model loaded'); + return session.address; } catch (e, s) { _logger.severe(e, s); diff --git a/mobile/lib/services/update_service.dart b/mobile/lib/services/update_service.dart index 759adaf428..156f0b6a8a 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 = 15; + static const currentChangeLogVersion = 16; LatestVersionInfo? _latestVersion; final _logger = Logger("UpdateService"); diff --git a/mobile/lib/ui/components/home_header_widget.dart b/mobile/lib/ui/components/home_header_widget.dart index 6fb8f0f6d3..7f2519a190 100644 --- a/mobile/lib/ui/components/home_header_widget.dart +++ b/mobile/lib/ui/components/home_header_widget.dart @@ -10,6 +10,7 @@ import 'package:photos/ui/components/buttons/icon_button_widget.dart'; import "package:photos/ui/settings/backup/backup_folder_selection_page.dart"; import "package:photos/utils/dialog_util.dart"; import "package:photos/utils/navigation_util.dart"; +import "package:photos/utils/photo_manager_util.dart"; class HomeHeaderWidget extends StatefulWidget { final Widget centerWidget; @@ -48,7 +49,7 @@ class _HomeHeaderWidgetState extends State { onTap: () async { try { final PermissionState state = - await PhotoManager.requestPermissionExtend(); + await requestPhotoMangerPermissions(); await LocalSyncService.instance.onUpdatePermission(state); } on Exception catch (e) { Logger("HomeHeaderWidget").severe( diff --git a/mobile/lib/ui/growth/referral_screen.dart b/mobile/lib/ui/growth/referral_screen.dart index 32843c192f..570114600d 100644 --- a/mobile/lib/ui/growth/referral_screen.dart +++ b/mobile/lib/ui/growth/referral_screen.dart @@ -255,7 +255,7 @@ class ReferralWidget extends StatelessWidget { context, WebPage( S.of(context).faq, - "https://ente.io/faq/general/referral-program", + "https://help.ente.io/photos/features/referral-program/", ), ); }, diff --git a/mobile/lib/ui/home/grant_permissions_widget.dart b/mobile/lib/ui/home/grant_permissions_widget.dart index 29c8e5e1b3..ab6aff07ba 100644 --- a/mobile/lib/ui/home/grant_permissions_widget.dart +++ b/mobile/lib/ui/home/grant_permissions_widget.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:photo_manager/photo_manager.dart'; import "package:photos/generated/l10n.dart"; import 'package:photos/services/sync_service.dart'; +import "package:photos/utils/photo_manager_util.dart"; import "package:styled_text/styled_text.dart"; class GrantPermissionsWidget extends StatelessWidget { @@ -91,7 +92,7 @@ class GrantPermissionsWidget extends StatelessWidget { key: const ValueKey("grantPermissionButton"), child: Text(S.of(context).grantPermission), onPressed: () async { - final state = await PhotoManager.requestPermissionExtend(); + final state = await requestPhotoMangerPermissions(); if (state == PermissionState.authorized || state == PermissionState.limited) { await SyncService.instance.onPermissionGranted(state); diff --git a/mobile/lib/ui/notification/update/change_log_page.dart b/mobile/lib/ui/notification/update/change_log_page.dart index 6ea2510f26..0160bf5504 100644 --- a/mobile/lib/ui/notification/update/change_log_page.dart +++ b/mobile/lib/ui/notification/update/change_log_page.dart @@ -1,5 +1,4 @@ import "dart:async"; -import "dart:io"; import 'package:flutter/material.dart'; import "package:photos/generated/l10n.dart"; @@ -85,22 +84,14 @@ class _ChangeLogPageState extends State { ButtonWidget( buttonType: ButtonType.trailingIconSecondary, buttonSize: ButtonSize.large, - // labelText: S.of(context).joinDiscord, - labelText: "Why we open sourced", - // icon: Icons.discord_outlined, - icon: Icons.rocket_rounded, + labelText: S.of(context).joinDiscord, + icon: Icons.discord_outlined, iconColor: enteColorScheme.primary500, onTap: () async { - // unawaited( - // launchUrlString( - // "https://discord.com/invite/z2YVKkycX3", - // mode: LaunchMode.externalApplication, - // ), - // ); unawaited( launchUrlString( - "https://ente.io/blog/open-sourcing-our-server/", - mode: LaunchMode.inAppBrowserView, + "https://discord.com/invite/z2YVKkycX3", + mode: LaunchMode.externalApplication, ), ); }, @@ -129,30 +120,14 @@ class _ChangeLogPageState extends State { Widget _getChangeLog() { final scrollController = ScrollController(); final List items = []; - if (Platform.isAndroid) { - items.add( - ChangeLogEntry( - "Home Widget ✨", - 'Introducing our new Android widget! Enjoy your favourite memories directly on your home screen.', - ), - ); - } items.addAll([ ChangeLogEntry( - "Redesigned Discovery Tab", - 'We\'ve given it a fresh new look for improved design and better visual separation between each section.', + "Share an Album to Multiple Contacts at Once", + 'Adding multiple viewers and collaborators just got easier!\n' + '\nYou can now select multiple contacts and add all of them at once.', ), ChangeLogEntry( - "Location Clustering ", - 'Now, see photos automatically organize into clusters around a radius of populated cities.', - ), - ChangeLogEntry( - "Ente is now fully Open Source!", - 'We took the final step in our open source journey.\n\n' - 'Our clients had always been open source. Now, we have released the source code for our servers.', - ), - ChangeLogEntry( - "Bug Fixes", + "Bug Fixes and Performance Improvements", '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! 🙏', ), diff --git a/mobile/lib/ui/payment/payment_web_page.dart b/mobile/lib/ui/payment/payment_web_page.dart index 39f4ea9d01..cbe55f671e 100644 --- a/mobile/lib/ui/payment/payment_web_page.dart +++ b/mobile/lib/ui/payment/payment_web_page.dart @@ -5,14 +5,15 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_inappwebview/flutter_inappwebview.dart'; import 'package:logging/logging.dart'; +import "package:photos/core/constants.dart"; import 'package:photos/ente_theme_data.dart'; import "package:photos/generated/l10n.dart"; import 'package:photos/models/subscription.dart'; import 'package:photos/services/billing_service.dart'; import 'package:photos/services/user_service.dart'; import 'package:photos/ui/common/loading_widget.dart'; -import 'package:photos/ui/common/progress_dialog.dart'; import 'package:photos/utils/dialog_util.dart'; +import "package:photos/utils/email_util.dart"; class PaymentWebPage extends StatefulWidget { final String? planId; @@ -30,7 +31,6 @@ class _PaymentWebPageState extends State { final UserService userService = UserService.instance; final BillingService billingService = BillingService.instance; final String basePaymentUrl = kWebPaymentBaseEndpoint; - late ProgressDialog _dialog; InAppWebViewController? webView; double progress = 0; Uri? initPaymentUrl; @@ -49,11 +49,6 @@ class _PaymentWebPageState extends State { @override Widget build(BuildContext context) { - _dialog = createProgressDialog( - context, - S.of(context).pleaseWait, - isDismissible: true, - ); if (initPaymentUrl == null) { return const EnteLoadingWidget(); } @@ -93,27 +88,20 @@ class _PaymentWebPageState extends State { return NavigationActionPolicy.ALLOW; }, onConsoleMessage: (controller, consoleMessage) { - _logger.info(consoleMessage); + _logger.info("onConsoleMessage $consoleMessage"); }, onLoadStart: (controller, navigationAction) async { - if (!_dialog.isShowing()) { - await _dialog.show(); - } + _logger.info("onLoadStart $navigationAction"); }, onLoadError: (controller, navigationAction, code, msg) async { - if (_dialog.isShowing()) { - await _dialog.hide(); - } + _logger.severe("onLoadError $navigationAction $code $msg"); }, onLoadHttpError: (controller, navigationAction, code, msg) async { _logger.info("onHttpError with $code and msg = $msg"); }, onLoadStop: (controller, navigationAction) async { - _logger.info("loadStart" + navigationAction.toString()); - if (_dialog.isShowing()) { - await _dialog.hide(); - } + _logger.info("onLoadStop $navigationAction"); }, ), ), @@ -125,7 +113,6 @@ class _PaymentWebPageState extends State { @override void dispose() { - _dialog.hide(); super.dispose(); } @@ -207,12 +194,17 @@ class _PaymentWebPageState extends State { barrierDismissible: false, builder: (context) => AlertDialog( title: Text(S.of(context).paymentFailed), - content: Text(S.of(context).paymentFailedWithReason(reason)), + content: Text(S.of(context).paymentFailedMessage), actions: [ TextButton( - child: Text(S.of(context).ok), - onPressed: () { + child: Text(S.of(context).contactSupport), + onPressed: () async { Navigator.of(context).pop('dialog'); + await sendEmail( + context, + to: supportEmail, + subject: "Billing issue", + ); }, ), ], @@ -224,7 +216,6 @@ class _PaymentWebPageState extends State { // return true if verifySubscription didn't throw any exceptions Future _handlePaymentSuccess(Map queryParams) async { final checkoutSessionID = queryParams['session_id'] ?? ''; - await _dialog.show(); try { // ignore: unused_local_variable final response = await billingService.verifySubscription( @@ -232,7 +223,6 @@ class _PaymentWebPageState extends State { checkoutSessionID, paymentProvider: stripe, ); - await _dialog.hide(); final content = widget.actionType == 'buy' ? S.of(context).yourPurchaseWasSuccessful : S.of(context).yourSubscriptionWasUpdatedSuccessfully; @@ -242,7 +232,6 @@ class _PaymentWebPageState extends State { ); } catch (error) { _logger.severe(error); - await _dialog.hide(); await _showExitPageDialog( title: S.of(context).failedToVerifyPaymentStatus, content: S.of(context).pleaseWaitForSometimeBeforeRetrying, diff --git a/mobile/lib/ui/payment/stripe_subscription_page.dart b/mobile/lib/ui/payment/stripe_subscription_page.dart index a36ad11512..8306c3c181 100644 --- a/mobile/lib/ui/payment/stripe_subscription_page.dart +++ b/mobile/lib/ui/payment/stripe_subscription_page.dart @@ -81,6 +81,11 @@ class _StripeSubscriptionPageState extends State { userDetails.hasPaidAddon(); _hasActiveSubscription = _currentSubscription!.isValid(); _isStripeSubscriber = _currentSubscription!.paymentProvider == stripe; + + if (_isStripeSubscriber && _currentSubscription!.isPastDue()) { + _redirectToPaymentPortal(); + } + return _filterStripeForUI().then((value) { _hasLoadedData = true; setState(() {}); @@ -254,7 +259,7 @@ class _StripeSubscriptionPageState extends State { singleBorderRadius: 4, alignCaptionedTextToLeft: true, onTap: () async { - _onStripSupportedPaymentDetailsTap(); + _redirectToPaymentPortal(); }, ), ), @@ -295,9 +300,9 @@ class _StripeSubscriptionPageState extends State { ); } - // _onStripSupportedPaymentDetailsTap action allows the user to update + // _redirectToPaymentPortal action allows the user to update // their stripe payment details - void _onStripSupportedPaymentDetailsTap() async { + void _redirectToPaymentPortal() async { final String paymentProvider = _currentSubscription!.paymentProvider; switch (_currentSubscription!.paymentProvider) { case stripe: diff --git a/mobile/lib/ui/settings/account_section_widget.dart b/mobile/lib/ui/settings/account_section_widget.dart index 8b9c7bbef3..ec4cac4f91 100644 --- a/mobile/lib/ui/settings/account_section_widget.dart +++ b/mobile/lib/ui/settings/account_section_widget.dart @@ -150,7 +150,7 @@ class AccountSectionWidget extends StatelessWidget { trailingIconIsMuted: true, onTap: () async { // ignore: unawaited_futures - launchUrlString("https://ente.io/faq/migration/out-of-ente/"); + launchUrlString("https://help.ente.io/photos/migration/export/"); }, ), sectionOptionSpacing, diff --git a/mobile/lib/ui/settings/support_section_widget.dart b/mobile/lib/ui/settings/support_section_widget.dart index aac3b9413f..fa730f703a 100644 --- a/mobile/lib/ui/settings/support_section_widget.dart +++ b/mobile/lib/ui/settings/support_section_widget.dart @@ -43,8 +43,8 @@ class SupportSectionWidget extends StatelessWidget { ), sectionOptionSpacing, AboutMenuItemWidget( - title: S.of(context).faq, - url: "https://ente.io/faq", + title: S.of(context).help, + url: "https://help.ente.io", ), sectionOptionSpacing, MenuItemWidget( diff --git a/mobile/lib/ui/viewer/gallery/hooks/add_photos_sheet.dart b/mobile/lib/ui/viewer/gallery/hooks/add_photos_sheet.dart index 0f2510d748..78dd4a4245 100644 --- a/mobile/lib/ui/viewer/gallery/hooks/add_photos_sheet.dart +++ b/mobile/lib/ui/viewer/gallery/hooks/add_photos_sheet.dart @@ -21,6 +21,7 @@ import "package:photos/ui/components/models/button_type.dart"; import "package:photos/ui/components/title_bar_title_widget.dart"; import "package:photos/ui/viewer/gallery/gallery.dart"; import "package:photos/utils/dialog_util.dart"; +import "package:photos/utils/photo_manager_util.dart"; import 'package:wechat_assets_picker/wechat_assets_picker.dart'; Future showAddPhotosSheet( @@ -203,7 +204,7 @@ class AddPhotosPhotoWidget extends StatelessWidget { } } catch (e) { if (e is StateError) { - final PermissionState ps = await PhotoManager.requestPermissionExtend(); + final PermissionState ps = await requestPhotoMangerPermissions(); if (ps != PermissionState.authorized && ps != PermissionState.limited) { await showChoiceDialog( context, diff --git a/mobile/lib/utils/photo_manager_util.dart b/mobile/lib/utils/photo_manager_util.dart new file mode 100644 index 0000000000..273d0b362e --- /dev/null +++ b/mobile/lib/utils/photo_manager_util.dart @@ -0,0 +1,12 @@ +import "package:photo_manager/photo_manager.dart"; + +Future requestPhotoMangerPermissions() { + return PhotoManager.requestPermissionExtend( + requestOption: const PermissionRequestOption( + androidPermission: AndroidPermission( + type: RequestType.common, + mediaLocation: true, + ), + ), + ); +} diff --git a/mobile/lib/utils/share_util.dart b/mobile/lib/utils/share_util.dart index 1f147ab8f8..ff9f691bd6 100644 --- a/mobile/lib/utils/share_util.dart +++ b/mobile/lib/utils/share_util.dart @@ -15,6 +15,7 @@ import 'package:photos/utils/exif_util.dart'; import 'package:photos/utils/file_util.dart'; import 'package:receive_sharing_intent/receive_sharing_intent.dart'; import 'package:share_plus/share_plus.dart'; +import "package:uuid/uuid.dart"; final _logger = Logger("ShareUtil"); // Set of possible image extensions @@ -116,14 +117,13 @@ Future> convertIncomingSharedMediaToFile( continue; } final enteFile = EnteFile(); + final sharedLocalId = const Uuid().v4(); // fileName: img_x.jpg enteFile.title = basename(media.path); var ioFile = File(media.path); try { ioFile = ioFile.renameSync( - Configuration.instance.getSharedMediaDirectory() + - "/" + - enteFile.title!, + Configuration.instance.getSharedMediaDirectory() + "/" + sharedLocalId, ); } catch (e) { if (e is FileSystemException) { @@ -135,7 +135,7 @@ Future> convertIncomingSharedMediaToFile( final newIoFile = ioFile.copySync( Configuration.instance.getSharedMediaDirectory() + "/" + - enteFile.title!, + sharedLocalId, ); if (media.path.contains("io.ente.photos")) { _logger.info("delete original file in path ${ioFile.path}"); @@ -146,7 +146,7 @@ Future> convertIncomingSharedMediaToFile( rethrow; } } - enteFile.localID = sharedMediaIdentifier + enteFile.title!; + enteFile.localID = sharedMediaIdentifier + sharedLocalId; enteFile.collectionID = collectionID; enteFile.fileType = media.type == SharedMediaType.image ? FileType.image : FileType.video; diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index 7e4534520b..39229bfb37 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -650,7 +650,7 @@ packages: source: hosted version: "0.6.0" flutter_driver: - dependency: "direct dev" + dependency: transitive description: flutter source: sdk version: "0.0.0" @@ -933,12 +933,11 @@ packages: home_widget: dependency: "direct main" description: - path: "." - ref: main - resolved-ref: "49158ce4a517e87817dc84c6b96c00639281229a" - url: "https://github.com/ente-io/FlutterHomeWidget" - source: git - version: "0.4.1" + name: home_widget + sha256: "29565bfee4b32eaf9e7e8b998d504618b779a74b2b1ac62dd4dac7468e66f1a3" + url: "https://pub.dev" + source: hosted + version: "0.5.0" html: dependency: transitive description: diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index ca8efa9c1f..727fe9dad6 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.71+591 +version: 0.8.72+592 publish_to: none environment: @@ -91,10 +91,7 @@ dependencies: fluttertoast: ^8.0.6 freezed_annotation: ^2.2.0 google_nav_bar: ^5.0.5 - home_widget: - git: - url: https://github.com/ente-io/FlutterHomeWidget - ref: main + home_widget: ^0.5.0 html_unescape: ^2.0.0 http: ^1.1.0 image: ^4.0.17 @@ -191,8 +188,6 @@ flutter_intl: dev_dependencies: build_runner: ^2.4.7 - flutter_driver: - sdk: flutter flutter_lints: ^2.0.1 flutter_test: sdk: flutter diff --git a/mobile/test_driver/perf_driver.dart b/mobile/test_driver/perf_driver.dart index b993c135e5..ecc0bd6b00 100644 --- a/mobile/test_driver/perf_driver.dart +++ b/mobile/test_driver/perf_driver.dart @@ -8,13 +8,13 @@ Future main() { responseDataCallback: (data) async { if (data != null) { final timeline = driver.Timeline.fromJson( - data['scrolling_summary'] as Map, + data['home_gallery_scrolling_summary'] as Map, ); final summary = driver.TimelineSummary.summarize(timeline); await summary.writeTimelineToFile( - 'scrolling_summary', + 'home_gallery_scrolling_summary', pretty: true, includeSummary: true, //Specify destination directory for the timeline files. diff --git a/server/README.md b/server/README.md index 40b479577d..66e17e5bd2 100644 --- a/server/README.md +++ b/server/README.md @@ -38,7 +38,13 @@ And ping again This time you'll see the updated message. For more details about how to get museum up and running, see -[RUNNING.md](RUNNING.md). +[RUNNING](RUNNING.md). + +> [!TIP] +> +> Also, there is a way to use our pre-built Docker images to directly start a +> cluster without needing to clone this repository - see +> [docs/docker](docs/docker.md). ## Architecture @@ -84,10 +90,11 @@ And it is built with containerization in mind - both during development and deployment. Just use the provided Dockerfile, configure to taste and you're off to the races. -> [!CAUTION] -> -> We don't publish any official docker images (yet). For self-hosters, the -> recommendation is to build your own image using the provided `Dockerfile`. +Overall, there are [three approaches](RUNNING.md) you can take: + +* Run using Docker using a pre-built Docker image +* Run using Docker but build an image from source +* Run without Docker Everything that you might needed to run museum is all in here, since this is the setup we ourselves use in production. diff --git a/server/RUNNING.md b/server/RUNNING.md index 22045fe2b9..132bc78013 100644 --- a/server/RUNNING.md +++ b/server/RUNNING.md @@ -8,13 +8,14 @@ environment that doesn't clutter your machine. You can also run museum directly on your machine if you wish - it is a single static go binary. -This document describes both these approaches, and also outlines configuration. +This document describes these approaches, and also outlines configuration. -- [Running using Docker](#docker) -- [Running without Docker](#without-docker) +- [Run using Docker using a pre-built Docker image](docs/docker.md) +- [Run using Docker but build an image from source](#build-and-run-using-docker) +- [Running without Docker](#run-without-docker) - [Configuration](#configuration) -## Docker +## Build and run using Docker Start the cluster @@ -70,7 +71,7 @@ Each time museum gets rebuilt from source, a new image gets created but the old one is retained as a dangling image. You can use `docker image prune --force`, or `docker system prune` if that's fine with you, to remove these. -## Without Docker +## Running without Docker The museum binary can be run by using `go run cmd/museum/main.go`. But first, you'll need to prepare your machine for development. Here we give the steps, @@ -132,7 +133,7 @@ pg_ctl -D /usr/local/var/postgres -l logfile start createuser -s postgres ``` -## Start museum +### Start museum ```sh export ENTE_DB_USER=postgres @@ -148,7 +149,7 @@ ENTE_DB_USER=ente_user air ``` -## Testing +### Testing Set up a local database for testing. This is not required for running the server. Create a test database with the following name and credentials: diff --git a/server/cmd/museum/main.go b/server/cmd/museum/main.go index 958054cab2..4cbc006122 100644 --- a/server/cmd/museum/main.go +++ b/server/cmd/museum/main.go @@ -37,7 +37,6 @@ import ( embeddingCtrl "github.com/ente-io/museum/pkg/controller/embedding" "github.com/ente-io/museum/pkg/controller/family" kexCtrl "github.com/ente-io/museum/pkg/controller/kex" - "github.com/ente-io/museum/pkg/controller/locationtag" "github.com/ente-io/museum/pkg/controller/lock" remoteStoreCtrl "github.com/ente-io/museum/pkg/controller/remotestore" "github.com/ente-io/museum/pkg/controller/storagebonus" @@ -50,7 +49,6 @@ import ( "github.com/ente-io/museum/pkg/repo/datacleanup" "github.com/ente-io/museum/pkg/repo/embedding" "github.com/ente-io/museum/pkg/repo/kex" - locationtagRepo "github.com/ente-io/museum/pkg/repo/locationtag" "github.com/ente-io/museum/pkg/repo/passkey" "github.com/ente-io/museum/pkg/repo/remotestore" storageBonusRepo "github.com/ente-io/museum/pkg/repo/storagebonus" @@ -150,7 +148,6 @@ func main() { twoFactorRecoveryRepo := &two_factor_recovery.Repository{Db: db, SecretEncryptionKey: secretEncryptionKeyBytes} billingRepo := &repo.BillingRepository{DB: db} userEntityRepo := &userEntityRepo.Repository{DB: db} - locationTagRepository := &locationtagRepo.Repository{DB: db} authRepo := &authenticatorRepo.Repository{DB: db} remoteStoreRepository := &remotestore.Repository{DB: db} dataCleanupRepository := &datacleanup.Repository{DB: db} @@ -641,13 +638,6 @@ func main() { privateAPI.DELETE("/user-entity/entity", userEntityHandler.DeleteEntity) privateAPI.GET("/user-entity/entity/diff", userEntityHandler.GetDiff) - locationTagController := &locationtag.Controller{Repo: locationTagRepository} - locationTagHandler := &api.LocationTagHandler{Controller: locationTagController} - privateAPI.POST("/locationtag/create", locationTagHandler.Create) - privateAPI.POST("/locationtag/update", locationTagHandler.Update) - privateAPI.DELETE("/locationtag/delete", locationTagHandler.Delete) - privateAPI.GET("/locationtag/diff", locationTagHandler.GetDiff) - authenticatorController := &authenticatorCtrl.Controller{Repo: authRepo} authenticatorHandler := &api.AuthenticatorHandler{Controller: authenticatorController} diff --git a/server/compose.yaml b/server/compose.yaml index 6972fc364c..a7d5a2c39e 100644 --- a/server/compose.yaml +++ b/server/compose.yaml @@ -17,6 +17,7 @@ services: - custom-logs:/var/logs - ./museum.yaml:/museum.yaml:ro - ./scripts/compose/credentials.yaml:/credentials.yaml:ro + - ./data:/data:ro networks: - internal diff --git a/server/configurations/local.yaml b/server/configurations/local.yaml index 80274df194..97dd353e1f 100644 --- a/server/configurations/local.yaml +++ b/server/configurations/local.yaml @@ -157,8 +157,24 @@ key: jwt: secret: i2DecQmfGreG6q1vBj5tCokhlN41gcfS2cjOs9Po-u8= +# SMTP configuration (optional) +# +# Configure credentials here for sending mails from museum (e.g. OTP emails). +# +# The smtp credentials will be used if the host is specified. Otherwise it will +# try to use the transmail credentials. Ideally, one of smtp or transmail should +# be configured for a production instance. +smtp: + host: + port: + username: + password: + # Zoho Zeptomail config (optional) -# Use case: Sending emails +# +# This is an alternative to the `smtp` configuration for sending emails. If this +# is set (and SMTP credentials are not set), then museum will use the transmail +# SDK for sending emails using Zoho Zeptomail. transmail: # Transmail token # Mail agent: dev @@ -279,4 +295,4 @@ jobs: # By default, this job is disabled. enabled: false # If provided, only objects that begin with this prefix are pruned. - prefix: "" + prefix: "" \ No newline at end of file diff --git a/server/docs/docker.md b/server/docs/docker.md new file mode 100644 index 0000000000..d8f3db9137 --- /dev/null +++ b/server/docs/docker.md @@ -0,0 +1,83 @@ +# Running using published Docker images + +Here we describe a way to run an Ente instance using a starter Docker compose +file and using the pre-built Docker images that we publish. This method does not +require you to clone the repository or build any images. + +1. Create a directory where you'll run Ente + + ```sh + mkdir ente && cd ente + ``` + +2. Copy the starter compose.yaml and two of its support files from the + repository onto your directory. You can do it by hand, or use (e.g.) curl + + ```sh + # compose.yaml + curl -LO https://raw.githubusercontent.com/ente-io/ente/main/server/compose.yaml + + mkdir -p scripts/compose + cd scripts/compose + + # scripts/compose/credentials.yaml + curl -LO https://raw.githubusercontent.com/ente-io/ente/main/server/scripts/compose/credentials.yaml + + # scripts/compose/minio-provision.sh + curl -LO https://raw.githubusercontent.com/ente-io/ente/main/server/scripts/compose/minio-provision.sh + + cd ../.. + ``` + +3. Modify `compose.yaml`. Instead of building from source, we want directly use + the published Docker image from `ghcr.io/ente-io/server` + + ```diff + --- a/server/compose.yaml + +++ b/server/compose.yaml + @@ -1,9 +1,6 @@ + services: + museum: + - build: + - context: . + - args: + - GIT_COMMIT: development-cluster + + image: ghcr.io/ente-io/server + ``` + +4. Create an (empty) configuration file. Yyou can later put your custom + configuration in this if needed. + + ```sh + touch museum.yaml + ``` + +5. That is all. You can now start everything. + + ```sh + docker compose up + ``` + +This will start a cluster containing: + +* Ente's own server +* PostgresQL (DB) +* MinIO (the S3 layer) + +For each of these, it'll use the latest published Docker image. + +You can do a quick smoke test by pinging the API: + +```sh +curl localhost:8080/ping +``` + +## Only the server + +Alternatively, if you have setup the database and object storage externally and +only want to run Ente's server, you can skip the steps above and directly pull +and run the image from **`ghcr.io/ente-io/server`**. + +```sh +docker pull ghcr.io/ente-io/server +``` diff --git a/server/docs/publish.md b/server/docs/publish.md new file mode 100644 index 0000000000..de4849d900 --- /dev/null +++ b/server/docs/publish.md @@ -0,0 +1,41 @@ +# Publishing images + +There are two different images we publish - internal and external. + +## Internal + +The internal images can be built and run by triggering the "Server (release)" +workflow. You can trigger it either from GitHub's UI on the Actions tab, or use +the following command: + + gh workflow run server-release.yml + +This will take the latest main, package it into a Docker image, and publish it +to our Scaleway registry. From there, we can update our production instances to +use this new image (see [deploy/README](../scripts/deploy/README.md)). + +## External + +Periodically, we can republish a new image from an existing known-to-be-good +commit to the GitHub Container Registry (GHCR) so that it can be used by folks +without needing to clone our repository just for building an image. For more +details about the use case, see [docker.md](docker.md). + +To publish such an external image, firstly find the commit of the currently +running production instance. + + curl -s https://api.ente.io/ping | jq -r '.id' + +> We can publish from any arbitrary commit really, but by using the commit +> that's already seen production for a few days, we avoid externally publishing +> images with issues. + +Then, trigger the "Publish (server)" workflow, providing it the commit. You can +trigger it either from GitHub's UI or using the `gh cli`. With the CLI, we can +combine both these steps too. + + gh workflow run server-publish.yml -F commit=`curl -s https://api.ente.io/ping | jq -r '.id'` + +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". diff --git a/server/ente/billing.go b/server/ente/billing.go index 5a0ff08a86..20c37bdb5a 100644 --- a/server/ente/billing.go +++ b/server/ente/billing.go @@ -42,7 +42,7 @@ const ( OnHoldTemplate = "on_hold.html" // AccountOnHoldEmailSubject is the subject of account on hold email - AccountOnHoldEmailSubject = "ente account on hold" + AccountOnHoldEmailSubject = "Ente account on hold" // Template for the email we send out when the user's subscription ends, // either because the user cancelled their subscription, or because it diff --git a/server/ente/locationtag.go b/server/ente/locationtag.go deleted file mode 100644 index 61c191006c..0000000000 --- a/server/ente/locationtag.go +++ /dev/null @@ -1,59 +0,0 @@ -package ente - -import ( - "database/sql/driver" - "encoding/json" - "github.com/ente-io/stacktrace" - "github.com/google/uuid" -) - -// LocationTag represents a location tag in the system. The location information -// is stored in an encrypted as Attributes -type LocationTag struct { - ID uuid.UUID `json:"id"` - OwnerID int64 `json:"ownerId,omitempty"` - EncryptedKey string `json:"encryptedKey" binding:"required"` - KeyDecryptionNonce string `json:"keyDecryptionNonce" binding:"required"` - Attributes LocationTagAttribute `json:"attributes" binding:"required"` - IsDeleted bool `json:"isDeleted"` - Provider string `json:"provider,omitempty"` - CreatedAt int64 `json:"createdAt,omitempty"` // utc epoch microseconds - UpdatedAt int64 `json:"updatedAt,omitempty"` // utc epoch microseconds -} - -// LocationTagAttribute holds encrypted data about user's location tag. -type LocationTagAttribute struct { - Version int `json:"version,omitempty" binding:"required"` - EncryptedData string `json:"encryptedData,omitempty" binding:"required"` - DecryptionNonce string `json:"decryptionNonce,omitempty" binding:"required"` -} - -// Value implements the driver.Valuer interface. This method -// simply returns the JSON-encoded representation of the struct. -func (la LocationTagAttribute) Value() (driver.Value, error) { - return json.Marshal(la) -} - -// Scan implements the sql.Scanner interface. This method -// simply decodes a JSON-encoded value into the struct fields. -func (la *LocationTagAttribute) Scan(value interface{}) error { - b, ok := value.([]byte) - if !ok { - return stacktrace.NewError("type assertion to []byte failed") - } - return json.Unmarshal(b, &la) -} - -// DeleteLocationTagRequest is request structure for deleting a location tag -type DeleteLocationTagRequest struct { - ID uuid.UUID `json:"id" binding:"required"` - OwnerID int64 // should be populated from req headers -} - -// GetLocationTagDiffRequest is request struct for fetching locationTag changes -type GetLocationTagDiffRequest struct { - // SinceTime *int64. Pointer allows us to pass 0 value otherwise binding fails for zero Value. - SinceTime *int64 `form:"sinceTime" binding:"required"` - Limit int16 `form:"limit" binding:"required"` - OwnerID int64 // should be populated from req headers -} diff --git a/server/ente/userentity/entity.go b/server/ente/userentity/entity.go index 729b69e45c..71baa3ae9e 100644 --- a/server/ente/userentity/entity.go +++ b/server/ente/userentity/entity.go @@ -8,6 +8,7 @@ type EntityType string const ( Location EntityType = "location" + Person EntityType = "person" ) type EntityKey struct { diff --git a/server/mail-templates/mobile_app_first_upload.html b/server/mail-templates/mobile_app_first_upload.html index 53d7cfa862..1ee9546720 100644 --- a/server/mail-templates/mobile_app_first_upload.html +++ b/server/mail-templates/mobile_app_first_upload.html @@ -302,7 +302,7 @@ About FAQ + href="https://help.ente.io/">Help Twitter @@ -234,7 +234,7 @@ from {{.PaymentProvider}} within the next - 30 days, our systems + 31 days, our systems may remove your account and all associated data with diff --git a/server/mail-templates/subscription_ended.html b/server/mail-templates/subscription_ended.html index 4a158d7671..a60fe0e2f9 100644 --- a/server/mail-templates/subscription_ended.html +++ b/server/mail-templates/subscription_ended.html @@ -214,7 +214,7 @@
If you still have data stored in ente, we encourage you to follow the steps outlined here to export your data: ente.io/faq/migration/out-of-ente. + style="font-family: helvetica, sans-serif">If you still have data stored in ente, we encourage you to follow the steps outlined here to export your data: help.ente.io/photos/migration/export.
diff --git a/server/mail-templates/successful_referral.html b/server/mail-templates/successful_referral.html index 60812e0fd6..fd7f3ea231 100644 --- a/server/mail-templates/successful_referral.html +++ b/server/mail-templates/successful_referral.html @@ -253,7 +253,7 @@ About FAQ + href="https://help.ente.io/">Help Twitter About FAQ + href="https://help.ente.io/">Help Twitter $2 - ORDER BY updated_at - LIMIT $3`, - ownerID, // $1 - sinceTime, // %2 - limit, // $3 - ) - if err != nil { - return nil, stacktrace.Propagate(err, "GetDiff query failed") - } - return convertRowsToLocationTags(rows) -} - -func (r *Repository) Delete(ctx context.Context, id string, ownerID int64) (bool, error) { - _, err := r.DB.ExecContext(ctx, - `UPDATE location_tag SET is_deleted=$1, attributes=$2 where id=$3 and user_id = $4`, - true, `{}`, // $1 is_deleted, $2 attr - id, ownerID) // $3 tagId, $4 ownerID - if err != nil { - return false, stacktrace.Propagate(err, fmt.Sprintf("faield to delele tag with id=%s", id)) - } - return true, nil -} - -func convertRowsToLocationTags(rows *sql.Rows) ([]ente.LocationTag, error) { - defer func() { - if err := rows.Close(); err != nil { - logrus.Error(err) - } - }() - locationTags := make([]ente.LocationTag, 0) - for rows.Next() { - tag := ente.LocationTag{} - err := rows.Scan( - &tag.ID, &tag.OwnerID, &tag.Provider, &tag.EncryptedKey, &tag.KeyDecryptionNonce, - &tag.Attributes, &tag.IsDeleted, &tag.CreatedAt, &tag.UpdatedAt) - if err != nil { - return nil, stacktrace.Propagate(err, "Failed to convert rowToLocationTag") - } - locationTags = append(locationTags, tag) - } - return locationTags, nil -} diff --git a/server/pkg/utils/email/email.go b/server/pkg/utils/email/email.go index 89993882cb..46202313e7 100644 --- a/server/pkg/utils/email/email.go +++ b/server/pkg/utils/email/email.go @@ -10,6 +10,7 @@ import ( "encoding/json" "html/template" "net/http" + "net/smtp" "strings" "github.com/ente-io/museum/ente" @@ -20,6 +21,78 @@ import ( // Send sends an email func Send(toEmails []string, fromName string, fromEmail string, subject string, htmlBody string, inlineImages []map[string]interface{}) error { + smtpHost := viper.GetString("smtp.host") + if smtpHost != "" { + return sendViaSMTP(toEmails, fromName, fromEmail, subject, htmlBody, inlineImages) + } else { + return sendViaTransmail(toEmails, fromName, fromEmail, subject, htmlBody, inlineImages) + } +} + +func sendViaSMTP(toEmails []string, fromName string, fromEmail string, subject string, htmlBody string, inlineImages []map[string]interface{}) error { + if len(toEmails) == 0 { + return ente.ErrBadRequest + } + + smtpServer := viper.GetString("smtp.host") + smtpPort := viper.GetString("smtp.port") + smtpUsername := viper.GetString("smtp.username") + smtpPassword := viper.GetString("smtp.password") + + var emailMessage string + + // Construct 'emailAddresses' with comma-separated email addresses + var emailAddresses string + for i, email := range toEmails { + if i != 0 { + emailAddresses += "," + } + emailAddresses += email + } + + header := "From: " + fromName + " <" + fromEmail + ">\n" + + "To: " + emailAddresses + "\n" + + "Subject: " + subject + "\n" + + "MIME-Version: 1.0\n" + + "Content-Type: multipart/related; boundary=boundary\n\n" + + "--boundary\n" + htmlContent := "Content-Type: text/html; charset=us-ascii\n\n" + htmlBody + "\n" + + emailMessage = header + htmlContent + + if inlineImages == nil { + emailMessage += "--boundary--" + } else { + for _, inlineImage := range inlineImages { + + emailMessage += "--boundary\n" + var mimeType = inlineImage["mime_type"].(string) + var contentID = inlineImage["cid"].(string) + var imgBase64Str = inlineImage["content"].(string) + + var image = "Content-Type: " + mimeType + "\n" + + "Content-Transfer-Encoding: base64\n" + + "Content-ID: <" + contentID + ">\n" + + "Content-Disposition: inline\n\n" + imgBase64Str + "\n" + + emailMessage += image + } + emailMessage += "--boundary--" + } + + // Send the email to each recipient + for _, toEmail := range toEmails { + auth := smtp.PlainAuth("", smtpUsername, smtpPassword, smtpServer) + err := smtp.SendMail(smtpServer+":"+smtpPort, auth, fromEmail, []string{toEmail}, []byte(emailMessage)) + if err != nil { + return stacktrace.Propagate(err, "") + } + } + + return nil +} + +func sendViaTransmail(toEmails []string, fromName string, fromEmail string, subject string, htmlBody string, inlineImages []map[string]interface{}) error { if len(toEmails) == 0 { return ente.ErrBadRequest } @@ -69,6 +142,7 @@ func SendTemplatedEmail(to []string, fromName string, fromEmail string, subject if err != nil { return stacktrace.Propagate(err, "") } + return Send(to, fromName, fromEmail, subject, body, inlineImages) } diff --git a/web/.gitignore b/web/.gitignore index f07d932590..0046043bd1 100644 --- a/web/.gitignore +++ b/web/.gitignore @@ -9,7 +9,7 @@ node_modules/ # Local env files .env -.env.*.local +.env*.local # Next.js .next/ diff --git a/web/README.md b/web/README.md index 36eb1fb25d..908676c558 100644 --- a/web/README.md +++ b/web/README.md @@ -32,7 +32,7 @@ yarn dev That's it. The web app will automatically hot reload when you make changes. -If you're new to web development and unsure about how to get started, or are +If you're new to web development and unsure about how to get started, or are facing some problems when running the above steps, see [docs/new](docs/new.md). ## Other apps diff --git a/web/apps/accounts/public/locales/en-US/translation.json b/web/apps/accounts/public/locales/en-US/translation.json index de8d2fe2a1..b06336bf52 100644 --- a/web/apps/accounts/public/locales/en-US/translation.json +++ b/web/apps/accounts/public/locales/en-US/translation.json @@ -7,7 +7,7 @@ "HERO_SLIDE_3": "Android, iOS, Web, Desktop", "LOGIN": "Login", "SIGN_UP": "Signup", - "NEW_USER": "New to ente", + "NEW_USER": "New to Ente", "EXISTING_USER": "Existing user", "ENTER_NAME": "Enter name", "PUBLIC_UPLOADER_NAME_MESSAGE": "Add a name so that your friends know who to thank for these great photos!", @@ -93,7 +93,7 @@ "TRASH_FILES_TITLE": "Delete files?", "TRASH_FILE_TITLE": "Delete file?", "DELETE_FILES_TITLE": "Delete immediately?", - "DELETE_FILES_MESSAGE": "Selected files will be permanently deleted from your ente account.", + "DELETE_FILES_MESSAGE": "Selected files will be permanently deleted from your Ente account.", "DELETE": "Delete", "DELETE_OPTION": "Delete (DEL)", "FAVORITE_OPTION": "Favorite (L)", @@ -105,7 +105,7 @@ "UPLOAD_STRATEGY_COLLECTION_PER_FOLDER": "Separate albums", "SESSION_EXPIRED_MESSAGE": "Your session has expired, please login again to continue", "SESSION_EXPIRED": "Session expired", - "PASSWORD_GENERATION_FAILED": "Your browser was unable to generate a strong key that meets ente's encryption standards, please try using the mobile app or another browser", + "PASSWORD_GENERATION_FAILED": "Your browser was unable to generate a strong key that meets Ente's encryption standards, please try using the mobile app or another browser", "CHANGE_PASSWORD": "Change password", "GO_BACK": "Go back", "RECOVERY_KEY": "Recovery key", @@ -278,11 +278,11 @@ "LAST_EXPORT_TIME": "Last export time", "EXPORT_AGAIN": "Resync", "LOCAL_STORAGE_NOT_ACCESSIBLE": "Local storage not accessible", - "LOCAL_STORAGE_NOT_ACCESSIBLE_MESSAGE": "Your browser or an addon is blocking ente from saving data into local storage. please try loading this page after switching your browsing mode.", + "LOCAL_STORAGE_NOT_ACCESSIBLE_MESSAGE": "Your browser or an addon is blocking Ente from saving data into local storage. please try loading this page after switching your browsing mode.", "SEND_OTT": "Send OTP", "EMAIl_ALREADY_OWNED": "Email already taken", - "ETAGS_BLOCKED": "

We were unable to upload the following files because of your browser configuration.

Please disable any addons that might be preventing ente from using eTags to upload large files, or use our desktop app for a more reliable import experience.

", - "SKIPPED_VIDEOS_INFO": "

Presently we do not support adding videos via public links.

To share videos, please signup for ente and share with the intended recipients using their email.

", + "ETAGS_BLOCKED": "

We were unable to upload the following files because of your browser configuration.

Please disable any addons that might be preventing Ente from using eTags to upload large files, or use our desktop app for a more reliable import experience.

", + "SKIPPED_VIDEOS_INFO": "

Presently we do not support adding videos via public links.

To share videos, please signup for Ente and share with the intended recipients using their email.

", "LIVE_PHOTOS_DETECTED": "The photo and video files from your Live Photos have been merged into a single file", "RETRY_FAILED": "Retry failed uploads", "FAILED_UPLOADS": "Failed uploads ", @@ -291,7 +291,7 @@ "UNSUPPORTED_FILES": "Unsupported files", "SUCCESSFUL_UPLOADS": "Successful uploads", "SKIPPED_INFO": "Skipped these as there are files with matching names in the same album", - "UNSUPPORTED_INFO": "ente does not support these file formats yet", + "UNSUPPORTED_INFO": "Ente does not support these file formats yet", "BLOCKED_UPLOADS": "Blocked uploads", "SKIPPED_VIDEOS": "Skipped videos", "INPROGRESS_METADATA_EXTRACTION": "In progress", @@ -327,7 +327,7 @@ "RESTORE_TO_COLLECTION": "Restore to album", "EMPTY_TRASH": "Empty trash", "EMPTY_TRASH_TITLE": "Empty trash?", - "EMPTY_TRASH_MESSAGE": "These files will be permanently deleted from your ente account.", + "EMPTY_TRASH_MESSAGE": "These files will be permanently deleted from your Ente account.", "LEAVE_SHARED_ALBUM": "Yes, leave", "LEAVE_ALBUM": "Leave album", "LEAVE_SHARED_ALBUM_TITLE": "Leave shared album?", @@ -342,7 +342,7 @@ "THUMBNAIL_REPLACED": "Thumbnails compressed", "FIX_THUMBNAIL": "Compress", "FIX_THUMBNAIL_LATER": "Compress later", - "REPLACE_THUMBNAIL_NOT_STARTED": "Some of your videos thumbnails can be compressed to save space. would you like ente to compress them?", + "REPLACE_THUMBNAIL_NOT_STARTED": "Some of your videos thumbnails can be compressed to save space. would you like Ente to compress them?", "REPLACE_THUMBNAIL_COMPLETED": "Successfully compressed all thumbnails", "REPLACE_THUMBNAIL_NOOP": "You have no thumbnails that can be compressed further", "REPLACE_THUMBNAIL_COMPLETED_WITH_ERROR": "Could not compress some of your thumbnails, please retry", @@ -421,8 +421,8 @@ "AUTHENTICATOR_SECTION": "Authenticator", "NO_DUPLICATES_FOUND": "You've no duplicate files that can be cleared", "CLUB_BY_CAPTURE_TIME": "Club by capture time", - "FILES": "Files", - "EACH": "Each", + "FILES": "files", + "EACH": "each", "DEDUPLICATE_BASED_ON_SIZE": "The following files were clubbed based on their sizes, please review and delete items you believe are duplicates", "STOP_ALL_UPLOADS_MESSAGE": "Are you sure that you want to stop all the uploads in progress?", "STOP_UPLOADS_HEADER": "Stop uploads?", @@ -455,12 +455,12 @@ "WATCHED_FOLDERS": "Watched folders", "NO_FOLDERS_ADDED": "No folders added yet!", "FOLDERS_AUTOMATICALLY_MONITORED": "The folders you add here will monitored to automatically", - "UPLOAD_NEW_FILES_TO_ENTE": "Upload new files to ente", - "REMOVE_DELETED_FILES_FROM_ENTE": "Remove deleted files from ente", + "UPLOAD_NEW_FILES_TO_ENTE": "Upload new files to Ente", + "REMOVE_DELETED_FILES_FROM_ENTE": "Remove deleted files from Ente", "ADD_FOLDER": "Add folder", "STOP_WATCHING": "Stop watching", "STOP_WATCHING_FOLDER": "Stop watching folder?", - "STOP_WATCHING_DIALOG_MESSAGE": "Your existing files will not be deleted, but ente will stop automatically updating the linked ente album on changes in this folder.", + "STOP_WATCHING_DIALOG_MESSAGE": "Your existing files will not be deleted, but Ente will stop automatically updating the linked Ente album on changes in this folder.", "YES_STOP": "Yes, stop", "MONTH_SHORT": "mo", "YEAR": "year", @@ -474,18 +474,18 @@ "FREE_PLAN_OPTION_LABEL": "Continue with free trial", "FREE_PLAN_DESCRIPTION": "1 GB 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", + "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", "CONFIRM_ACCOUNT_DELETION_MESSAGE": "Your uploaded data will be scheduled for deletion, and your account will be permanently deleted.

This action is not reversible.", "AUTHENTICATE": "Authenticate", "UPLOADED_TO_SINGLE_COLLECTION": "Uploaded to single collection", "UPLOADED_TO_SEPARATE_COLLECTIONS": "Uploaded to separate collections", "NEVERMIND": "Nevermind", "UPDATE_AVAILABLE": "Update available", - "UPDATE_INSTALLABLE_MESSAGE": "A new version of ente is ready to be installed.", + "UPDATE_INSTALLABLE_MESSAGE": "A new version of Ente is ready to be installed.", "INSTALL_NOW": "Install now", "INSTALL_ON_NEXT_LAUNCH": "Install on next launch", - "UPDATE_AVAILABLE_MESSAGE": "A new version of ente has been released, but it cannot be automatically downloaded and installed.", + "UPDATE_AVAILABLE_MESSAGE": "A new version of Ente has been released, but it cannot be automatically downloaded and installed.", "DOWNLOAD_AND_INSTALL": "Download and install", "IGNORE_THIS_VERSION": "Ignore this version", "TODAY": "Today", @@ -499,13 +499,13 @@ "ML_MORE_DETAILS": "More details", "ENABLE_FACE_SEARCH": "Enable face recognition", "ENABLE_FACE_SEARCH_TITLE": "Enable face recognition?", - "ENABLE_FACE_SEARCH_DESCRIPTION": "

If you enable face recognition, ente will extract face geometry from your photos. This will happen on your device, and any generated biometric data will be end-to-encrypted.

Please click here for more details about this feature in our privacy policy

", + "ENABLE_FACE_SEARCH_DESCRIPTION": "

If you enable face recognition, Ente will extract face geometry from your photos. This will happen on your device, and any generated biometric data will be end-to-encrypted.

Please click here for more details about this feature in our privacy policy

", "DISABLE_BETA": "Pause recognition", "DISABLE_FACE_SEARCH": "Disable face recognition", "DISABLE_FACE_SEARCH_TITLE": "Disable face recognition?", "DISABLE_FACE_SEARCH_DESCRIPTION": "

Ente will stop processing face geometry.

You can reenable face recognition again if you wish, so this operation is safe.

", "ADVANCED": "Advanced", - "FACE_SEARCH_CONFIRMATION": "I understand, and wish to allow ente to process face geometry", + "FACE_SEARCH_CONFIRMATION": "I understand, and wish to allow Ente to process face geometry", "LABS": "Labs", "YOURS": "yours", "PASSPHRASE_STRENGTH_WEAK": "Password strength: Weak", @@ -597,7 +597,7 @@ "LIVE_PHOTO": "Live Photo", "CONVERT": "Convert", "CONFIRM_EDITOR_CLOSE_MESSAGE": "Are you sure you want to close the editor?", - "CONFIRM_EDITOR_CLOSE_DESCRIPTION": "Download your edited image or save a copy to ente to persist your changes.", + "CONFIRM_EDITOR_CLOSE_DESCRIPTION": "Download your edited image or save a copy to Ente to persist your changes.", "BRIGHTNESS": "Brightness", "CONTRAST": "Contrast", "SATURATION": "Saturation", @@ -610,7 +610,7 @@ "FLIP_VERTICALLY": "Flip Vertically", "FLIP_HORIZONTALLY": "Flip Horizontally", "DOWNLOAD_EDITED": "Download Edited", - "SAVE_A_COPY_TO_ENTE": "Save a copy to ente", + "SAVE_A_COPY_TO_ENTE": "Save a copy to Ente", "RESTORE_ORIGINAL": "Restore Original", "TRANSFORM": "Transform", "COLORS": "Colors", diff --git a/web/apps/accounts/src/pages/_app.tsx b/web/apps/accounts/src/pages/_app.tsx index 03d675a2f1..8ff4c6a9fa 100644 --- a/web/apps/accounts/src/pages/_app.tsx +++ b/web/apps/accounts/src/pages/_app.tsx @@ -1,7 +1,5 @@ import { setupI18n } from "@/ui/i18n"; -import { CacheProvider } from "@emotion/react"; import { APPS, APP_TITLES } from "@ente/shared/apps/constants"; -import { EnteAppProps } from "@ente/shared/apps/types"; import { Overlay } from "@ente/shared/components/Container"; import DialogBoxV2 from "@ente/shared/components/DialogBoxV2"; import { @@ -15,9 +13,9 @@ import HTTPService from "@ente/shared/network/HTTPService"; import { LS_KEYS, getData } from "@ente/shared/storage/localStorage"; import { getTheme } from "@ente/shared/themes"; import { THEME_COLOR } from "@ente/shared/themes/constants"; -import createEmotionCache from "@ente/shared/themes/createEmotionCache"; import { CssBaseline, useMediaQuery } from "@mui/material"; import { ThemeProvider } from "@mui/material/styles"; +import { AppProps } from "next/app"; import Head from "next/head"; import { useRouter } from "next/router"; import { createContext, useEffect, useState } from "react"; @@ -31,10 +29,7 @@ interface AppContextProps { export const AppContext = createContext({} as AppContextProps); -// Client-side cache, shared for the whole session of the user in the browser. -const clientSideEmotionCache = createEmotionCache(); - -export default function App(props: EnteAppProps) { +export default function App(props: AppProps) { const [isI18nReady, setIsI18nReady] = useState(false); const [showNavbar, setShowNavBar] = useState(false); @@ -54,11 +49,7 @@ export default function App(props: EnteAppProps) { const router = useRouter(); - const { - Component, - emotionCache = clientSideEmotionCache, - pageProps, - } = props; + const { Component, pageProps } = props; const [themeColor] = useLocalState(LS_KEYS.THEME, THEME_COLOR.DARK); @@ -87,7 +78,7 @@ export default function App(props: EnteAppProps) { // TODO: Localise APP_TITLES return ( - + <> {APP_TITLES.get(APPS.ACCOUNTS)} )} {showNavbar && } - + {isI18nReady && } - + ); } diff --git a/web/apps/accounts/src/pages/_document.tsx b/web/apps/accounts/src/pages/_document.tsx index 09d4d57823..3c6c2a9590 100644 --- a/web/apps/accounts/src/pages/_document.tsx +++ b/web/apps/accounts/src/pages/_document.tsx @@ -1,7 +1,3 @@ -import DocumentPage, { - EnteDocumentProps, -} from "@ente/shared/next/pages/_document"; +import DocumentPage from "@ente/shared/next/pages/_document"; -export default function Document(props: EnteDocumentProps) { - return ; -} +export default DocumentPage; diff --git a/web/apps/accounts/src/styles/global.css b/web/apps/accounts/src/styles/global.css index 0ea6c125da..98ad85a9b3 100644 --- a/web/apps/accounts/src/styles/global.css +++ b/web/apps/accounts/src/styles/global.css @@ -150,21 +150,6 @@ body { background-color: #51cd7c; } -.carousel-inner { - padding-bottom: 50px !important; -} - -.carousel-indicators li { - width: 10px; - height: 10px; - border-radius: 50%; - margin-right: 12px; -} - -.carousel-indicators .active { - background-color: #51cd7c; -} - div.otp-input input { width: 36px !important; height: 36px; diff --git a/web/apps/auth/src/pages/_app.tsx b/web/apps/auth/src/pages/_app.tsx index c06531ab42..06dfc2402c 100644 --- a/web/apps/auth/src/pages/_app.tsx +++ b/web/apps/auth/src/pages/_app.tsx @@ -1,7 +1,9 @@ -import AppNavbar from "@ente/shared/components/Navbar/app"; -import { t } from "i18next"; -import { createContext, useEffect, useRef, useState } from "react"; - +import { setupI18n } from "@/ui/i18n"; +import { + APPS, + APP_TITLES, + CLIENT_PACKAGE_NAMES, +} from "@ente/shared/apps/constants"; import { Overlay } from "@ente/shared/components/Container"; import DialogBoxV2 from "@ente/shared/components/DialogBoxV2"; import { @@ -10,32 +12,26 @@ import { } from "@ente/shared/components/DialogBoxV2/types"; import EnteSpinner from "@ente/shared/components/EnteSpinner"; import { MessageContainer } from "@ente/shared/components/MessageContainer"; +import AppNavbar from "@ente/shared/components/Navbar/app"; +import { PHOTOS_PAGES as PAGES } from "@ente/shared/constants/pages"; +import { useLocalState } from "@ente/shared/hooks/useLocalState"; import { clearLogsIfLocalStorageLimitExceeded, logStartupMessage, } from "@ente/shared/logging/web"; import HTTPService from "@ente/shared/network/HTTPService"; import { LS_KEYS } from "@ente/shared/storage/localStorage"; -import { CssBaseline, useMediaQuery } from "@mui/material"; -import { ThemeProvider } from "@mui/material/styles"; -import Head from "next/head"; -import { useRouter } from "next/router"; -import LoadingBar from "react-top-loading-bar"; - -import { setupI18n } from "@/ui/i18n"; -import { CacheProvider } from "@emotion/react"; -import { - APP_TITLES, - APPS, - CLIENT_PACKAGE_NAMES, -} from "@ente/shared/apps/constants"; -import { EnteAppProps } from "@ente/shared/apps/types"; -import { PHOTOS_PAGES as PAGES } from "@ente/shared/constants/pages"; -import { useLocalState } from "@ente/shared/hooks/useLocalState"; import { getTheme } from "@ente/shared/themes"; import { THEME_COLOR } from "@ente/shared/themes/constants"; -import createEmotionCache from "@ente/shared/themes/createEmotionCache"; import { SetTheme } from "@ente/shared/themes/types"; +import { CssBaseline, useMediaQuery } from "@mui/material"; +import { ThemeProvider } from "@mui/material/styles"; +import { t } from "i18next"; +import { AppProps } from "next/app"; +import Head from "next/head"; +import { useRouter } from "next/router"; +import { createContext, useEffect, useRef, useState } from "react"; +import LoadingBar from "react-top-loading-bar"; import "../../public/css/global.css"; type AppContextType = { @@ -51,15 +47,8 @@ type AppContextType = { export const AppContext = createContext(null); -// Client-side cache, shared for the whole session of the user in the browser. -const clientSideEmotionCache = createEmotionCache(); - -export default function App(props: EnteAppProps) { - const { - Component, - emotionCache = clientSideEmotionCache, - pageProps, - } = props; +export default function App(props: AppProps) { + const { Component, pageProps } = props; const router = useRouter(); const [isI18nReady, setIsI18nReady] = useState(false); const [loading, setLoading] = useState(false); @@ -141,7 +130,7 @@ export default function App(props: EnteAppProps) { }); return ( - + <> {isI18nReady @@ -195,9 +184,11 @@ export default function App(props: EnteAppProps) { <EnteSpinner /> </Overlay> )} - <Component setLoading={setLoading} {...pageProps} /> + {isI18nReady && ( + <Component setLoading={setLoading} {...pageProps} /> + )} </AppContext.Provider> </ThemeProvider> - </CacheProvider> + </> ); } diff --git a/web/apps/auth/src/pages/_document.tsx b/web/apps/auth/src/pages/_document.tsx index 09d4d57823..3c6c2a9590 100644 --- a/web/apps/auth/src/pages/_document.tsx +++ b/web/apps/auth/src/pages/_document.tsx @@ -1,7 +1,3 @@ -import DocumentPage, { - EnteDocumentProps, -} from "@ente/shared/next/pages/_document"; +import DocumentPage from "@ente/shared/next/pages/_document"; -export default function Document(props: EnteDocumentProps) { - return <DocumentPage {...props} />; -} +export default DocumentPage; diff --git a/web/apps/cast/src/components/LargeType.tsx b/web/apps/cast/src/components/LargeType.tsx index bb0728699f..61d31e4098 100644 --- a/web/apps/cast/src/components/LargeType.tsx +++ b/web/apps/cast/src/components/LargeType.tsx @@ -1,3 +1,5 @@ +import styled from "@emotion/styled"; + const colourPool = [ "#87CEFA", // Light Blue "#90EE90", // Light Green @@ -23,44 +25,41 @@ const colourPool = [ export default function LargeType({ chars }: { chars: string[] }) { return ( - <table - style={{ - fontSize: "4rem", - fontWeight: "bold", - fontFamily: "monospace", - display: "flex", - position: "relative", - }} - > + <Container style={{}}> {chars.map((char, i) => ( - <tr + <span key={i} style={{ - display: "flex", - flexDirection: "column", - alignItems: "center", - padding: "0.5rem", // alternating background backgroundColor: i % 2 === 0 ? "#2e2e2e" : "#5e5e5e", + // varying colors + color: colourPool[i % colourPool.length], }} > - <span - style={{ - color: colourPool[i % colourPool.length], - lineHeight: 1.2, - }} - > - {char} - </span> - <span - style={{ - fontSize: "1rem", - }} - > - {i + 1} - </span> - </tr> + {char} + </span> ))} - </table> + </Container> ); } + +const Container = styled.div` + font-size: 4rem; + font-weight: bold; + font-family: monospace; + + line-height: 1.2; + + /* + * - We want them to be spans so that when the text is copy pasted, there + * is no extra whitespace inserted. + * + * - But we also want them to have a block level padding. + * + * To achieve both these goals, make them inline-blocks + */ + span { + display: inline-block; + padding: 0.5rem; + } +`; diff --git a/web/apps/cast/src/components/PhotoAuditorium.tsx b/web/apps/cast/src/components/PhotoAuditorium.tsx new file mode 100644 index 0000000000..0042dfe953 --- /dev/null +++ b/web/apps/cast/src/components/PhotoAuditorium.tsx @@ -0,0 +1,95 @@ +import { SlideshowContext } from "pages/slideshow"; +import { useContext, useEffect, useState } from "react"; + +export default function PhotoAuditorium({ + url, + nextSlideUrl, +}: { + url: string; + nextSlideUrl: string; +}) { + const { showNextSlide } = useContext(SlideshowContext); + + const [showPreloadedNextSlide, setShowPreloadedNextSlide] = useState(false); + const [nextSlidePrerendered, setNextSlidePrerendered] = useState(false); + const [prerenderTime, setPrerenderTime] = useState<number | null>(null); + + useEffect(() => { + let timeout: NodeJS.Timeout; + let timeout2: NodeJS.Timeout; + + if (nextSlidePrerendered) { + const elapsedTime = prerenderTime ? Date.now() - prerenderTime : 0; + const delayTime = Math.max(10000 - elapsedTime, 0); + + if (elapsedTime >= 10000) { + setShowPreloadedNextSlide(true); + } else { + timeout = setTimeout(() => { + setShowPreloadedNextSlide(true); + }, delayTime); + } + + if (showNextSlide) { + timeout2 = setTimeout(() => { + showNextSlide(); + setNextSlidePrerendered(false); + setPrerenderTime(null); + setShowPreloadedNextSlide(false); + }, delayTime); + } + } + + return () => { + if (timeout) clearTimeout(timeout); + if (timeout2) clearTimeout(timeout2); + }; + }, [nextSlidePrerendered, showNextSlide, prerenderTime]); + + return ( + <div + style={{ + width: "100vw", + height: "100vh", + backgroundImage: `url(${url})`, + backgroundSize: "cover", + backgroundPosition: "center", + backgroundRepeat: "no-repeat", + backgroundBlendMode: "multiply", + backgroundColor: "rgba(0, 0, 0, 0.5)", + }} + > + <div + style={{ + height: "100%", + width: "100%", + display: "flex", + justifyContent: "center", + alignItems: "center", + backdropFilter: "blur(10px)", + }} + > + <img + src={url} + style={{ + maxWidth: "100%", + maxHeight: "100%", + display: showPreloadedNextSlide ? "none" : "block", + }} + /> + <img + src={nextSlideUrl} + style={{ + maxWidth: "100%", + maxHeight: "100%", + display: showPreloadedNextSlide ? "block" : "none", + }} + onLoad={() => { + setNextSlidePrerendered(true); + setPrerenderTime(Date.now()); + }} + /> + </div> + </div> + ); +} diff --git a/web/apps/cast/src/components/Theatre/PhotoAuditorium.tsx b/web/apps/cast/src/components/Theatre/PhotoAuditorium.tsx index dc5a18f0b9..0042dfe953 100644 --- a/web/apps/cast/src/components/Theatre/PhotoAuditorium.tsx +++ b/web/apps/cast/src/components/Theatre/PhotoAuditorium.tsx @@ -20,9 +20,9 @@ export default function PhotoAuditorium({ if (nextSlidePrerendered) { const elapsedTime = prerenderTime ? Date.now() - prerenderTime : 0; - const delayTime = Math.max(5000 - elapsedTime, 0); + const delayTime = Math.max(10000 - elapsedTime, 0); - if (elapsedTime >= 5000) { + if (elapsedTime >= 10000) { setShowPreloadedNextSlide(true); } else { timeout = setTimeout(() => { diff --git a/web/apps/cast/src/components/TimerBar.tsx b/web/apps/cast/src/components/TimerBar.tsx deleted file mode 100644 index 7f4d021711..0000000000 --- a/web/apps/cast/src/components/TimerBar.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { useEffect, useState } from "react"; - -export default function TimerBar({ percentage }: { percentage: number }) { - const okColor = "#75C157"; - const warningColor = "#FFC000"; - const lateColor = "#FF0000"; - - const [backgroundColor, setBackgroundColor] = useState(okColor); - - useEffect(() => { - if (percentage >= 40) { - setBackgroundColor(okColor); - } else if (percentage >= 20) { - setBackgroundColor(warningColor); - } else { - setBackgroundColor(lateColor); - } - }, [percentage]); - - return ( - <div - style={{ - width: `${percentage}%`, // Set the width based on the time left - height: "10px", // Same as the border thickness - backgroundColor, // The color of the moving border - transition: "width 1s linear", // Smooth transition for the width change - }} - /> - ); -} diff --git a/web/apps/cast/src/constants/apps.ts b/web/apps/cast/src/constants/apps.ts deleted file mode 100644 index f8c3f96575..0000000000 --- a/web/apps/cast/src/constants/apps.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { getAlbumsURL } from "@ente/shared/network/api"; -import { runningInBrowser } from "@ente/shared/platform"; -import { PAGES } from "constants/pages"; - -export enum APPS { - PHOTOS = "PHOTOS", - AUTH = "AUTH", - ALBUMS = "ALBUMS", -} - -export const ALLOWED_APP_PAGES = new Map([ - [APPS.ALBUMS, [PAGES.SHARED_ALBUMS, PAGES.ROOT]], - [ - APPS.AUTH, - [ - PAGES.ROOT, - PAGES.LOGIN, - PAGES.SIGNUP, - PAGES.VERIFY, - PAGES.CREDENTIALS, - PAGES.RECOVER, - PAGES.CHANGE_PASSWORD, - PAGES.GENERATE, - PAGES.AUTH, - PAGES.TWO_FACTOR_VERIFY, - PAGES.TWO_FACTOR_RECOVER, - ], - ], -]); - -export const CLIENT_PACKAGE_NAMES = new Map([ - [APPS.ALBUMS, "io.ente.albums.web"], - [APPS.PHOTOS, "io.ente.photos.web"], - [APPS.AUTH, "io.ente.auth.web"], -]); - -export const getAppNameAndTitle = () => { - if (!runningInBrowser()) { - return {}; - } - const currentURL = new URL(window.location.href); - const albumsURL = new URL(getAlbumsURL()); - if (currentURL.origin === albumsURL.origin) { - return { name: APPS.ALBUMS, title: "ente Photos" }; - } else { - return { name: APPS.PHOTOS, title: "ente Photos" }; - } -}; - -export const getAppTitle = () => { - return getAppNameAndTitle().title; -}; - -export const getAppName = () => { - return getAppNameAndTitle().name; -}; diff --git a/web/apps/cast/src/constants/cache.ts b/web/apps/cast/src/constants/cache.ts deleted file mode 100644 index cf88f63a22..0000000000 --- a/web/apps/cast/src/constants/cache.ts +++ /dev/null @@ -1,5 +0,0 @@ -export enum CACHES { - THUMBS = "thumbs", - FACE_CROPS = "face-crops", - FILES = "files", -} diff --git a/web/apps/cast/src/constants/file.ts b/web/apps/cast/src/constants/file.ts index 46065136c9..9be5746388 100644 --- a/web/apps/cast/src/constants/file.ts +++ b/web/apps/cast/src/constants/file.ts @@ -1,14 +1,3 @@ -export const MIN_EDITED_CREATION_TIME = new Date(1800, 0, 1); -export const MAX_EDITED_CREATION_TIME = new Date(); - -export const MAX_EDITED_FILE_NAME_LENGTH = 100; -export const MAX_CAPTION_SIZE = 5000; - -export const TYPE_HEIC = "heic"; -export const TYPE_HEIF = "heif"; -export const TYPE_JPEG = "jpeg"; -export const TYPE_JPG = "jpg"; - export enum FILE_TYPE { IMAGE, VIDEO, @@ -29,15 +18,3 @@ export const RAW_FORMATS = [ "dng", "tif", ]; -export const SUPPORTED_RAW_FORMATS = [ - "heic", - "rw2", - "tiff", - "arw", - "cr3", - "cr2", - "nef", - "psd", - "dng", - "tif", -]; diff --git a/web/apps/cast/src/constants/gallery.ts b/web/apps/cast/src/constants/gallery.ts deleted file mode 100644 index 9865d2e809..0000000000 --- a/web/apps/cast/src/constants/gallery.ts +++ /dev/null @@ -1,15 +0,0 @@ -export const GAP_BTW_TILES = 4; -export const DATE_CONTAINER_HEIGHT = 48; -export const SIZE_AND_COUNT_CONTAINER_HEIGHT = 72; -export const IMAGE_CONTAINER_MAX_HEIGHT = 180; -export const IMAGE_CONTAINER_MAX_WIDTH = 180; -export const MIN_COLUMNS = 4; -export const SPACE_BTW_DATES = 44; -export const SPACE_BTW_DATES_TO_IMAGE_CONTAINER_WIDTH_RATIO = 0.244; - -export enum PLAN_PERIOD { - MONTH = "month", - YEAR = "year", -} - -export const SYNC_INTERVAL_IN_MICROSECONDS = 1000 * 60 * 5; // 5 minutes diff --git a/web/apps/cast/src/constants/pages.ts b/web/apps/cast/src/constants/pages.ts deleted file mode 100644 index af532801d8..0000000000 --- a/web/apps/cast/src/constants/pages.ts +++ /dev/null @@ -1,20 +0,0 @@ -export enum PAGES { - CHANGE_EMAIL = "/change-email", - CHANGE_PASSWORD = "/change-password", - CREDENTIALS = "/credentials", - GALLERY = "/gallery", - GENERATE = "/generate", - LOGIN = "/login", - RECOVER = "/recover", - SIGNUP = "/signup", - TWO_FACTOR_SETUP = "/two-factor/setup", - TWO_FACTOR_VERIFY = "/two-factor/verify", - TWO_FACTOR_RECOVER = "/two-factor/recover", - VERIFY = "/verify", - ROOT = "/", - SHARED_ALBUMS = "/shared-albums", - // ML_DEBUG = '/ml-debug', - DEDUPLICATE = "/deduplicate", - // AUTH page is used to show (auth)enticator codes - AUTH = "/auth", -} diff --git a/web/apps/cast/src/constants/upload.ts b/web/apps/cast/src/constants/upload.ts index bc6006e468..63d044fb49 100644 --- a/web/apps/cast/src/constants/upload.ts +++ b/web/apps/cast/src/constants/upload.ts @@ -1,11 +1,5 @@ -import { ENCRYPTION_CHUNK_SIZE } from "@ente/shared/crypto/constants"; import { FILE_TYPE } from "constants/file"; -import { - FileTypeInfo, - ImportSuggestion, - Location, - ParsedExtractedMetadata, -} from "types/upload"; +import { FileTypeInfo } from "types/upload"; // list of format that were missed by type-detection for some files. export const WHITELISTED_FILE_FORMATS: FileTypeInfo[] = [ @@ -45,98 +39,3 @@ export const WHITELISTED_FILE_FORMATS: FileTypeInfo[] = [ ]; export const KNOWN_NON_MEDIA_FORMATS = ["xmp", "html", "txt"]; - -export const EXIFLESS_FORMATS = ["gif", "bmp"]; - -// this is the chunk size of the un-encrypted file which is read and encrypted before uploading it as a single part. -export const MULTIPART_PART_SIZE = 20 * 1024 * 1024; - -export const FILE_READER_CHUNK_SIZE = ENCRYPTION_CHUNK_SIZE; - -export const FILE_CHUNKS_COMBINED_FOR_A_UPLOAD_PART = Math.floor( - MULTIPART_PART_SIZE / FILE_READER_CHUNK_SIZE, -); - -export const RANDOM_PERCENTAGE_PROGRESS_FOR_PUT = () => 90 + 10 * Math.random(); - -export const NULL_LOCATION: Location = { latitude: null, longitude: null }; - -export enum UPLOAD_STAGES { - START, - READING_GOOGLE_METADATA_FILES, - EXTRACTING_METADATA, - UPLOADING, - CANCELLING, - FINISH, -} - -export enum UPLOAD_STRATEGY { - SINGLE_COLLECTION, - COLLECTION_PER_FOLDER, -} - -export enum UPLOAD_RESULT { - FAILED, - ALREADY_UPLOADED, - UNSUPPORTED, - BLOCKED, - TOO_LARGE, - LARGER_THAN_AVAILABLE_STORAGE, - UPLOADED, - UPLOADED_WITH_STATIC_THUMBNAIL, - ADDED_SYMLINK, -} - -export enum PICKED_UPLOAD_TYPE { - FILES = "files", - FOLDERS = "folders", - ZIPS = "zips", -} - -export const MAX_FILE_SIZE_SUPPORTED = 4 * 1024 * 1024 * 1024; // 4 GB - -export const LIVE_PHOTO_ASSET_SIZE_LIMIT = 20 * 1024 * 1024; // 20MB - -export const NULL_EXTRACTED_METADATA: ParsedExtractedMetadata = { - location: NULL_LOCATION, - creationTime: null, - width: null, - height: null, -}; - -export const A_SEC_IN_MICROSECONDS = 1e6; - -export const DEFAULT_IMPORT_SUGGESTION: ImportSuggestion = { - rootFolderName: "", - hasNestedFolders: false, - hasRootLevelFileWithFolder: false, -}; - -export const BLACK_THUMBNAIL_BASE64 = - "/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAEBAQEBAQEB" + - "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/2wBDAQEBAQEBAQ" + - "EBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/wAARC" + - "ACWASwDAREAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUF" + - "BAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk" + - "6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztL" + - "W2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAA" + - "AAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVY" + - "nLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImK" + - "kpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oAD" + - "AMBAAIRAxEAPwD/AD/6ACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKA" + - "CgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACg" + - "AoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKAC" + - "gAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAo" + - "AKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACg" + - "AoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACg" + - "AoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKA" + - "CgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKA" + - "CgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoA" + - "KACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACg" + - "AoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAo" + - "AKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKA" + - "CgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAK" + - "ACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoA" + - "KACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAo" + - "AKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAo" + - "AKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgAoAKACgD/9k="; diff --git a/web/apps/cast/src/constants/urls.ts b/web/apps/cast/src/constants/urls.ts deleted file mode 100644 index b5b453c312..0000000000 --- a/web/apps/cast/src/constants/urls.ts +++ /dev/null @@ -1,19 +0,0 @@ -export const ENTE_WEBSITE_LINK = "https://ente.io"; - -export const ML_BLOG_LINK = "https://ente.io/blog/desktop-ml-beta"; - -export const FACE_SEARCH_PRIVACY_POLICY_LINK = - "https://ente.io/privacy#8-biometric-information-privacy-policy"; - -export const SUPPORT_EMAIL = "support@ente.io"; - -export const APP_DOWNLOAD_URL = "https://ente.io/download/desktop"; - -export const FEEDBACK_EMAIL = "feedback@ente.io"; - -export const DELETE_ACCOUNT_EMAIL = "account-deletion@ente.io"; - -export const WEB_ROADMAP_URL = "https://github.com/ente-io/ente/discussions"; - -export const DESKTOP_ROADMAP_URL = - "https://github.com/ente-io/ente/discussions"; diff --git a/web/apps/cast/src/pages/slideshow.tsx b/web/apps/cast/src/pages/slideshow.tsx index a49d497de0..3251b26aba 100644 --- a/web/apps/cast/src/pages/slideshow.tsx +++ b/web/apps/cast/src/pages/slideshow.tsx @@ -59,21 +59,12 @@ export default function Slideshow() { } }; - const init = async () => { - try { - const castToken = window.localStorage.getItem("castToken"); - setCastToken(castToken); - } catch (e) { - logError(e, "error during sync"); - router.push("/"); - } - }; - useEffect(() => { if (castToken) { const intervalId = setInterval(() => { syncCastFiles(castToken); - }, 5000); + }, 10000); + syncCastFiles(castToken); return () => clearInterval(intervalId); } @@ -105,7 +96,20 @@ export default function Slideshow() { const router = useRouter(); useEffect(() => { - init(); + 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) { + logError(e, "error during sync"); + router.push("/"); + } }, []); useEffect(() => { diff --git a/web/apps/cast/src/services/InMemoryStore.ts b/web/apps/cast/src/services/InMemoryStore.ts deleted file mode 100644 index 88e77b8699..0000000000 --- a/web/apps/cast/src/services/InMemoryStore.ts +++ /dev/null @@ -1,31 +0,0 @@ -export enum MS_KEYS { - SRP_CONFIGURE_IN_PROGRESS = "srpConfigureInProgress", - REDIRECT_URL = "redirectUrl", -} - -type StoreType = Map<Partial<MS_KEYS>, any>; - -class InMemoryStore { - private store: StoreType = new Map(); - - get(key: MS_KEYS) { - return this.store.get(key); - } - - set(key: MS_KEYS, value: any) { - this.store.set(key, value); - } - - delete(key: MS_KEYS) { - this.store.delete(key); - } - - has(key: MS_KEYS) { - return this.store.has(key); - } - clear() { - this.store.clear(); - } -} - -export default new InMemoryStore(); diff --git a/web/apps/cast/src/services/cache/cacheStorageFactory.ts b/web/apps/cast/src/services/cache/cacheStorageFactory.ts deleted file mode 100644 index fd979e2f9c..0000000000 --- a/web/apps/cast/src/services/cache/cacheStorageFactory.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { LimitedCacheStorage } from "types/cache/index"; -// import { ElectronCacheStorage } from 'services/electron/cache'; -// import { runningInElectron, runningInWorker } from 'utils/common'; -// import { WorkerElectronCacheStorageService } from 'services/workerElectronCache/service'; - -class cacheStorageFactory { - // workerElectronCacheStorageServiceInstance: WorkerElectronCacheStorageService; - getCacheStorage(): LimitedCacheStorage { - // if (runningInElectron()) { - // if (runningInWorker()) { - // if (!this.workerElectronCacheStorageServiceInstance) { - // // this.workerElectronCacheStorageServiceInstance = - // // new WorkerElectronCacheStorageService(); - // } - // return this.workerElectronCacheStorageServiceInstance; - // } else { - // // return ElectronCacheStorage; - // } - // } else { - return transformBrowserCacheStorageToLimitedCacheStorage(caches); - // } - } -} - -export const CacheStorageFactory = new cacheStorageFactory(); - -function transformBrowserCacheStorageToLimitedCacheStorage( - caches: CacheStorage, -): LimitedCacheStorage { - return { - async open(cacheName) { - const cache = await caches.open(cacheName); - return { - match: cache.match.bind(cache), - put: cache.put.bind(cache), - delete: cache.delete.bind(cache), - }; - }, - delete: caches.delete.bind(caches), - }; -} diff --git a/web/apps/cast/src/services/cache/cacheStorageService.ts b/web/apps/cast/src/services/cache/cacheStorageService.ts deleted file mode 100644 index 391aefb552..0000000000 --- a/web/apps/cast/src/services/cache/cacheStorageService.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { logError } from "@ente/shared/sentry"; -import { CacheStorageFactory } from "./cacheStorageFactory"; - -const SecurityError = "SecurityError"; -const INSECURE_OPERATION = "The operation is insecure."; -async function openCache(cacheName: string) { - try { - return await CacheStorageFactory.getCacheStorage().open(cacheName); - } catch (e) { - // ignoring insecure operation error, as it is thrown in incognito mode in firefox - if (e.name === SecurityError && e.message === INSECURE_OPERATION) { - // no-op - } else { - // log and ignore, we don't want to break the caller flow, when cache is not available - logError(e, "openCache failed"); - } - } -} -async function deleteCache(cacheName: string) { - try { - return await CacheStorageFactory.getCacheStorage().delete(cacheName); - } catch (e) { - // ignoring insecure operation error, as it is thrown in incognito mode in firefox - if (e.name === SecurityError && e.message === INSECURE_OPERATION) { - // no-op - } else { - // log and ignore, we don't want to break the caller flow, when cache is not available - logError(e, "deleteCache failed"); - } - } -} - -export const CacheStorageService = { open: openCache, delete: deleteCache }; diff --git a/web/apps/cast/src/services/castDownloadManager.ts b/web/apps/cast/src/services/castDownloadManager.ts index b56aec928c..a99f0481da 100644 --- a/web/apps/cast/src/services/castDownloadManager.ts +++ b/web/apps/cast/src/services/castDownloadManager.ts @@ -1,162 +1,14 @@ -import { EnteFile } from "types/file"; -import { - createTypedObjectURL, - generateStreamFromArrayBuffer, - getRenderableFileURL, -} from "utils/file"; - import { CustomError } from "@ente/shared/error"; import HTTPService from "@ente/shared/network/HTTPService"; -import { getCastFileURL, getCastThumbnailURL } from "@ente/shared/network/api"; -import { logError } from "@ente/shared/sentry"; -import { CACHES } from "constants/cache"; +import { getCastFileURL } from "@ente/shared/network/api"; import { FILE_TYPE } from "constants/file"; -import { LimitedCache } from "types/cache"; +import { EnteFile } from "types/file"; import ComlinkCryptoWorker from "utils/comlink/ComlinkCryptoWorker"; -import { CacheStorageService } from "./cache/cacheStorageService"; +import { generateStreamFromArrayBuffer } from "utils/file"; class CastDownloadManager { - private fileObjectURLPromise = new Map< - string, - Promise<{ original: string[]; converted: string[] }> - >(); - private thumbnailObjectURLPromise = new Map<number, Promise<string>>(); - - private fileDownloadProgress = new Map<number, number>(); - - private progressUpdater: (value: Map<number, number>) => void; - - setProgressUpdater(progressUpdater: (value: Map<number, number>) => void) { - this.progressUpdater = progressUpdater; - } - - private async getThumbnailCache() { - try { - const thumbnailCache = await CacheStorageService.open( - CACHES.THUMBS, - ); - return thumbnailCache; - } catch (e) { - return null; - // ignore - } - } - - public async getCachedThumbnail( - file: EnteFile, - thumbnailCache?: LimitedCache, - ) { - try { - if (!thumbnailCache) { - thumbnailCache = await this.getThumbnailCache(); - } - const cacheResp: Response = await thumbnailCache?.match( - file.id.toString(), - ); - - if (cacheResp) { - return URL.createObjectURL(await cacheResp.blob()); - } - return null; - } catch (e) { - logError(e, "failed to get cached thumbnail"); - throw e; - } - } - - public async getThumbnail(file: EnteFile, castToken: string) { - try { - if (!this.thumbnailObjectURLPromise.has(file.id)) { - const downloadPromise = async () => { - const thumbnailCache = await this.getThumbnailCache(); - const cachedThumb = await this.getCachedThumbnail( - file, - thumbnailCache, - ); - if (cachedThumb) { - return cachedThumb; - } - - const thumb = await this.downloadThumb(castToken, file); - const thumbBlob = new Blob([thumb]); - try { - await thumbnailCache?.put( - file.id.toString(), - new Response(thumbBlob), - ); - } catch (e) { - // TODO: handle storage full exception. - } - return URL.createObjectURL(thumbBlob); - }; - this.thumbnailObjectURLPromise.set(file.id, downloadPromise()); - } - - return await this.thumbnailObjectURLPromise.get(file.id); - } catch (e) { - this.thumbnailObjectURLPromise.delete(file.id); - logError(e, "get castDownloadManager preview Failed"); - throw e; - } - } - - private downloadThumb = async (castToken: string, file: EnteFile) => { - const resp = await HTTPService.get( - getCastThumbnailURL(file.id), - null, - { - "X-Cast-Access-Token": castToken, - }, - { responseType: "arraybuffer" }, - ); - if (typeof resp.data === "undefined") { - throw Error(CustomError.REQUEST_FAILED); - } - const cryptoWorker = await ComlinkCryptoWorker.getInstance(); - const decrypted = await cryptoWorker.decryptThumbnail( - new Uint8Array(resp.data), - await cryptoWorker.fromB64(file.thumbnail.decryptionHeader), - file.key, - ); - return decrypted; - }; - - getFile = async (file: EnteFile, castToken: string, forPreview = false) => { - const fileKey = forPreview ? `${file.id}_preview` : `${file.id}`; - try { - const getFilePromise = async () => { - const fileStream = await this.downloadFile(castToken, file); - const fileBlob = await new Response(fileStream).blob(); - if (forPreview) { - return await getRenderableFileURL(file, fileBlob); - } else { - const fileURL = await createTypedObjectURL( - fileBlob, - file.metadata.title, - ); - return { converted: [fileURL], original: [fileURL] }; - } - }; - - if (!this.fileObjectURLPromise.get(fileKey)) { - this.fileObjectURLPromise.set(fileKey, getFilePromise()); - } - const fileURLs = await this.fileObjectURLPromise.get(fileKey); - return fileURLs; - } catch (e) { - this.fileObjectURLPromise.delete(fileKey); - logError(e, "castDownloadManager failed to get file"); - throw e; - } - }; - - public async getCachedOriginalFile(file: EnteFile) { - return await this.fileObjectURLPromise.get(file.id.toString()); - } - async downloadFile(castToken: string, file: EnteFile) { const cryptoWorker = await ComlinkCryptoWorker.getInstance(); - const onDownloadProgress = this.trackDownloadProgress(file.id); if ( file.metadata.fileType === FILE_TYPE.IMAGE || @@ -187,9 +39,6 @@ class CastDownloadManager { }); const reader = resp.body.getReader(); - const contentLength = +resp.headers.get("Content-Length"); - let downloadedBytes = 0; - const stream = new ReadableStream({ async start(controller) { const decryptionHeader = await cryptoWorker.fromB64( @@ -208,11 +57,6 @@ class CastDownloadManager { reader.read().then(async ({ done, value }) => { // Is there more data to read? if (!done) { - downloadedBytes += value.byteLength; - onDownloadProgress({ - loaded: downloadedBytes, - total: contentLength, - }); const buffer = new Uint8Array( data.byteLength + value.byteLength, ); @@ -254,20 +98,6 @@ class CastDownloadManager { }); return stream; } - - trackDownloadProgress = (fileID: number) => { - return (event: { loaded: number; total: number }) => { - if (event.loaded === event.total) { - this.fileDownloadProgress.delete(fileID); - } else { - this.fileDownloadProgress.set( - fileID, - Math.round((event.loaded * 100) / event.total), - ); - } - this.progressUpdater(new Map(this.fileDownloadProgress)); - }; - }; } export default new CastDownloadManager(); diff --git a/web/apps/cast/src/services/events.ts b/web/apps/cast/src/services/events.ts deleted file mode 100644 index 32306fc64d..0000000000 --- a/web/apps/cast/src/services/events.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { EventEmitter } from "eventemitter3"; - -// When registering event handlers, -// handle errors to avoid unhandled rejection or propagation to emit call - -export enum Events { - LOGOUT = "logout", - FILE_UPLOADED = "fileUploaded", - LOCAL_FILES_UPDATED = "localFilesUpdated", -} - -export const eventBus = new EventEmitter<Events>(); diff --git a/web/apps/cast/src/services/ffmpeg/ffmpegFactory.ts b/web/apps/cast/src/services/ffmpeg/ffmpegFactory.ts index b3c716d995..0f7d226c89 100644 --- a/web/apps/cast/src/services/ffmpeg/ffmpegFactory.ts +++ b/web/apps/cast/src/services/ffmpeg/ffmpegFactory.ts @@ -1,26 +1,19 @@ -// import isElectron from 'is-electron'; -// import { ElectronFFmpeg } from 'services/electron/ffmpeg'; -import { ElectronFile } from "types/upload"; import ComlinkFFmpegWorker from "utils/comlink/ComlinkFFmpegWorker"; export interface IFFmpeg { run: ( cmd: string[], - inputFile: File | ElectronFile, + inputFile: File, outputFilename: string, dontTimeout?: boolean, - ) => Promise<File | ElectronFile>; + ) => Promise<File>; } class FFmpegFactory { private client: IFFmpeg; async getFFmpegClient() { if (!this.client) { - // if (isElectron()) { - // this.client = new ElectronFFmpeg(); - // } else { this.client = await ComlinkFFmpegWorker.getInstance(); - // } } return this.client; } diff --git a/web/apps/cast/src/services/ffmpeg/ffmpegService.ts b/web/apps/cast/src/services/ffmpeg/ffmpegService.ts index 85bab9939e..325f1d66b3 100644 --- a/web/apps/cast/src/services/ffmpeg/ffmpegService.ts +++ b/web/apps/cast/src/services/ffmpeg/ffmpegService.ts @@ -4,10 +4,9 @@ import { INPUT_PATH_PLACEHOLDER, OUTPUT_PATH_PLACEHOLDER, } from "constants/ffmpeg"; -import { ElectronFile } from "types/upload"; import ffmpegFactory from "./ffmpegFactory"; -export async function convertToMP4(file: File | ElectronFile) { +export async function convertToMP4(file: File) { try { const ffmpegClient = await ffmpegFactory.getFFmpegClient(); return await ffmpegClient.run( diff --git a/web/apps/cast/src/services/heicConversionService.ts b/web/apps/cast/src/services/heicConversionService.ts deleted file mode 100644 index f11a9f4a4e..0000000000 --- a/web/apps/cast/src/services/heicConversionService.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { logError } from "@ente/shared/sentry"; -import WasmHEICConverterService from "./wasmHeicConverter/wasmHEICConverterService"; - -class HeicConversionService { - async convert(heicFileData: Blob): Promise<Blob> { - try { - return await WasmHEICConverterService.convert(heicFileData); - } catch (e) { - logError(e, "failed to convert heic file"); - throw e; - } - } -} -export default new HeicConversionService(); diff --git a/web/apps/cast/src/services/livePhotoService.ts b/web/apps/cast/src/services/livePhotoService.ts index 4d96e812cc..789234bd3e 100644 --- a/web/apps/cast/src/services/livePhotoService.ts +++ b/web/apps/cast/src/services/livePhotoService.ts @@ -30,16 +30,3 @@ export const decodeLivePhoto = async (file: EnteFile, zipBlob: Blob) => { } return livePhoto; }; - -export const encodeLivePhoto = async (livePhoto: LivePhoto) => { - const zip = new JSZip(); - zip.file( - "image" + getFileExtensionWithDot(livePhoto.imageNameTitle), - livePhoto.image, - ); - zip.file( - "video" + getFileExtensionWithDot(livePhoto.videoNameTitle), - livePhoto.video, - ); - return await zip.generateAsync({ type: "uint8array" }); -}; diff --git a/web/apps/cast/src/services/readerService.ts b/web/apps/cast/src/services/readerService.ts index 344fd9f202..7682f15802 100644 --- a/web/apps/cast/src/services/readerService.ts +++ b/web/apps/cast/src/services/readerService.ts @@ -1,10 +1,7 @@ import { logError } from "@ente/shared/sentry"; import { convertBytesToHumanReadable } from "@ente/shared/utils/size"; -import { ElectronFile } from "types/upload"; -export async function getUint8ArrayView( - file: Blob | ElectronFile, -): Promise<Uint8Array> { +export async function getUint8ArrayView(file: Blob): Promise<Uint8Array> { try { return new Uint8Array(await file.arrayBuffer()); } catch (e) { @@ -14,80 +11,3 @@ export async function getUint8ArrayView( throw e; } } - -export function getFileStream(file: File, chunkSize: number) { - const fileChunkReader = fileChunkReaderMaker(file, chunkSize); - - const stream = new ReadableStream<Uint8Array>({ - async pull(controller: ReadableStreamDefaultController) { - const chunk = await fileChunkReader.next(); - if (chunk.done) { - controller.close(); - } else { - controller.enqueue(chunk.value); - } - }, - }); - const chunkCount = Math.ceil(file.size / chunkSize); - return { - stream, - chunkCount, - }; -} - -export async function getElectronFileStream( - file: ElectronFile, - chunkSize: number, -) { - const chunkCount = Math.ceil(file.size / chunkSize); - return { - stream: await file.stream(), - chunkCount, - }; -} - -async function* fileChunkReaderMaker(file: File, chunkSize: number) { - let offset = 0; - while (offset < file.size) { - const blob = file.slice(offset, chunkSize + offset); - const fileChunk = await getUint8ArrayView(blob); - yield fileChunk; - offset += chunkSize; - } - return null; -} - -// depreciated -// eslint-disable-next-line @typescript-eslint/no-unused-vars -async function getUint8ArrayViewOld( - reader: FileReader, - file: Blob, -): Promise<Uint8Array> { - return await new Promise((resolve, reject) => { - reader.onabort = () => - reject( - Error( - `file reading was aborted, file size= ${convertBytesToHumanReadable( - file.size, - )}`, - ), - ); - reader.onerror = () => - reject( - Error( - `file reading has failed, file size= ${convertBytesToHumanReadable( - file.size, - )} , reason= ${reader.error}`, - ), - ); - reader.onload = () => { - // Do whatever you want with the file contents - const result = - typeof reader.result === "string" - ? new TextEncoder().encode(reader.result) - : new Uint8Array(reader.result); - resolve(result); - }; - reader.readAsArrayBuffer(file); - }); -} diff --git a/web/apps/cast/src/services/typeDetectionService.ts b/web/apps/cast/src/services/typeDetectionService.ts index c280baf510..826253ce47 100644 --- a/web/apps/cast/src/services/typeDetectionService.ts +++ b/web/apps/cast/src/services/typeDetectionService.ts @@ -6,37 +6,25 @@ import { KNOWN_NON_MEDIA_FORMATS, WHITELISTED_FILE_FORMATS, } from "constants/upload"; -import FileType, { FileTypeResult } from "file-type"; -import { ElectronFile, FileTypeInfo } from "types/upload"; +import FileType from "file-type"; +import { FileTypeInfo } from "types/upload"; import { getFileExtension } from "utils/file"; import { getUint8ArrayView } from "./readerService"; -function getFileSize(file: File | ElectronFile) { - return file.size; -} - const TYPE_VIDEO = "video"; const TYPE_IMAGE = "image"; const CHUNK_SIZE_FOR_TYPE_DETECTION = 4100; -export async function getFileType( - receivedFile: File | ElectronFile, -): Promise<FileTypeInfo> { +export async function getFileType(receivedFile: File): Promise<FileTypeInfo> { try { let fileType: FILE_TYPE; - let typeResult: FileTypeResult; - - if (receivedFile instanceof File) { - typeResult = await extractFileType(receivedFile); - } else { - typeResult = await extractElectronFileType(receivedFile); - } + const typeResult = await extractFileType(receivedFile); const mimTypeParts: string[] = typeResult.mime?.split("/"); - if (mimTypeParts?.length !== 2) { throw Error(CustomError.INVALID_MIME_TYPE(typeResult.mime)); } + switch (mimTypeParts[0]) { case TYPE_IMAGE: fileType = FILE_TYPE.IMAGE; @@ -54,7 +42,7 @@ export async function getFileType( }; } catch (e) { const fileFormat = getFileExtension(receivedFile.name); - const fileSize = convertBytesToHumanReadable(getFileSize(receivedFile)); + const fileSize = convertBytesToHumanReadable(receivedFile.size); const whiteListedFormat = WHITELISTED_FILE_FORMATS.find( (a) => a.exactType === fileFormat, ); @@ -85,14 +73,6 @@ async function extractFileType(file: File) { return getFileTypeFromBuffer(fileDataChunk); } -async function extractElectronFileType(file: ElectronFile) { - const stream = await file.stream(); - const reader = stream.getReader(); - const { value: fileDataChunk } = await reader.read(); - await reader.cancel(); - return getFileTypeFromBuffer(fileDataChunk); -} - async function getFileTypeFromBuffer(buffer: Uint8Array) { const result = await FileType.fromBuffer(buffer); if (!result?.mime) { diff --git a/web/apps/cast/src/services/wasm/ffmpeg.ts b/web/apps/cast/src/services/wasm/ffmpeg.ts index ce6f871ed7..50ab5a5a97 100644 --- a/web/apps/cast/src/services/wasm/ffmpeg.ts +++ b/web/apps/cast/src/services/wasm/ffmpeg.ts @@ -1,10 +1,10 @@ import { addLogLine } from "@ente/shared/logging"; import { promiseWithTimeout } from "@ente/shared/promise"; import { logError } from "@ente/shared/sentry"; +import QueueProcessor from "@ente/shared/utils/queueProcessor"; +import { generateTempName } from "@ente/shared/utils/temp"; import { createFFmpeg, FFmpeg } from "ffmpeg-wasm"; -import QueueProcessor from "services/queueProcessor"; import { getUint8ArrayView } from "services/readerService"; -import { generateTempName } from "utils/temp"; const INPUT_PATH_PLACEHOLDER = "INPUT"; const FFMPEG_PLACEHOLDER = "FFMPEG"; diff --git a/web/apps/cast/src/services/wasmHeicConverter/wasmHEICConverterClient.ts b/web/apps/cast/src/services/wasmHeicConverter/wasmHEICConverterClient.ts deleted file mode 100644 index 03b390fb9a..0000000000 --- a/web/apps/cast/src/services/wasmHeicConverter/wasmHEICConverterClient.ts +++ /dev/null @@ -1,13 +0,0 @@ -import * as HeicConvert from "heic-convert"; -import { getUint8ArrayView } from "services/readerService"; - -export async function convertHEIC( - fileBlob: Blob, - format: string, -): Promise<Blob> { - const filedata = await getUint8ArrayView(fileBlob); - const result = await HeicConvert({ buffer: filedata, format }); - const convertedFileData = new Uint8Array(result); - const convertedFileBlob = new Blob([convertedFileData]); - return convertedFileBlob; -} diff --git a/web/apps/cast/src/services/wasmHeicConverter/wasmHEICConverterService.ts b/web/apps/cast/src/services/wasmHeicConverter/wasmHEICConverterService.ts deleted file mode 100644 index a49d8e4f8c..0000000000 --- a/web/apps/cast/src/services/wasmHeicConverter/wasmHEICConverterService.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { CustomError } from "@ente/shared/error"; -import { addLogLine } from "@ente/shared/logging"; -import { logError } from "@ente/shared/sentry"; -import { convertBytesToHumanReadable } from "@ente/shared/utils/size"; -import QueueProcessor from "services/queueProcessor"; -import { getDedicatedConvertWorker } from "utils/comlink/ComlinkConvertWorker"; -import { ComlinkWorker } from "utils/comlink/comlinkWorker"; -import { retryAsyncFunction } from "utils/network"; -import { DedicatedConvertWorker } from "worker/convert.worker"; - -const WORKER_POOL_SIZE = 2; -const MAX_CONVERSION_IN_PARALLEL = 1; -const WAIT_TIME_BEFORE_NEXT_ATTEMPT_IN_MICROSECONDS = [100, 100]; -const WAIT_TIME_IN_MICROSECONDS = 30 * 1000; -const BREATH_TIME_IN_MICROSECONDS = 1000; -const CONVERT_FORMAT = "JPEG"; - -class HEICConverter { - private convertProcessor = new QueueProcessor<Blob>( - MAX_CONVERSION_IN_PARALLEL, - ); - private workerPool: ComlinkWorker<typeof DedicatedConvertWorker>[] = []; - private ready: Promise<void>; - - constructor() { - this.ready = this.init(); - } - private async init() { - this.workerPool = []; - for (let i = 0; i < WORKER_POOL_SIZE; i++) { - this.workerPool.push(getDedicatedConvertWorker()); - } - } - async convert(fileBlob: Blob): Promise<Blob> { - await this.ready; - const response = this.convertProcessor.queueUpRequest(() => - retryAsyncFunction<Blob>(async () => { - const convertWorker = this.workerPool.shift(); - const worker = await convertWorker.remote; - try { - const convertedHEIC = await new Promise<Blob>( - (resolve, reject) => { - const main = async () => { - try { - const timeout = setTimeout(() => { - reject(Error("wait time exceeded")); - }, WAIT_TIME_IN_MICROSECONDS); - const startTime = Date.now(); - const convertedHEIC = - await worker.convertHEIC( - fileBlob, - CONVERT_FORMAT, - ); - addLogLine( - `originalFileSize:${convertBytesToHumanReadable( - fileBlob?.size, - )},convertedFileSize:${convertBytesToHumanReadable( - convertedHEIC?.size, - )}, heic conversion time: ${ - Date.now() - startTime - }ms `, - ); - clearTimeout(timeout); - resolve(convertedHEIC); - } catch (e) { - reject(e); - } - }; - main(); - }, - ); - if (!convertedHEIC || convertedHEIC?.size === 0) { - logError( - Error(`converted heic fileSize is Zero`), - "converted heic fileSize is Zero", - { - originalFileSize: convertBytesToHumanReadable( - fileBlob?.size ?? 0, - ), - convertedFileSize: convertBytesToHumanReadable( - convertedHEIC?.size ?? 0, - ), - }, - ); - } - await new Promise((resolve) => { - setTimeout( - () => resolve(null), - BREATH_TIME_IN_MICROSECONDS, - ); - }); - this.workerPool.push(convertWorker); - return convertedHEIC; - } catch (e) { - logError(e, "heic conversion failed"); - convertWorker.terminate(); - this.workerPool.push(getDedicatedConvertWorker()); - throw e; - } - }, WAIT_TIME_BEFORE_NEXT_ATTEMPT_IN_MICROSECONDS), - ); - try { - return await response.promise; - } catch (e) { - if (e.message === CustomError.REQUEST_CANCELLED) { - // ignore - return null; - } - throw e; - } - } -} - -export default new HEICConverter(); diff --git a/web/apps/cast/src/types/cache/index.ts b/web/apps/cast/src/types/cache/index.ts deleted file mode 100644 index 2920ece105..0000000000 --- a/web/apps/cast/src/types/cache/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -export interface LimitedCacheStorage { - open: (cacheName: string) => Promise<LimitedCache>; - delete: (cacheName: string) => Promise<boolean>; -} - -export interface LimitedCache { - match: (key: string) => Promise<Response>; - put: (key: string, data: Response) => Promise<void>; - delete: (key: string) => Promise<boolean>; -} - -export interface ProxiedLimitedCacheStorage { - open: (cacheName: string) => Promise<ProxiedWorkerLimitedCache>; - delete: (cacheName: string) => Promise<boolean>; -} -export interface ProxiedWorkerLimitedCache { - match: (key: string) => Promise<ArrayBuffer>; - put: (key: string, data: ArrayBuffer) => Promise<void>; - delete: (key: string) => Promise<boolean>; -} diff --git a/web/apps/cast/src/types/cast/index.ts b/web/apps/cast/src/types/cast/index.ts deleted file mode 100644 index f082e433eb..0000000000 --- a/web/apps/cast/src/types/cast/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface CastPayload { - collectionID: number; - collectionKey: string; - castToken: string; -} diff --git a/web/apps/cast/src/types/gallery/index.ts b/web/apps/cast/src/types/gallery/index.ts deleted file mode 100644 index 0216825c80..0000000000 --- a/web/apps/cast/src/types/gallery/index.ts +++ /dev/null @@ -1,57 +0,0 @@ -// import { CollectionDownloadProgressAttributes } from 'components/Collections/CollectionDownloadProgress'; -// import { CollectionSelectorAttributes } from 'components/Collections/CollectionSelector'; -// import { TimeStampListItem } from 'components/PhotoList'; -import { User } from "@ente/shared/user/types"; -import { Collection } from "types/collection"; -import { EnteFile } from "types/file"; - -export type SelectedState = { - [k: number]: boolean; - ownCount: number; - count: number; - collectionID: number; -}; -export type SetFiles = React.Dispatch<React.SetStateAction<EnteFile[]>>; -export type SetCollections = React.Dispatch<React.SetStateAction<Collection[]>>; -export type SetLoading = React.Dispatch<React.SetStateAction<boolean>>; -// export type SetCollectionSelectorAttributes = React.Dispatch< -// React.SetStateAction<CollectionSelectorAttributes> -// >; -// export type SetCollectionDownloadProgressAttributes = React.Dispatch< -// React.SetStateAction<CollectionDownloadProgressAttributes> -// >; - -export type MergedSourceURL = { - original: string; - converted: string; -}; -export enum UploadTypeSelectorIntent { - normalUpload, - import, - collectPhotos, -} -export type GalleryContextType = { - thumbs: Map<number, string>; - files: Map<number, MergedSourceURL>; - showPlanSelectorModal: () => void; - setActiveCollectionID: (collectionID: number) => void; - syncWithRemote: (force?: boolean, silent?: boolean) => Promise<void>; - setBlockingLoad: (value: boolean) => void; - setIsInSearchMode: (value: boolean) => void; - // photoListHeader: TimeStampListItem; - openExportModal: () => void; - authenticateUser: (callback: () => void) => void; - user: User; - userIDToEmailMap: Map<number, string>; - emailList: string[]; - openHiddenSection: (callback?: () => void) => void; - isClipSearchResult: boolean; -}; - -export enum CollectionSelectorIntent { - upload, - add, - move, - restore, - unhide, -} diff --git a/web/apps/cast/src/types/upload/index.ts b/web/apps/cast/src/types/upload/index.ts index 0d38f6190f..ef44b4a23f 100644 --- a/web/apps/cast/src/types/upload/index.ts +++ b/web/apps/cast/src/types/upload/index.ts @@ -3,7 +3,6 @@ import { LocalFileAttributes, } from "@ente/shared/crypto/types"; import { FILE_TYPE } from "constants/file"; -import { Collection } from "types/collection"; import { FilePublicMagicMetadata, FilePublicMagicMetadataProps, @@ -39,24 +38,6 @@ export interface Metadata { deviceFolder?: string; } -export interface Location { - latitude: number; - longitude: number; -} - -export interface ParsedMetadataJSON { - creationTime: number; - modificationTime: number; - latitude: number; - longitude: number; -} - -export interface MultipartUploadURLs { - objectKey: string; - partURLs: string[]; - completeURL: string; -} - export interface FileTypeInfo { fileType: FILE_TYPE; exactType: string; @@ -65,43 +46,6 @@ export interface FileTypeInfo { videoType?: string; } -/* - * 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<ReadableStream<Uint8Array>>; - blob: () => Promise<Blob>; - arrayBuffer: () => Promise<Uint8Array>; -} - -export interface UploadAsset { - isLivePhoto?: boolean; - file?: File | ElectronFile; - livePhotoAssets?: LivePhotoAssets; - isElectron?: boolean; -} -export interface LivePhotoAssets { - image: globalThis.File | ElectronFile; - video: globalThis.File | ElectronFile; -} - -export interface FileWithCollection extends UploadAsset { - localID: number; - collection?: Collection; - collectionID?: number; -} - -export type ParsedMetadataJSONMap = Map<string, ParsedMetadataJSON>; - export interface UploadURL { url: string; objectKey: string; diff --git a/web/apps/cast/src/types/upload/ui.ts b/web/apps/cast/src/types/upload/ui.ts deleted file mode 100644 index bce381213f..0000000000 --- a/web/apps/cast/src/types/upload/ui.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { UPLOAD_RESULT, UPLOAD_STAGES } from "constants/upload"; - -export type FileID = number; -export type FileName = string; - -export type PercentageUploaded = number; -export type UploadFileNames = Map<FileID, FileName>; - -export interface UploadCounter { - finished: number; - total: number; -} - -export interface InProgressUpload { - localFileID: FileID; - progress: PercentageUploaded; -} - -export interface FinishedUpload { - localFileID: FileID; - result: UPLOAD_RESULT; -} - -export type InProgressUploads = Map<FileID, PercentageUploaded>; - -export type FinishedUploads = Map<FileID, UPLOAD_RESULT>; - -export type SegregatedFinishedUploads = Map<UPLOAD_RESULT, FileID[]>; - -export interface ProgressUpdater { - setPercentComplete: React.Dispatch<React.SetStateAction<number>>; - setUploadCounter: React.Dispatch<React.SetStateAction<UploadCounter>>; - setUploadStage: React.Dispatch<React.SetStateAction<UPLOAD_STAGES>>; - setInProgressUploads: React.Dispatch< - React.SetStateAction<InProgressUpload[]> - >; - setFinishedUploads: React.Dispatch< - React.SetStateAction<SegregatedFinishedUploads> - >; - setUploadFilenames: React.Dispatch<React.SetStateAction<UploadFileNames>>; - setHasLivePhotos: React.Dispatch<React.SetStateAction<boolean>>; - setUploadProgressView: React.Dispatch<React.SetStateAction<boolean>>; -} diff --git a/web/apps/cast/src/utils/collection/index.ts b/web/apps/cast/src/utils/collection/index.ts deleted file mode 100644 index bd6c2791d0..0000000000 --- a/web/apps/cast/src/utils/collection/index.ts +++ /dev/null @@ -1,147 +0,0 @@ -import { LS_KEYS, getData } from "@ente/shared/storage/localStorage"; -import { User } from "@ente/shared/user/types"; -import { - CollectionSummaryType, - CollectionType, - HIDE_FROM_COLLECTION_BAR_TYPES, - OPTIONS_NOT_HAVING_COLLECTION_TYPES, -} from "constants/collection"; -import { COLLECTION_ROLE, Collection } from "types/collection"; -import { SUB_TYPE, VISIBILITY_STATE } from "types/magicMetadata"; - -export enum COLLECTION_OPS_TYPE { - ADD, - MOVE, - REMOVE, - RESTORE, - UNHIDE, -} - -export function getSelectedCollection( - collectionID: number, - collections: Collection[], -) { - return collections.find((collection) => collection.id === collectionID); -} - -export const shouldShowOptions = (type: CollectionSummaryType) => { - return !OPTIONS_NOT_HAVING_COLLECTION_TYPES.has(type); -}; -export const showEmptyTrashQuickOption = (type: CollectionSummaryType) => { - return type === CollectionSummaryType.trash; -}; -export const showDownloadQuickOption = (type: CollectionSummaryType) => { - return ( - type === CollectionSummaryType.folder || - type === CollectionSummaryType.favorites || - type === CollectionSummaryType.album || - type === CollectionSummaryType.uncategorized || - type === CollectionSummaryType.hiddenItems || - type === CollectionSummaryType.incomingShareViewer || - type === CollectionSummaryType.incomingShareCollaborator || - type === CollectionSummaryType.outgoingShare || - type === CollectionSummaryType.sharedOnlyViaLink || - type === CollectionSummaryType.archived || - type === CollectionSummaryType.pinned - ); -}; -export const showShareQuickOption = (type: CollectionSummaryType) => { - return ( - type === CollectionSummaryType.folder || - type === CollectionSummaryType.album || - type === CollectionSummaryType.outgoingShare || - type === CollectionSummaryType.sharedOnlyViaLink || - type === CollectionSummaryType.archived || - type === CollectionSummaryType.incomingShareViewer || - type === CollectionSummaryType.incomingShareCollaborator || - type === CollectionSummaryType.pinned - ); -}; -export const shouldBeShownOnCollectionBar = (type: CollectionSummaryType) => { - return !HIDE_FROM_COLLECTION_BAR_TYPES.has(type); -}; - -export const getUserOwnedCollections = (collections: Collection[]) => { - const user: User = getData(LS_KEYS.USER); - if (!user?.id) { - throw Error("user missing"); - } - return collections.filter((collection) => collection.owner.id === user.id); -}; - -export const isDefaultHiddenCollection = (collection: Collection) => - collection.magicMetadata?.data.subType === SUB_TYPE.DEFAULT_HIDDEN; - -export const isHiddenCollection = (collection: Collection) => - collection.magicMetadata?.data.visibility === VISIBILITY_STATE.HIDDEN; - -export const isQuickLinkCollection = (collection: Collection) => - collection.magicMetadata?.data.subType === SUB_TYPE.QUICK_LINK_COLLECTION; - -export function isOutgoingShare(collection: Collection, user: User): boolean { - return collection.owner.id === user.id && collection.sharees?.length > 0; -} - -export function isIncomingShare(collection: Collection, user: User) { - return collection.owner.id !== user.id; -} - -export function isIncomingViewerShare(collection: Collection, user: User) { - const sharee = collection.sharees?.find((sharee) => sharee.id === user.id); - return sharee?.role === COLLECTION_ROLE.VIEWER; -} - -export function isIncomingCollabShare(collection: Collection, user: User) { - const sharee = collection.sharees?.find((sharee) => sharee.id === user.id); - return sharee?.role === COLLECTION_ROLE.COLLABORATOR; -} - -export function isSharedOnlyViaLink(collection: Collection) { - return collection.publicURLs?.length && !collection.sharees?.length; -} - -export function isValidMoveTarget( - sourceCollectionID: number, - targetCollection: Collection, - user: User, -) { - return ( - sourceCollectionID !== targetCollection.id && - !isHiddenCollection(targetCollection) && - !isQuickLinkCollection(targetCollection) && - !isIncomingShare(targetCollection, user) - ); -} - -export function isValidReplacementAlbum( - collection: Collection, - user: User, - wantedCollectionName: string, -) { - return ( - collection.name === wantedCollectionName && - (collection.type === CollectionType.album || - collection.type === CollectionType.folder) && - !isHiddenCollection(collection) && - !isQuickLinkCollection(collection) && - !isIncomingShare(collection, user) - ); -} - -export function getCollectionNameMap( - collections: Collection[], -): Map<number, string> { - return new Map<number, string>( - collections.map((collection) => [collection.id, collection.name]), - ); -} - -export function getNonHiddenCollections( - collections: Collection[], -): Collection[] { - return collections.filter((collection) => !isHiddenCollection(collection)); -} - -export function getHiddenCollections(collections: Collection[]): Collection[] { - return collections.filter((collection) => isHiddenCollection(collection)); -} diff --git a/web/apps/cast/src/utils/comlink/ComlinkConvertWorker.ts b/web/apps/cast/src/utils/comlink/ComlinkConvertWorker.ts deleted file mode 100644 index dc15136d9f..0000000000 --- a/web/apps/cast/src/utils/comlink/ComlinkConvertWorker.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { runningInBrowser } from "@ente/shared/platform"; -import { Remote } from "comlink"; -import { DedicatedConvertWorker } from "worker/convert.worker"; -import { ComlinkWorker } from "./comlinkWorker"; - -class ComlinkConvertWorker { - private comlinkWorkerInstance: Remote<DedicatedConvertWorker>; - - async getInstance() { - if (!this.comlinkWorkerInstance) { - this.comlinkWorkerInstance = - await getDedicatedConvertWorker().remote; - } - return this.comlinkWorkerInstance; - } -} - -export const getDedicatedConvertWorker = () => { - if (runningInBrowser()) { - const cryptoComlinkWorker = new ComlinkWorker< - typeof DedicatedConvertWorker - >( - "ente-convert-worker", - new Worker(new URL("worker/convert.worker.ts", import.meta.url)), - ); - return cryptoComlinkWorker; - } -}; - -export default new ComlinkConvertWorker(); diff --git a/web/apps/cast/src/utils/comlink/comlinkWorker.ts b/web/apps/cast/src/utils/comlink/comlinkWorker.ts index 9c1aacff6b..e924a3a966 100644 --- a/web/apps/cast/src/utils/comlink/comlinkWorker.ts +++ b/web/apps/cast/src/utils/comlink/comlinkWorker.ts @@ -1,6 +1,5 @@ import { addLocalLog } from "@ente/shared/logging"; import { Remote, wrap } from "comlink"; -// import { WorkerElectronCacheStorageClient } from 'services/workerElectronCache/client'; export class ComlinkWorker<T extends new () => InstanceType<T>> { public remote: Promise<Remote<InstanceType<T>>>; @@ -17,7 +16,6 @@ export class ComlinkWorker<T extends new () => InstanceType<T>> { addLocalLog(() => `Initiated ${this.name}`); const comlink = wrap<T>(this.worker); this.remote = new comlink() as Promise<Remote<InstanceType<T>>>; - // expose(WorkerElectronCacheStorageClient, this.worker); } public terminate() { diff --git a/web/apps/cast/src/utils/file/blob.ts b/web/apps/cast/src/utils/file/blob.ts deleted file mode 100644 index cb2e8c7a22..0000000000 --- a/web/apps/cast/src/utils/file/blob.ts +++ /dev/null @@ -1,15 +0,0 @@ -export const readAsDataURL = (blob) => - new Promise<string>((resolve, reject) => { - const fileReader = new FileReader(); - fileReader.onload = () => resolve(fileReader.result as string); - fileReader.onerror = () => reject(fileReader.error); - fileReader.readAsDataURL(blob); - }); - -export const readAsText = (blob) => - new Promise<string>((resolve, reject) => { - const fileReader = new FileReader(); - fileReader.onload = () => resolve(fileReader.result as string); - fileReader.onerror = () => reject(fileReader.error); - fileReader.readAsText(blob); - }); diff --git a/web/apps/cast/src/utils/file/index.ts b/web/apps/cast/src/utils/file/index.ts index 63672c0ef9..9b9c8e21e8 100644 --- a/web/apps/cast/src/utils/file/index.ts +++ b/web/apps/cast/src/utils/file/index.ts @@ -1,14 +1,6 @@ import { logError } from "@ente/shared/sentry"; -import { - FILE_TYPE, - RAW_FORMATS, - SUPPORTED_RAW_FORMATS, - TYPE_HEIC, - TYPE_HEIF, -} from "constants/file"; +import { FILE_TYPE, RAW_FORMATS } from "constants/file"; import CastDownloadManager from "services/castDownloadManager"; -import * as ffmpegService from "services/ffmpeg/ffmpegService"; -import heicConversionService from "services/heicConversionService"; import { decodeLivePhoto } from "services/livePhotoService"; import { getFileType } from "services/typeDetectionService"; import { @@ -17,58 +9,7 @@ import { FileMagicMetadata, FilePublicMagicMetadata, } from "types/file"; -import { SelectedState } from "types/gallery"; -import { isArchivedFile } from "utils/magicMetadata"; - -import { CustomError } from "@ente/shared/error"; -import { addLocalLog, addLogLine } from "@ente/shared/logging"; -import { LS_KEYS, getData } from "@ente/shared/storage/localStorage"; -import { User } from "@ente/shared/user/types"; -import { convertBytesToHumanReadable } from "@ente/shared/utils/size"; -import isElectron from "is-electron"; -import { FileTypeInfo } from "types/upload"; import ComlinkCryptoWorker from "utils/comlink/ComlinkCryptoWorker"; -import { isPlaybackPossible } from "utils/photoFrame"; - -const WAIT_TIME_IMAGE_CONVERSION = 30 * 1000; - -export enum FILE_OPS_TYPE { - DOWNLOAD, - FIX_TIME, - ARCHIVE, - UNARCHIVE, - HIDE, - TRASH, - DELETE_PERMANENTLY, -} - -export function groupFilesBasedOnCollectionID(files: EnteFile[]) { - const collectionWiseFiles = new Map<number, EnteFile[]>(); - for (const file of files) { - if (!collectionWiseFiles.has(file.collectionID)) { - collectionWiseFiles.set(file.collectionID, []); - } - collectionWiseFiles.get(file.collectionID).push(file); - } - return collectionWiseFiles; -} - -function getSelectedFileIds(selectedFiles: SelectedState) { - const filesIDs: number[] = []; - for (const [key, val] of Object.entries(selectedFiles)) { - if (typeof val === "boolean" && val) { - filesIDs.push(Number(key)); - } - } - return new Set(filesIDs); -} -export function getSelectedFiles( - selected: SelectedState, - files: EnteFile[], -): EnteFile[] { - const selectedFilesIDs = getSelectedFileIds(selected); - return files.filter((file) => selectedFilesIDs.has(file.id)); -} export function sortFiles(files: EnteFile[], sortAsc = false) { // sort based on the time of creation time of the file, @@ -85,20 +26,6 @@ export function sortFiles(files: EnteFile[], sortAsc = false) { }); } -export function sortTrashFiles(files: EnteFile[]) { - return files.sort((a, b) => { - if (a.deleteBy === b.deleteBy) { - if (a.metadata.creationTime === b.metadata.creationTime) { - return ( - b.metadata.modificationTime - a.metadata.modificationTime - ); - } - return b.metadata.creationTime - a.metadata.creationTime; - } - return a.deleteBy - b.deleteBy; - }); -} - export async function decryptFile( file: EncryptedEnteFile, collectionKey: string, @@ -193,176 +120,6 @@ export function generateStreamFromArrayBuffer(data: Uint8Array) { }); } -export async function getRenderableFileURL(file: EnteFile, fileBlob: Blob) { - switch (file.metadata.fileType) { - case FILE_TYPE.IMAGE: { - const convertedBlob = await getRenderableImage( - file.metadata.title, - fileBlob, - ); - const { originalURL, convertedURL } = getFileObjectURLs( - fileBlob, - convertedBlob, - ); - return { - converted: [convertedURL], - original: [originalURL], - }; - } - case FILE_TYPE.LIVE_PHOTO: { - return await getRenderableLivePhotoURL(file, fileBlob); - } - case FILE_TYPE.VIDEO: { - const convertedBlob = await getPlayableVideo( - file.metadata.title, - fileBlob, - ); - const { originalURL, convertedURL } = getFileObjectURLs( - fileBlob, - convertedBlob, - ); - return { - converted: [convertedURL], - original: [originalURL], - }; - } - default: { - const previewURL = await createTypedObjectURL( - fileBlob, - file.metadata.title, - ); - return { - converted: [previewURL], - original: [previewURL], - }; - } - } -} - -async function getRenderableLivePhotoURL( - file: EnteFile, - fileBlob: Blob, -): Promise<{ original: string[]; converted: string[] }> { - const livePhoto = await decodeLivePhoto(file, fileBlob); - const imageBlob = new Blob([livePhoto.image]); - const videoBlob = new Blob([livePhoto.video]); - const convertedImageBlob = await getRenderableImage( - livePhoto.imageNameTitle, - imageBlob, - ); - const convertedVideoBlob = await getPlayableVideo( - livePhoto.videoNameTitle, - videoBlob, - true, - ); - const { originalURL: originalImageURL, convertedURL: convertedImageURL } = - getFileObjectURLs(imageBlob, convertedImageBlob); - - const { originalURL: originalVideoURL, convertedURL: convertedVideoURL } = - getFileObjectURLs(videoBlob, convertedVideoBlob); - return { - converted: [convertedImageURL, convertedVideoURL], - original: [originalImageURL, originalVideoURL], - }; -} - -export async function getPlayableVideo( - videoNameTitle: string, - videoBlob: Blob, - forceConvert = false, -) { - try { - const isPlayable = await isPlaybackPossible( - URL.createObjectURL(videoBlob), - ); - if (isPlayable && !forceConvert) { - return videoBlob; - } else { - if (!forceConvert && !isElectron()) { - return null; - } - addLogLine( - "video format not supported, converting it name:", - videoNameTitle, - ); - const mp4ConvertedVideo = await ffmpegService.convertToMP4( - new File([videoBlob], videoNameTitle), - ); - addLogLine("video successfully converted", videoNameTitle); - return new Blob([await mp4ConvertedVideo.arrayBuffer()]); - } - } catch (e) { - addLogLine("video conversion failed", videoNameTitle); - logError(e, "video conversion failed"); - return null; - } -} - -export async function getRenderableImage(fileName: string, imageBlob: Blob) { - let fileTypeInfo: FileTypeInfo; - try { - const tempFile = new File([imageBlob], fileName); - fileTypeInfo = await getFileType(tempFile); - addLocalLog(() => `file type info: ${JSON.stringify(fileTypeInfo)}`); - const { exactType } = fileTypeInfo; - let convertedImageBlob: Blob; - if (isRawFile(exactType)) { - try { - if (!isSupportedRawFormat(exactType)) { - throw Error(CustomError.UNSUPPORTED_RAW_FORMAT); - } - - if (!isElectron()) { - throw Error(CustomError.NOT_AVAILABLE_ON_WEB); - } - addLogLine( - `RawConverter called for ${fileName}-${convertBytesToHumanReadable( - imageBlob.size, - )}`, - ); - // convertedImageBlob = await imageProcessor.convertToJPEG( - // imageBlob, - // fileName - // ); - addLogLine(`${fileName} successfully converted`); - } catch (e) { - try { - if (!isFileHEIC(exactType)) { - throw e; - } - addLogLine( - `HEICConverter called for ${fileName}-${convertBytesToHumanReadable( - imageBlob.size, - )}`, - ); - convertedImageBlob = - await heicConversionService.convert(imageBlob); - addLogLine(`${fileName} successfully converted`); - } catch (e) { - throw Error(CustomError.NON_PREVIEWABLE_FILE); - } - } - return convertedImageBlob; - } else { - return imageBlob; - } - } catch (e) { - logError(e, "get Renderable Image failed", { fileTypeInfo }); - return null; - } -} - -export function isFileHEIC(exactType: string) { - return ( - exactType.toLowerCase().endsWith(TYPE_HEIC) || - exactType.toLowerCase().endsWith(TYPE_HEIF) - ); -} - -export function isRawFile(exactType: string) { - return RAW_FORMATS.includes(exactType.toLowerCase()); -} - export function isRawFileFromFileName(fileName: string) { for (const rawFormat of RAW_FORMATS) { if (fileName.toLowerCase().endsWith(rawFormat)) { @@ -372,10 +129,6 @@ export function isRawFileFromFileName(fileName: string) { return false; } -export function isSupportedRawFormat(exactType: string) { - return SUPPORTED_RAW_FORMATS.includes(exactType.toLowerCase()); -} - export function mergeMetadata(files: EnteFile[]): EnteFile[] { return files.map((file) => { if (file.pubMagicMetadata?.data.editedTime) { @@ -389,187 +142,24 @@ export function mergeMetadata(files: EnteFile[]): EnteFile[] { }); } -export async function getFileFromURL(fileURL: string) { - const fileBlob = await (await fetch(fileURL)).blob(); - const fileFile = new File([fileBlob], "temp"); - return fileFile; -} - -export function getUniqueFiles(files: EnteFile[]) { - const idSet = new Set<number>(); - const uniqueFiles = files.filter((file) => { - if (!idSet.has(file.id)) { - idSet.add(file.id); - return true; - } else { - return false; - } - }); - - return uniqueFiles; -} - -export const isImageOrVideo = (fileType: FILE_TYPE) => - [FILE_TYPE.IMAGE, FILE_TYPE.VIDEO].includes(fileType); - -export const getArchivedFiles = (files: EnteFile[]) => { - return files.filter(isArchivedFile).map((file) => file.id); -}; - -export const createTypedObjectURL = async (blob: Blob, fileName: string) => { - const type = await getFileType(new File([blob], fileName)); - return URL.createObjectURL(new Blob([blob], { type: type.mimeType })); -}; - -export const getUserOwnedFiles = (files: EnteFile[]) => { - const user: User = getData(LS_KEYS.USER); - if (!user?.id) { - throw Error("user missing"); - } - return files.filter((file) => file.ownerID === user.id); -}; - -// doesn't work on firefox -export const copyFileToClipboard = async (fileUrl: string) => { - const canvas = document.createElement("canvas"); - const canvasCTX = canvas.getContext("2d"); - const image = new Image(); - - const blobPromise = new Promise<Blob>((resolve, reject) => { - let timeout: NodeJS.Timeout = null; - try { - image.setAttribute("src", fileUrl); - image.onload = () => { - canvas.width = image.width; - canvas.height = image.height; - canvasCTX.drawImage(image, 0, 0, image.width, image.height); - canvas.toBlob( - (blob) => { - resolve(blob); - }, - "image/png", - 1, - ); - - clearTimeout(timeout); - }; - } catch (e) { - void logError(e, "failed to copy to clipboard"); - reject(e); - } finally { - clearTimeout(timeout); - } - timeout = setTimeout( - () => reject(Error(CustomError.WAIT_TIME_EXCEEDED)), - WAIT_TIME_IMAGE_CONVERSION, - ); - }); - - const { ClipboardItem } = window; - - await navigator.clipboard - .write([new ClipboardItem({ "image/png": blobPromise })]) - .catch((e) => logError(e, "failed to copy to clipboard")); -}; - -export function getLatestVersionFiles(files: EnteFile[]) { - const latestVersionFiles = new Map<string, EnteFile>(); - files.forEach((file) => { - const uid = `${file.collectionID}-${file.id}`; - if ( - !latestVersionFiles.has(uid) || - latestVersionFiles.get(uid).updationTime < file.updationTime - ) { - latestVersionFiles.set(uid, file); - } - }); - return Array.from(latestVersionFiles.values()).filter( - (file) => !file.isDeleted, - ); -} - -export function getPersonalFiles(files: EnteFile[], user: User) { - if (!user?.id) { - throw Error("user missing"); - } - return files.filter((file) => file.ownerID === user.id); -} - -export function getIDBasedSortedFiles(files: EnteFile[]) { - return files.sort((a, b) => a.id - b.id); -} - -export function constructFileToCollectionMap(files: EnteFile[]) { - const fileToCollectionsMap = new Map<number, number[]>(); - (files ?? []).forEach((file) => { - if (!fileToCollectionsMap.get(file.id)) { - fileToCollectionsMap.set(file.id, []); - } - fileToCollectionsMap.get(file.id).push(file.collectionID); - }); - return fileToCollectionsMap; -} - -export const shouldShowAvatar = (file: EnteFile, user: User) => { - if (!file || !user) { - return false; - } - // is Shared file - else if (file.ownerID !== user.id) { - return true; - } - // is public collected file - else if ( - file.ownerID === user.id && - file.pubMagicMetadata?.data?.uploaderName - ) { - return true; - } else { - return false; - } -}; - export const getPreviewableImage = async ( file: EnteFile, castToken: string, ): Promise<Blob> => { try { - let fileBlob: Blob; - const fileURL = - await CastDownloadManager.getCachedOriginalFile(file)[0]; - if (!fileURL) { - fileBlob = await new Response( - await CastDownloadManager.downloadFile(castToken, file), - ).blob(); - } else { - fileBlob = await (await fetch(fileURL)).blob(); - } + let fileBlob = await new Response( + await CastDownloadManager.downloadFile(castToken, file), + ).blob(); if (file.metadata.fileType === FILE_TYPE.LIVE_PHOTO) { const livePhoto = await decodeLivePhoto(file, fileBlob); fileBlob = new Blob([livePhoto.image]); } - const convertedBlob = await getRenderableImage( - file.metadata.title, - fileBlob, - ); - fileBlob = convertedBlob; const fileType = await getFileType( new File([fileBlob], file.metadata.title), ); - fileBlob = new Blob([fileBlob], { type: fileType.mimeType }); return fileBlob; } catch (e) { logError(e, "failed to download file"); } }; - -const getFileObjectURLs = (originalBlob: Blob, convertedBlob: Blob) => { - const originalURL = URL.createObjectURL(originalBlob); - const convertedURL = convertedBlob - ? convertedBlob === originalBlob - ? originalURL - : URL.createObjectURL(convertedBlob) - : null; - return { originalURL, convertedURL }; -}; diff --git a/web/apps/cast/src/utils/file/livePhoto.ts b/web/apps/cast/src/utils/file/livePhoto.ts deleted file mode 100644 index 7d687217ce..0000000000 --- a/web/apps/cast/src/utils/file/livePhoto.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { FILE_TYPE } from "constants/file"; -import { getFileExtension } from "utils/file"; - -const IMAGE_EXTENSIONS = [ - "heic", - "heif", - "jpeg", - "jpg", - "png", - "gif", - "bmp", - "tiff", - "webp", -]; - -const VIDEO_EXTENSIONS = [ - "mov", - "mp4", - "m4v", - "avi", - "wmv", - "flv", - "mkv", - "webm", - "3gp", - "3g2", - "avi", - "ogv", - "mpg", - "mp", -]; - -export function getFileTypeFromExtensionForLivePhotoClustering( - filename: string, -) { - const extension = getFileExtension(filename)?.toLowerCase(); - if (IMAGE_EXTENSIONS.includes(extension)) { - return FILE_TYPE.IMAGE; - } else if (VIDEO_EXTENSIONS.includes(extension)) { - return FILE_TYPE.VIDEO; - } -} diff --git a/web/apps/cast/src/utils/magicMetadata/index.ts b/web/apps/cast/src/utils/magicMetadata/index.ts deleted file mode 100644 index 7beb457727..0000000000 --- a/web/apps/cast/src/utils/magicMetadata/index.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { Collection } from "types/collection"; -import { EnteFile } from "types/file"; -import { MagicMetadataCore, VISIBILITY_STATE } from "types/magicMetadata"; -import ComlinkCryptoWorker from "utils/comlink/ComlinkCryptoWorker"; - -export function isArchivedFile(item: EnteFile): boolean { - if (!item || !item.magicMetadata || !item.magicMetadata.data) { - return false; - } - return item.magicMetadata.data.visibility === VISIBILITY_STATE.ARCHIVED; -} - -export function isArchivedCollection(item: Collection): boolean { - if (!item) { - return false; - } - - if (item.magicMetadata && item.magicMetadata.data) { - return item.magicMetadata.data.visibility === VISIBILITY_STATE.ARCHIVED; - } - - if (item.sharedMagicMetadata && item.sharedMagicMetadata.data) { - return ( - item.sharedMagicMetadata.data.visibility === - VISIBILITY_STATE.ARCHIVED - ); - } - return false; -} - -export function isPinnedCollection(item: Collection) { - if ( - !item || - !item.magicMetadata || - !item.magicMetadata.data || - typeof item.magicMetadata.data === "string" || - typeof item.magicMetadata.data.order === "undefined" - ) { - return false; - } - return item.magicMetadata.data.order !== 0; -} - -export async function updateMagicMetadata<T>( - magicMetadataUpdates: T, - originalMagicMetadata?: MagicMetadataCore<T>, - decryptionKey?: string, -): Promise<MagicMetadataCore<T>> { - const cryptoWorker = await ComlinkCryptoWorker.getInstance(); - - if (!originalMagicMetadata) { - originalMagicMetadata = getNewMagicMetadata<T>(); - } - - if (typeof originalMagicMetadata?.data === "string") { - originalMagicMetadata.data = await cryptoWorker.decryptMetadata( - originalMagicMetadata.data, - originalMagicMetadata.header, - decryptionKey, - ); - } - // copies the existing magic metadata properties of the files and updates the visibility value - // The expected behavior while updating magic metadata is to let the existing property as it is and update/add the property you want - const magicMetadataProps: T = { - ...originalMagicMetadata.data, - ...magicMetadataUpdates, - }; - - const nonEmptyMagicMetadataProps = - getNonEmptyMagicMetadataProps(magicMetadataProps); - - const magicMetadata = { - ...originalMagicMetadata, - data: nonEmptyMagicMetadataProps, - count: Object.keys(nonEmptyMagicMetadataProps).length, - }; - - return magicMetadata; -} - -export const getNewMagicMetadata = <T>(): MagicMetadataCore<T> => { - return { - version: 1, - data: null, - header: null, - count: 0, - }; -}; - -export const getNonEmptyMagicMetadataProps = <T>(magicMetadataProps: T): T => { - return Object.fromEntries( - Object.entries(magicMetadataProps).filter( - // eslint-disable-next-line @typescript-eslint/no-unused-vars - ([_, v]) => v !== null && v !== undefined, - ), - ) as T; -}; diff --git a/web/apps/cast/src/utils/network/index.ts b/web/apps/cast/src/utils/network/index.ts deleted file mode 100644 index f7bac98eca..0000000000 --- a/web/apps/cast/src/utils/network/index.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { sleep } from "@ente/shared/sleep"; - -const waitTimeBeforeNextAttemptInMilliSeconds = [2000, 5000, 10000]; - -export async function retryAsyncFunction<T>( - request: (abort?: () => void) => Promise<T>, - waitTimeBeforeNextTry?: number[], -): Promise<T> { - if (!waitTimeBeforeNextTry) { - waitTimeBeforeNextTry = waitTimeBeforeNextAttemptInMilliSeconds; - } - - for ( - let attemptNumber = 0; - attemptNumber <= waitTimeBeforeNextTry.length; - attemptNumber++ - ) { - try { - const resp = await request(); - return resp; - } catch (e) { - if (attemptNumber === waitTimeBeforeNextTry.length) { - throw e; - } - await sleep(waitTimeBeforeNextTry[attemptNumber]); - } - } -} diff --git a/web/apps/cast/src/utils/photoFrame/index.ts b/web/apps/cast/src/utils/photoFrame/index.ts deleted file mode 100644 index 0cb6fc2015..0000000000 --- a/web/apps/cast/src/utils/photoFrame/index.ts +++ /dev/null @@ -1,148 +0,0 @@ -import { logError } from "@ente/shared/sentry"; -import { FILE_TYPE } from "constants/file"; -import { EnteFile } from "types/file"; -import { MergedSourceURL } from "types/gallery"; - -const WAIT_FOR_VIDEO_PLAYBACK = 1 * 1000; - -export async function isPlaybackPossible(url: string): Promise<boolean> { - return await new Promise((resolve) => { - const t = setTimeout(() => { - resolve(false); - }, WAIT_FOR_VIDEO_PLAYBACK); - - const video = document.createElement("video"); - video.addEventListener("canplay", function () { - clearTimeout(t); - video.remove(); // Clean up the video element - // also check for duration > 0 to make sure it is not a broken video - if (video.duration > 0) { - resolve(true); - } else { - resolve(false); - } - }); - video.addEventListener("error", function () { - clearTimeout(t); - video.remove(); - resolve(false); - }); - - video.src = url; - }); -} - -export async function playVideo(livePhotoVideo, livePhotoImage) { - const videoPlaying = !livePhotoVideo.paused; - if (videoPlaying) return; - livePhotoVideo.style.opacity = 1; - livePhotoImage.style.opacity = 0; - livePhotoVideo.load(); - livePhotoVideo.play().catch(() => { - pauseVideo(livePhotoVideo, livePhotoImage); - }); -} - -export async function pauseVideo(livePhotoVideo, livePhotoImage) { - const videoPlaying = !livePhotoVideo.paused; - if (!videoPlaying) return; - livePhotoVideo.pause(); - livePhotoVideo.style.opacity = 0; - livePhotoImage.style.opacity = 1; -} - -export function updateFileMsrcProps(file: EnteFile, url: string) { - file.msrc = url; - file.isSourceLoaded = false; - file.conversionFailed = false; - file.isConverted = false; - if (file.metadata.fileType === FILE_TYPE.IMAGE) { - file.src = url; - } else { - file.html = ` - <div class = 'pswp-item-container'> - <img src="${url}"/> - </div> - `; - } -} - -export async function updateFileSrcProps( - file: EnteFile, - mergedURL: MergedSourceURL, -) { - const urls = { - original: mergedURL.original.split(","), - converted: mergedURL.converted.split(","), - }; - let originalImageURL; - let originalVideoURL; - let convertedImageURL; - let convertedVideoURL; - let originalURL; - let isConverted; - let conversionFailed; - if (file.metadata.fileType === FILE_TYPE.LIVE_PHOTO) { - [originalImageURL, originalVideoURL] = urls.original; - [convertedImageURL, convertedVideoURL] = urls.converted; - isConverted = - originalVideoURL !== convertedVideoURL || - originalImageURL !== convertedImageURL; - conversionFailed = !convertedVideoURL || !convertedImageURL; - } else if (file.metadata.fileType === FILE_TYPE.VIDEO) { - [originalVideoURL] = urls.original; - [convertedVideoURL] = urls.converted; - isConverted = originalVideoURL !== convertedVideoURL; - conversionFailed = !convertedVideoURL; - } else if (file.metadata.fileType === FILE_TYPE.IMAGE) { - [originalImageURL] = urls.original; - [convertedImageURL] = urls.converted; - isConverted = originalImageURL !== convertedImageURL; - conversionFailed = !convertedImageURL; - } else { - [originalURL] = urls.original; - isConverted = false; - conversionFailed = false; - } - - const isPlayable = !isConverted || (isConverted && !conversionFailed); - - file.w = window.innerWidth; - file.h = window.innerHeight; - file.isSourceLoaded = true; - file.originalImageURL = originalImageURL; - file.originalVideoURL = originalVideoURL; - file.isConverted = isConverted; - file.conversionFailed = conversionFailed; - - if (!isPlayable) { - return; - } - - if (file.metadata.fileType === FILE_TYPE.VIDEO) { - file.html = ` - <video controls onContextMenu="return false;"> - <source src="${convertedVideoURL}" /> - Your browser does not support the video tag. - </video> - `; - } else if (file.metadata.fileType === FILE_TYPE.LIVE_PHOTO) { - file.html = ` - <div class = 'pswp-item-container'> - <img id = "live-photo-image-${file.id}" src="${convertedImageURL}" onContextMenu="return false;"/> - <video id = "live-photo-video-${file.id}" loop muted onContextMenu="return false;"> - <source src="${convertedVideoURL}" /> - Your browser does not support the video tag. - </video> - </div> - `; - } else if (file.metadata.fileType === FILE_TYPE.IMAGE) { - file.src = convertedImageURL; - } else { - logError( - Error(`unknown file type - ${file.metadata.fileType}`), - "Unknown file type", - ); - file.src = originalURL; - } -} diff --git a/web/apps/cast/src/utils/time/format.ts b/web/apps/cast/src/utils/time/format.ts deleted file mode 100644 index 0e2dc68b52..0000000000 --- a/web/apps/cast/src/utils/time/format.ts +++ /dev/null @@ -1,78 +0,0 @@ -import i18n, { t } from "i18next"; - -const dateTimeFullFormatter1 = new Intl.DateTimeFormat(i18n.language, { - weekday: "short", - month: "short", - day: "numeric", -}); - -const dateTimeFullFormatter2 = new Intl.DateTimeFormat(i18n.language, { - year: "numeric", -}); -const dateTimeShortFormatter = new Intl.DateTimeFormat(i18n.language, { - month: "short", - day: "numeric", - year: "numeric", - hour: "2-digit", - minute: "2-digit", -}); - -const timeFormatter = new Intl.DateTimeFormat(i18n.language, { - timeStyle: "short", -}); - -export function formatDateFull(date: number | Date) { - return [dateTimeFullFormatter1, dateTimeFullFormatter2] - .map((f) => f.format(date)) - .join(" "); -} - -export function formatDate(date: number | Date) { - const withinYear = - new Date().getFullYear() === new Date(date).getFullYear(); - const dateTimeFormat2 = !withinYear ? dateTimeFullFormatter2 : null; - return [dateTimeFullFormatter1, dateTimeFormat2] - .filter((f) => !!f) - .map((f) => f.format(date)) - .join(" "); -} - -export function formatDateTimeShort(date: number | Date) { - return dateTimeShortFormatter.format(date); -} - -export function formatTime(date: number | Date) { - return timeFormatter.format(date).toUpperCase(); -} - -export function formatDateTimeFull(dateTime: number | Date): string { - return [formatDateFull(dateTime), t("at"), formatTime(dateTime)].join(" "); -} - -export function formatDateTime(dateTime: number | Date): string { - return [formatDate(dateTime), t("at"), formatTime(dateTime)].join(" "); -} - -export function formatDateRelative(date: number) { - const units = { - year: 24 * 60 * 60 * 1000 * 365, - month: (24 * 60 * 60 * 1000 * 365) / 12, - day: 24 * 60 * 60 * 1000, - hour: 60 * 60 * 1000, - minute: 60 * 1000, - second: 1000, - }; - const relativeDateFormat = new Intl.RelativeTimeFormat(i18n.language, { - localeMatcher: "best fit", - numeric: "always", - style: "long", - }); - const elapsed = date - Date.now(); // "Math.abs" accounts for both "past" & "future" scenarios - - for (const u in units) - if (Math.abs(elapsed) > units[u] || u === "second") - return relativeDateFormat.format( - Math.round(elapsed / units[u]), - u as Intl.RelativeTimeFormatUnit, - ); -} diff --git a/web/apps/cast/src/utils/time/index.ts b/web/apps/cast/src/utils/time/index.ts deleted file mode 100644 index 592d2c7afc..0000000000 --- a/web/apps/cast/src/utils/time/index.ts +++ /dev/null @@ -1,136 +0,0 @@ -export interface TimeDelta { - hours?: number; - days?: number; - months?: number; - years?: number; -} - -interface DateComponent<T = number> { - year: T; - month: T; - day: T; - hour: T; - minute: T; - second: T; -} - -export function validateAndGetCreationUnixTimeInMicroSeconds(dateTime: Date) { - if (!dateTime || isNaN(dateTime.getTime())) { - return null; - } - const unixTime = dateTime.getTime() * 1000; - //ignoring dateTimeString = "0000:00:00 00:00:00" - if (unixTime === Date.UTC(0, 0, 0, 0, 0, 0, 0) || unixTime === 0) { - return null; - } else if (unixTime > Date.now() * 1000) { - return null; - } else { - return unixTime; - } -} - -/* -generates data component for date in format YYYYMMDD-HHMMSS - */ -export function parseDateFromFusedDateString(dateTime: string) { - const dateComponent: DateComponent<number> = convertDateComponentToNumber({ - year: dateTime.slice(0, 4), - month: dateTime.slice(4, 6), - day: dateTime.slice(6, 8), - hour: dateTime.slice(9, 11), - minute: dateTime.slice(11, 13), - second: dateTime.slice(13, 15), - }); - return validateAndGetDateFromComponents(dateComponent); -} - -/* sample date format = 2018-08-19 12:34:45 - the date has six symbol separated number values - which we would extract and use to form the date - */ -export function tryToParseDateTime(dateTime: string): Date { - const dateComponent = getDateComponentsFromSymbolJoinedString(dateTime); - if (dateComponent.year?.length === 8 && dateComponent.month?.length === 6) { - // the filename has size 8 consecutive and then 6 consecutive digits - // high possibility that the it is a date in format YYYYMMDD-HHMMSS - const possibleDateTime = dateComponent.year + "-" + dateComponent.month; - return parseDateFromFusedDateString(possibleDateTime); - } - return validateAndGetDateFromComponents( - convertDateComponentToNumber(dateComponent), - ); -} - -function getDateComponentsFromSymbolJoinedString( - dateTime: string, -): DateComponent<string> { - const [year, month, day, hour, minute, second] = - dateTime.match(/\d+/g) ?? []; - - return { year, month, day, hour, minute, second }; -} - -function validateAndGetDateFromComponents( - dateComponent: DateComponent<number>, -) { - let date = getDateFromComponents(dateComponent); - if (hasTimeValues(dateComponent) && !isTimePartValid(date, dateComponent)) { - // if the date has time values but they are not valid - // then we remove the time values and try to validate the date - date = getDateFromComponents(removeTimeValues(dateComponent)); - } - if (!isDatePartValid(date, dateComponent)) { - return null; - } - return date; -} - -function isTimePartValid(date: Date, dateComponent: DateComponent<number>) { - return ( - date.getHours() === dateComponent.hour && - date.getMinutes() === dateComponent.minute && - date.getSeconds() === dateComponent.second - ); -} - -function isDatePartValid(date: Date, dateComponent: DateComponent<number>) { - return ( - date.getFullYear() === dateComponent.year && - date.getMonth() === dateComponent.month && - date.getDate() === dateComponent.day - ); -} - -function convertDateComponentToNumber( - dateComponent: DateComponent<string>, -): DateComponent<number> { - return { - year: Number(dateComponent.year), - // https://stackoverflow.com/questions/2552483/why-does-the-month-argument-range-from-0-to-11-in-javascripts-date-constructor - month: Number(dateComponent.month) - 1, - day: Number(dateComponent.day), - hour: Number(dateComponent.hour), - minute: Number(dateComponent.minute), - second: Number(dateComponent.second), - }; -} - -function getDateFromComponents(dateComponent: DateComponent<number>) { - const { year, month, day, hour, minute, second } = dateComponent; - if (hasTimeValues(dateComponent)) { - return new Date(year, month, day, hour, minute, second); - } else { - return new Date(year, month, day); - } -} - -function hasTimeValues(dateComponent: DateComponent<number>) { - const { hour, minute, second } = dateComponent; - return !isNaN(hour) && !isNaN(minute) && !isNaN(second); -} - -function removeTimeValues( - dateComponent: DateComponent<number>, -): DateComponent<number> { - return { ...dateComponent, hour: 0, minute: 0, second: 0 }; -} diff --git a/web/apps/cast/src/worker/convert.worker.ts b/web/apps/cast/src/worker/convert.worker.ts deleted file mode 100644 index a805752acf..0000000000 --- a/web/apps/cast/src/worker/convert.worker.ts +++ /dev/null @@ -1,10 +0,0 @@ -import * as Comlink from "comlink"; -import { convertHEIC } from "services/wasmHeicConverter/wasmHEICConverterClient"; - -export class DedicatedConvertWorker { - async convertHEIC(fileBlob: Blob, format: string) { - return convertHEIC(fileBlob, format); - } -} - -Comlink.expose(DedicatedConvertWorker, self); diff --git a/web/apps/payments/.eslintrc.js b/web/apps/payments/.eslintrc.js new file mode 100644 index 0000000000..cc165fa137 --- /dev/null +++ b/web/apps/payments/.eslintrc.js @@ -0,0 +1,8 @@ +module.exports = { + extends: ["@/build-config/eslintrc-typescript-react"], + parserOptions: { + tsconfigRootDir: __dirname, + }, + // TODO (MR): Figure out a way to not have to ignored the next config .js + ignorePatterns: [".eslintrc.js", "next.config.js"], +}; diff --git a/web/apps/payments/README.md b/web/apps/payments/README.md new file mode 100644 index 0000000000..e01a8b1f92 --- /dev/null +++ b/web/apps/payments/README.md @@ -0,0 +1,91 @@ +Code that runs on `payments.ente.io`. It brokers between our services and +Stripe's API for payments. + +## Development + +There are three pieces that need to be connected to have a working local setup: + +- A client app +- This web app +- Museum + +### Client app + +For the client, let us consider the Photos web app (similar configuration can be +done in the mobile client too). + +Add the following to `web/apps/photos/.env.local`: + +```env +NEXT_PUBLIC_ENTE_ENDPOINT = http://localhost:8080 +NEXT_PUBLIC_ENTE_PAYMENTS_ENDPOINT = http://localhost:3001 +``` +Then start it locally + +```sh +yarn dev:photos +``` + +This tells it to connect to the museum and payments app running on localhost. + +> For connecting from the mobile app, you'll need to run museum on a local IP +> instead localhost. If so, just replace "http://localhost:8080" with (say) +> "http://192.168.1.2:8080" wherever mentioned. + +### Payments app + +For this (payments) web app, configure it to connect to the local museum, and +use a set of (development) Stripe keys which can be found in [Stripe's developer +dashboard](https://dashboard.stripe.com). + +Add the following to +`web/apps/payments/.env.local` + +```env +NEXT_PUBLIC_ENTE_ENDPOINT = http://localhost:8080 +NEXT_PUBLIC_STRIPE_US_PUBLISHABLE_KEY = stripe_publishable_key +``` + +Then start it locally + +```sh +yarn dev:payments +``` + +### Museum + +1. Install the [stripe-cli](https://docs.stripe.com/stripe-cli) and capture the + webhook signing secret. + +2. Define this secret within your `musuem.yaml` + +3. Update the `whitelisted-redirect-urls` so that it supports redirecting to + the locally running payments app. + +Assuming that your local payments app is running on `localhost:3001`, your +`server/museum.yaml` should look as follows. + +```yaml +stripe: + us: + key: stripe_dev_key + webhook-secret: stripe_dev_webhook_secret + whitelisted-redirect-urls: ["http://localhost:3000/gallery", "http://192.168.1.2:3001/frameRedirect"] + path: + success: ?status=success&session_id={CHECKOUT_SESSION_ID} + cancel: ?status=fail&reason=canceled +``` + +Make sure you have test plans available for museum to use, by placing them in +(say) `server/data/billing/us-testing.json`. + +Finally, start museum, for example: + +``` +docker compose up +``` + +Now if you try to purchase a plan from your locally running photos web client, +it should redirect to the locally running payments app, and from there to +Stripe. Once the test purchase completes it should redirect back to the local +web client. diff --git a/web/apps/payments/next.config.js b/web/apps/payments/next.config.js new file mode 100644 index 0000000000..64a1e98678 --- /dev/null +++ b/web/apps/payments/next.config.js @@ -0,0 +1,18 @@ +// @ts-check + +/** + * Configuration for the Next.js build + * + * See also: + * - packages/next/next.config.base.js + * - https://nextjs.org/docs/pages/api-reference/next-config-js + * + * @type {import("next").NextConfig} + */ +const nextConfig = { + /* generate a static export when we run `next build` */ + output: "export", + reactStrictMode: true, +}; + +module.exports = nextConfig; diff --git a/web/apps/payments/package.json b/web/apps/payments/package.json new file mode 100644 index 0000000000..e73f26c6c8 --- /dev/null +++ b/web/apps/payments/package.json @@ -0,0 +1,10 @@ +{ + "name": "payments", + "version": "0.0.0", + "private": true, + "dependencies": { + "@/next": "*", + "@stripe/stripe-js": "^1.17.0", + "axios": "^1.6.7" + } +} diff --git a/web/apps/payments/src/components/Container.tsx b/web/apps/payments/src/components/Container.tsx new file mode 100644 index 0000000000..33ab44a6bc --- /dev/null +++ b/web/apps/payments/src/components/Container.tsx @@ -0,0 +1,9 @@ +import styled from "@emotion/styled"; + +export const Container = styled.div` + display: flex; + flex: 1; + flex-direction: column; + justify-content: center; + align-items: center; +`; diff --git a/web/apps/payments/src/components/EnteSpinner.tsx b/web/apps/payments/src/components/EnteSpinner.tsx new file mode 100644 index 0000000000..d65c736143 --- /dev/null +++ b/web/apps/payments/src/components/EnteSpinner.tsx @@ -0,0 +1,5 @@ +import * as React from "react"; + +export const Spinner: React.FC = () => { + return <div className="loading-spinner"></div>; +}; diff --git a/web/apps/payments/src/pages/404.tsx b/web/apps/payments/src/pages/404.tsx new file mode 100644 index 0000000000..4e1bd8f260 --- /dev/null +++ b/web/apps/payments/src/pages/404.tsx @@ -0,0 +1,6 @@ +import { Container } from "components/Container"; +import constants from "utils/strings"; + +export default function Home() { + return <Container>{constants.NOT_FOUND}</Container>; +} diff --git a/web/apps/payments/src/pages/_app.tsx b/web/apps/payments/src/pages/_app.tsx new file mode 100644 index 0000000000..34713277c3 --- /dev/null +++ b/web/apps/payments/src/pages/_app.tsx @@ -0,0 +1,17 @@ +import type { AppProps } from "next/app"; +import Head from "next/head"; +import constants from "utils/strings"; +import "../styles/globals.css"; + +function MyApp({ Component, pageProps }: AppProps) { + return ( + <> + <Head> + <title>{constants.TITLE} + + + + ); +} + +export default MyApp; diff --git a/web/apps/payments/src/pages/desktop-redirect.tsx b/web/apps/payments/src/pages/desktop-redirect.tsx new file mode 100644 index 0000000000..2bccc7a0a3 --- /dev/null +++ b/web/apps/payments/src/pages/desktop-redirect.tsx @@ -0,0 +1,18 @@ +import { Container } from "components/Container"; +import { EnteSpinner } from "components/EnteSpinner"; +import * as React from "react"; + +export default function DesktopRedirect() { + React.useEffect(() => { + const currentURL = new URL(window.location.href); + const desktopRedirectURL = new URL("ente://app/gallery"); + desktopRedirectURL.search = currentURL.search; + window.location.href = desktopRedirectURL.href; + }, []); + + return ( + + + + ); +} diff --git a/web/apps/payments/src/pages/index.tsx b/web/apps/payments/src/pages/index.tsx new file mode 100644 index 0000000000..93535f9c5f --- /dev/null +++ b/web/apps/payments/src/pages/index.tsx @@ -0,0 +1,42 @@ +import { Container } from "components/Container"; +import { Spinner } from "components/EnteSpinner"; +import * as React from "react"; +import { parseAndHandleRequest } from "services/billingService"; +import { CUSTOM_ERROR } from "utils/error"; +import constants from "utils/strings"; + +export default function Home() { + const [errorMessageView, setErrorMessageView] = React.useState(false); + const [loading, setLoading] = React.useState(false); + + React.useEffect(() => { + async function main() { + try { + setLoading(true); + await parseAndHandleRequest(); + } catch (e: unknown) { + if ( + e instanceof Error && + e.message === CUSTOM_ERROR.DIRECT_OPEN_WITH_NO_QUERY_PARAMS + ) { + window.location.href = "https://ente.io"; + } else { + setErrorMessageView(true); + } + } + } + // TODO: audit + // eslint-disable-next-line @typescript-eslint/no-floating-promises + main(); + }, []); + + return ( + + {errorMessageView ? ( +
{constants.SOMETHING_WENT_WRONG}
+ ) : ( + loading && + )} +
+ ); +} diff --git a/web/apps/payments/src/services/HTTPService.ts b/web/apps/payments/src/services/HTTPService.ts new file mode 100644 index 0000000000..834a18ae69 --- /dev/null +++ b/web/apps/payments/src/services/HTTPService.ts @@ -0,0 +1,185 @@ +// TODO: Audit +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/prefer-promise-reject-errors */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/consistent-indexed-object-style */ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import axios, { AxiosRequestConfig } from "axios"; + +interface IHTTPHeaders { + [headerKey: string]: any; +} + +interface IQueryPrams { + [paramName: string]: any; +} + +/** + * Service to manage all HTTP calls. + */ +class HTTPService { + constructor() { + axios.interceptors.response.use( + (response) => Promise.resolve(response), + (err) => { + if (!err.response) { + return Promise.reject(err); + } + const { response } = err; + return Promise.reject(response); + }, + ); + } + + /** + * header object to be append to all api calls. + */ + private headers: IHTTPHeaders = { + "content-type": "application/json", + }; + + /** + * Sets the headers to the given object. + */ + public setHeaders(headers: IHTTPHeaders) { + this.headers = headers; + } + + /** + * Adds a header to list of headers. + */ + public appendHeader(key: string, value: string) { + this.headers = { + ...this.headers, + [key]: value, + }; + } + + /** + * Removes the given header. + */ + public removeHeader(key: string) { + this.headers[key] = undefined; + } + + /** + * Returns axios interceptors. + */ + // eslint-disable-next-line class-methods-use-this + public getInterceptors() { + return axios.interceptors; + } + + /** + * Generic HTTP request. + * This is done so that developer can use any functionality + * provided by axios. Here, only the set headers are spread + * over what was sent in config. + */ + public async request(config: AxiosRequestConfig, customConfig?: any) { + // eslint-disable-next-line no-param-reassign + config.headers = { + ...this.headers, + ...config.headers, + }; + if (customConfig?.cancel) { + config.cancelToken = new axios.CancelToken( + (c) => (customConfig.cancel.exec = c), + ); + } + return await axios({ ...config, ...customConfig }); + } + + /** + * Get request. + */ + public get( + url: string, + params?: IQueryPrams, + headers?: IHTTPHeaders, + customConfig?: any, + ) { + return this.request( + { + headers, + method: "GET", + params, + url, + }, + customConfig, + ); + } + + /** + * Post request + */ + public post( + url: string, + data?: any, + params?: IQueryPrams, + headers?: IHTTPHeaders, + customConfig?: any, + ) { + return this.request( + { + data, + headers, + method: "POST", + params, + url, + }, + customConfig, + ); + } + + /** + * Put request + */ + public put( + url: string, + data: any, + params?: IQueryPrams, + headers?: IHTTPHeaders, + customConfig?: any, + ) { + return this.request( + { + data, + headers, + method: "PUT", + params, + url, + }, + customConfig, + ); + } + + /** + * Delete request + */ + public delete( + url: string, + data: any, + params?: IQueryPrams, + headers?: IHTTPHeaders, + customConfig?: any, + ) { + return this.request( + { + data, + headers, + method: "DELETE", + params, + url, + }, + customConfig, + ); + } +} + +// Creates a Singleton Service. +// This will help me maintain common headers / functionality +// at a central place. +export default new HTTPService(); diff --git a/web/apps/payments/src/services/billingService.ts b/web/apps/payments/src/services/billingService.ts new file mode 100644 index 0000000000..9224129d3d --- /dev/null +++ b/web/apps/payments/src/services/billingService.ts @@ -0,0 +1,287 @@ +// TODO: Audit this and other eslints +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-confusing-void-expression */ +/* eslint-disable @typescript-eslint/no-unsafe-return */ +/* eslint-disable @typescript-eslint/no-unsafe-enum-comparison */ +/* eslint-disable @typescript-eslint/no-unnecessary-condition */ + +import { loadStripe } from "@stripe/stripe-js"; +import { CUSTOM_ERROR } from "utils/error"; +import { logError } from "utils/log"; +import HTTPService from "./HTTPService"; + +const getStripePublishableKey = (stripeAccount: StripeAccountCountry) => { + if (stripeAccount === StripeAccountCountry.STRIPE_IN) { + return ( + process.env.NEXT_PUBLIC_STRIPE_IN_PUBLISHABLE_KEY ?? + "pk_live_51HAhqDK59oeucIMOiTI6MDDM2UWUbCAJXJCGsvjJhiO8nYJz38rQq5T4iyQLDMKxqEDUfU5Hopuj4U5U4dff23oT00fHvZeodC" + ); + } else if (stripeAccount === StripeAccountCountry.STRIPE_US) { + return ( + process.env.NEXT_PUBLIC_STRIPE_US_PUBLISHABLE_KEY ?? + "pk_live_51LZ9P4G1ITnQlpAnrP6pcS7NiuJo3SnJ7gibjJlMRatkrd2EY1zlMVTVQG5RkSpLPbsHQzFfnEtgHnk1PiylIFkk00tC0LWXwi" + ); + } else { + throw Error("stripe account not found"); + } +}; + +const getEndpoint = () => { + const endPoint = + process.env.NEXT_PUBLIC_ENTE_ENDPOINT ?? "https://api.ente.io"; + return endPoint; +}; +enum PAYMENT_INTENT_STATUS { + SUCCESS = "success", + REQUIRE_ACTION = "requires_action", + REQUIRE_PAYMENT_METHOD = "requires_payment_method", +} + +enum FAILURE_REASON { + // Unable to authenticate card or 3DS + // User should be showing button for fixing card via customer portal + AUTHENTICATION_FAILED = "authentication_failed", + // Card declined result in this error. Show button to the customer portal. + REQUIRE_PAYMENT_METHOD = "requires_payment_method", + STRIPE_ERROR = "stripe_error", + CANCELED = "canceled", + SERVER_ERROR = "server_error", +} + +enum STRIPE_ERROR_TYPE { + CARD_ERROR = "card_error", + AUTHENTICATION_ERROR = "authentication_error", +} + +enum STRIPE_ERROR_CODE { + AUTHENTICATION_ERROR = "payment_intent_authentication_failure", +} + +enum RESPONSE_STATUS { + success = "success", + fail = "fail", +} + +enum PaymentActionType { + Buy = "buy", + Update = "update", +} + +enum StripeAccountCountry { + STRIPE_IN = "IN", + STRIPE_US = "US", +} + +interface SubscriptionUpdateResponse { + result: { + status: PAYMENT_INTENT_STATUS; + clientSecret: string; + }; +} + +export async function parseAndHandleRequest() { + try { + const urlParams = new URLSearchParams(window.location.search); + const productID = urlParams.get("productID"); + const paymentToken = urlParams.get("paymentToken"); + const action = urlParams.get("action"); + const redirectURL = urlParams.get("redirectURL"); + if (!action && !paymentToken && !productID && !redirectURL) { + throw Error(CUSTOM_ERROR.DIRECT_OPEN_WITH_NO_QUERY_PARAMS); + } else if (!action || !paymentToken || !productID || !redirectURL) { + throw Error(CUSTOM_ERROR.MISSING_REQUIRED_QUERY_PARAM); + } + switch (action) { + case PaymentActionType.Buy: + await buyPaidSubscription(productID, paymentToken, redirectURL); + break; + case PaymentActionType.Update: + await updateSubscription(productID, paymentToken, redirectURL); + break; + default: + throw Error(CUSTOM_ERROR.INVALID_ACTION); + } + } catch (e: any) { + console.error("Error: ", JSON.stringify(e)); + if (e.message !== CUSTOM_ERROR.DIRECT_OPEN_WITH_NO_QUERY_PARAMS) { + logError(e); + } + throw e; + } +} + +async function getUserStripeAccountCountry( + paymentToken: string, +): Promise<{ stripeAccountCountry: StripeAccountCountry }> { + const response = await HTTPService.get( + `${getEndpoint()}/billing/stripe-account-country`, + undefined, + { + "X-Auth-Token": paymentToken, + }, + ); + return response.data; +} + +async function getStripe( + redirectURL: string, + stripeAccount: StripeAccountCountry, +) { + try { + const publishableKey = getStripePublishableKey(stripeAccount); + const stripe = await loadStripe(publishableKey); + + if (!stripe) { + throw Error("stripe load failed"); + } + return stripe; + } catch (e) { + logError(e, "stripe load failed"); + redirectToApp( + redirectURL, + RESPONSE_STATUS.fail, + FAILURE_REASON.STRIPE_ERROR, + ); + throw e; + } +} + +export async function buyPaidSubscription( + productID: string, + paymentToken: string, + redirectURL: string, +) { + try { + const { stripeAccountCountry } = + await getUserStripeAccountCountry(paymentToken); + const stripe = await getStripe(redirectURL, stripeAccountCountry); + const { sessionID } = await createCheckoutSession( + productID, + paymentToken, + redirectURL, + ); + await stripe.redirectToCheckout({ + sessionId: sessionID, + }); + } catch (e) { + logError(e, "subscription purchase failed"); + redirectToApp( + redirectURL, + RESPONSE_STATUS.fail, + FAILURE_REASON.SERVER_ERROR, + ); + throw e; + } +} + +async function createCheckoutSession( + productID: string, + paymentToken: string, + redirectURL: string, +): Promise<{ sessionID: string }> { + const response = await HTTPService.get( + `${getEndpoint()}/billing/stripe/checkout-session`, + { + productID, + redirectURL, + }, + { + "X-Auth-Token": paymentToken, + }, + ); + return response.data; +} + +export async function updateSubscription( + productID: string, + paymentToken: string, + redirectURL: string, +) { + try { + const { stripeAccountCountry } = + await getUserStripeAccountCountry(paymentToken); + const stripe = await getStripe(redirectURL, stripeAccountCountry); + const { result } = await subscriptionUpdateRequest( + paymentToken, + productID, + ); + switch (result.status) { + case PAYMENT_INTENT_STATUS.SUCCESS: + // subscription updated successfully + // no-op required + return redirectToApp(redirectURL, RESPONSE_STATUS.success); + + case PAYMENT_INTENT_STATUS.REQUIRE_PAYMENT_METHOD: + return redirectToApp( + redirectURL, + RESPONSE_STATUS.fail, + FAILURE_REASON.REQUIRE_PAYMENT_METHOD, + ); + case PAYMENT_INTENT_STATUS.REQUIRE_ACTION: { + const { error } = await stripe.confirmCardPayment( + result.clientSecret, + ); + if (error) { + logError( + error, + `${error.message} - subscription update failed`, + ); + if (error.type === STRIPE_ERROR_TYPE.CARD_ERROR) { + return redirectToApp( + redirectURL, + RESPONSE_STATUS.fail, + FAILURE_REASON.REQUIRE_PAYMENT_METHOD, + ); + } else if ( + error.type === STRIPE_ERROR_TYPE.AUTHENTICATION_ERROR || + error.code === STRIPE_ERROR_CODE.AUTHENTICATION_ERROR + ) { + return redirectToApp( + redirectURL, + RESPONSE_STATUS.fail, + FAILURE_REASON.AUTHENTICATION_FAILED, + ); + } else { + return redirectToApp(redirectURL, RESPONSE_STATUS.fail); + } + } else { + return redirectToApp(redirectURL, RESPONSE_STATUS.success); + } + } + } + } catch (e) { + logError(e, "subscription update failed"); + redirectToApp( + redirectURL, + RESPONSE_STATUS.fail, + FAILURE_REASON.SERVER_ERROR, + ); + throw e; + } +} + +async function subscriptionUpdateRequest( + paymentToken: string, + productID: string, +): Promise { + const response = await HTTPService.post( + `${getEndpoint()}/billing/stripe/update-subscription`, + { + productID, + }, + undefined, + { + "X-Auth-Token": paymentToken, + }, + ); + return response.data; +} + +function redirectToApp(redirectURL: string, status: string, reason?: string) { + let completePath = `${redirectURL}?status=${status}`; + if (reason) { + completePath = `${completePath}&reason=${reason}`; + } + window.location.href = completePath; +} diff --git a/web/apps/payments/src/styles/globals.css b/web/apps/payments/src/styles/globals.css new file mode 100644 index 0000000000..e75d7598d1 --- /dev/null +++ b/web/apps/payments/src/styles/globals.css @@ -0,0 +1,38 @@ +html, +body { + padding: 0; + margin: 0; + font-family: system-ui, sans-serif; + height: 100%; + flex: 1; + display: flex; + flex-direction: column; + background-color: #191919 !important; + color: #aaa !important; +} + +:is(h1, h2, h3, h4, h5, h6) { + color: #d7d7d7; +} + +#__next { + flex: 1; + display: flex; + flex-direction: column; +} + +.loading-spinner { + color: #28a745; + width: 2rem; + height: 2rem; + border: 0.25em solid currentColor; + border-right-color: transparent; + border-radius: 50%; + animation: 0.75s linear infinite spinner-border; +} + +@keyframes spinner-border { + 100% { + transform: rotate(360deg); + } +} diff --git a/web/apps/payments/src/utils/error.ts b/web/apps/payments/src/utils/error.ts new file mode 100644 index 0000000000..c38b2bf6cd --- /dev/null +++ b/web/apps/payments/src/utils/error.ts @@ -0,0 +1,5 @@ +export const CUSTOM_ERROR = { + DIRECT_OPEN_WITH_NO_QUERY_PARAMS: "direct open with no query params", + MISSING_REQUIRED_QUERY_PARAM: "missing required query param", + INVALID_ACTION: "invalid action", +}; diff --git a/web/apps/payments/src/utils/log.ts b/web/apps/payments/src/utils/log.ts new file mode 100644 index 0000000000..5a26401135 --- /dev/null +++ b/web/apps/payments/src/utils/log.ts @@ -0,0 +1,3 @@ +export const logError = (e: unknown, msg?: string) => { + console.error(msg, e); +}; diff --git a/web/apps/payments/src/utils/strings.ts b/web/apps/payments/src/utils/strings.ts new file mode 100644 index 0000000000..44ba64b8fe --- /dev/null +++ b/web/apps/payments/src/utils/strings.ts @@ -0,0 +1,7 @@ +const englishConstants = { + TITLE: "Payments | ente.io", + SOMETHING_WENT_WRONG: "Oops, something went wrong.", + NOT_FOUND: "404 | This page could not be found.", +}; + +export default englishConstants; diff --git a/web/apps/payments/tsconfig.json b/web/apps/payments/tsconfig.json new file mode 100644 index 0000000000..f40d4ddd7b --- /dev/null +++ b/web/apps/payments/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": ["dom", "dom.iterable", "esnext"], + "skipLibCheck": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "baseUrl": "./src", + "incremental": true, + "allowJs": true + }, + "include": ["next-env.d.ts", "src/**/*.ts", "src/**/*.tsx"], + "exclude": ["node_modules", "next.config.js"] +} diff --git a/web/apps/photos/.env.development b/web/apps/photos/.env.development index 548e5bbfb1..891e621806 100644 --- a/web/apps/photos/.env.development +++ b/web/apps/photos/.env.development @@ -1,7 +1,7 @@ # Sample configuration file # # All variables are commented out by default. Copy paste this into a new file -# called `.env.local` (or create a new file with that name) and add the +# called `.env.local` (or create a new empty file with that name) and add the # environment variables you want to apply during development. `.env.local` is # gitignored, so you can freely customize it for your local setup. # @@ -39,7 +39,7 @@ # The Ente API endpoint for payments related functionality # -# NEXT_PUBLIC_ENTE_PAYMENT_ENDPOINT = http://localhost:3001 +# NEXT_PUBLIC_ENTE_PAYMENTS_ENDPOINT = http://localhost:3001 # The URL for the shared albums deployment # @@ -66,7 +66,7 @@ # # Enhancement: Consider moving that into the app/ folder in this repository. # -# NEXT_PUBLIC_ENTE_FAMILY_PORTAL_ENDPOINT = http://localhost:3003 +# NEXT_PUBLIC_ENTE_FAMILY_ENDPOINT = http://localhost:3001 # The JSON which describes the expected results of our integration tests. See # `upload.test.ts` for more details of the expected format. diff --git a/web/apps/photos/.env.localhost b/web/apps/photos/.env.localhost index 136d0118b8..9fbf1bcbfc 100644 --- a/web/apps/photos/.env.localhost +++ b/web/apps/photos/.env.localhost @@ -1,8 +1,10 @@ # Develop against a server running on localhost # -# Copy this file to `.env`. Then if you run a local instance of the web client -# with `yarn dev:photos`, it will connect to a locally running instance of the -# server. Not everything will work, you might need to set other env vars (see +# Copy this file to `.env.local`. Then if you run a local instance of the web +# client with `yarn dev:photos`, it will connect to a locally running instance +# of the server. +# +# Not everything will work, you might need to set other env vars (see # `.env.development`), but it should give you a usable baseline setup. # # Equivalent CLI command using environment variables would be diff --git a/web/apps/photos/package.json b/web/apps/photos/package.json index e0098cd367..deac7ad047 100644 --- a/web/apps/photos/package.json +++ b/web/apps/photos/package.json @@ -16,10 +16,8 @@ "@tensorflow/tfjs-converter": "^4.10.0", "@tensorflow/tfjs-core": "^4.10.0", "@tensorflow/tfjs-tflite": "0.0.1-alpha.7", - "@zip.js/zip.js": "2.4.2", "bip39": "^3.0.4", "blazeface-back": "^0.0.9", - "bootstrap": "^4.5.2", "bs58": "^5.0.0", "chrono-node": "^2.2.6", "comlink": "^4.3.0", @@ -45,7 +43,7 @@ "p-queue": "^7.1.0", "photoswipe": "file:./thirdparty/photoswipe", "piexifjs": "^1.0.6", - "react-bootstrap": "^1.3.0", + "pure-react-carousel": "^1.30.1", "react-datepicker": "^4.16.0", "react-dropzone": "^11.2.4", "react-otp-input": "^2.3.1", diff --git a/web/apps/photos/public/locales/de-DE/translation.json b/web/apps/photos/public/locales/de-DE/translation.json index 9a1c4073c6..1d79b5bb7c 100644 --- a/web/apps/photos/public/locales/de-DE/translation.json +++ b/web/apps/photos/public/locales/de-DE/translation.json @@ -7,7 +7,7 @@ "HERO_SLIDE_3": "Android, iOS, Web, Desktop", "LOGIN": "Anmelden", "SIGN_UP": "Registrieren", - "NEW_USER": "Neu bei ente", + "NEW_USER": "Neu bei Ente", "EXISTING_USER": "Existierender Benutzer", "ENTER_NAME": "Name eingeben", "PUBLIC_UPLOADER_NAME_MESSAGE": "Füge einen Namen hinzu, damit deine Freunde wissen, wem sie für diese tollen Fotos zu danken haben!", @@ -278,7 +278,7 @@ "LAST_EXPORT_TIME": "Letztes Exportdatum", "EXPORT_AGAIN": "Neusynchronisation", "LOCAL_STORAGE_NOT_ACCESSIBLE": "Lokaler Speicher nicht zugänglich", - "LOCAL_STORAGE_NOT_ACCESSIBLE_MESSAGE": "Ihr Browser oder ein Addon blockiert ente vor der Speicherung von Daten im lokalen Speicher. Bitte versuchen Sie, den Browser-Modus zu wechseln und die Seite neu zu laden.", + "LOCAL_STORAGE_NOT_ACCESSIBLE_MESSAGE": "Ihr Browser oder ein Addon blockiert Ente vor der Speicherung von Daten im lokalen Speicher. Bitte versuchen Sie, den Browser-Modus zu wechseln und die Seite neu zu laden.", "SEND_OTT": "OTP senden", "EMAIl_ALREADY_OWNED": "Diese E-Mail wird bereits verwendet", "ETAGS_BLOCKED": "", @@ -291,7 +291,7 @@ "UNSUPPORTED_FILES": "Nicht unterstützte Dateien", "SUCCESSFUL_UPLOADS": "Erfolgreiche Uploads", "SKIPPED_INFO": "", - "UNSUPPORTED_INFO": "ente unterstützt diese Dateiformate noch nicht", + "UNSUPPORTED_INFO": "Ente unterstützt diese Dateiformate noch nicht", "BLOCKED_UPLOADS": "Blockierte Uploads", "SKIPPED_VIDEOS": "Übersprungene Videos", "INPROGRESS_METADATA_EXTRACTION": "In Bearbeitung", @@ -421,7 +421,7 @@ "AUTHENTICATOR_SECTION": "Authenticator", "NO_DUPLICATES_FOUND": "Du hast keine Duplikate, die gelöscht werden können", "CLUB_BY_CAPTURE_TIME": "", - "FILES": "Dateien", + "FILES": "dateien", "EACH": "", "DEDUPLICATE_BASED_ON_SIZE": "", "STOP_ALL_UPLOADS_MESSAGE": "", diff --git a/web/apps/photos/public/locales/en-US/translation.json b/web/apps/photos/public/locales/en-US/translation.json index de8d2fe2a1..b06336bf52 100644 --- a/web/apps/photos/public/locales/en-US/translation.json +++ b/web/apps/photos/public/locales/en-US/translation.json @@ -7,7 +7,7 @@ "HERO_SLIDE_3": "Android, iOS, Web, Desktop", "LOGIN": "Login", "SIGN_UP": "Signup", - "NEW_USER": "New to ente", + "NEW_USER": "New to Ente", "EXISTING_USER": "Existing user", "ENTER_NAME": "Enter name", "PUBLIC_UPLOADER_NAME_MESSAGE": "Add a name so that your friends know who to thank for these great photos!", @@ -93,7 +93,7 @@ "TRASH_FILES_TITLE": "Delete files?", "TRASH_FILE_TITLE": "Delete file?", "DELETE_FILES_TITLE": "Delete immediately?", - "DELETE_FILES_MESSAGE": "Selected files will be permanently deleted from your ente account.", + "DELETE_FILES_MESSAGE": "Selected files will be permanently deleted from your Ente account.", "DELETE": "Delete", "DELETE_OPTION": "Delete (DEL)", "FAVORITE_OPTION": "Favorite (L)", @@ -105,7 +105,7 @@ "UPLOAD_STRATEGY_COLLECTION_PER_FOLDER": "Separate albums", "SESSION_EXPIRED_MESSAGE": "Your session has expired, please login again to continue", "SESSION_EXPIRED": "Session expired", - "PASSWORD_GENERATION_FAILED": "Your browser was unable to generate a strong key that meets ente's encryption standards, please try using the mobile app or another browser", + "PASSWORD_GENERATION_FAILED": "Your browser was unable to generate a strong key that meets Ente's encryption standards, please try using the mobile app or another browser", "CHANGE_PASSWORD": "Change password", "GO_BACK": "Go back", "RECOVERY_KEY": "Recovery key", @@ -278,11 +278,11 @@ "LAST_EXPORT_TIME": "Last export time", "EXPORT_AGAIN": "Resync", "LOCAL_STORAGE_NOT_ACCESSIBLE": "Local storage not accessible", - "LOCAL_STORAGE_NOT_ACCESSIBLE_MESSAGE": "Your browser or an addon is blocking ente from saving data into local storage. please try loading this page after switching your browsing mode.", + "LOCAL_STORAGE_NOT_ACCESSIBLE_MESSAGE": "Your browser or an addon is blocking Ente from saving data into local storage. please try loading this page after switching your browsing mode.", "SEND_OTT": "Send OTP", "EMAIl_ALREADY_OWNED": "Email already taken", - "ETAGS_BLOCKED": "

We were unable to upload the following files because of your browser configuration.

Please disable any addons that might be preventing ente from using eTags to upload large files, or use our desktop app for a more reliable import experience.

", - "SKIPPED_VIDEOS_INFO": "

Presently we do not support adding videos via public links.

To share videos, please signup for ente and share with the intended recipients using their email.

", + "ETAGS_BLOCKED": "

We were unable to upload the following files because of your browser configuration.

Please disable any addons that might be preventing Ente from using eTags to upload large files, or use our desktop app for a more reliable import experience.

", + "SKIPPED_VIDEOS_INFO": "

Presently we do not support adding videos via public links.

To share videos, please signup for Ente and share with the intended recipients using their email.

", "LIVE_PHOTOS_DETECTED": "The photo and video files from your Live Photos have been merged into a single file", "RETRY_FAILED": "Retry failed uploads", "FAILED_UPLOADS": "Failed uploads ", @@ -291,7 +291,7 @@ "UNSUPPORTED_FILES": "Unsupported files", "SUCCESSFUL_UPLOADS": "Successful uploads", "SKIPPED_INFO": "Skipped these as there are files with matching names in the same album", - "UNSUPPORTED_INFO": "ente does not support these file formats yet", + "UNSUPPORTED_INFO": "Ente does not support these file formats yet", "BLOCKED_UPLOADS": "Blocked uploads", "SKIPPED_VIDEOS": "Skipped videos", "INPROGRESS_METADATA_EXTRACTION": "In progress", @@ -327,7 +327,7 @@ "RESTORE_TO_COLLECTION": "Restore to album", "EMPTY_TRASH": "Empty trash", "EMPTY_TRASH_TITLE": "Empty trash?", - "EMPTY_TRASH_MESSAGE": "These files will be permanently deleted from your ente account.", + "EMPTY_TRASH_MESSAGE": "These files will be permanently deleted from your Ente account.", "LEAVE_SHARED_ALBUM": "Yes, leave", "LEAVE_ALBUM": "Leave album", "LEAVE_SHARED_ALBUM_TITLE": "Leave shared album?", @@ -342,7 +342,7 @@ "THUMBNAIL_REPLACED": "Thumbnails compressed", "FIX_THUMBNAIL": "Compress", "FIX_THUMBNAIL_LATER": "Compress later", - "REPLACE_THUMBNAIL_NOT_STARTED": "Some of your videos thumbnails can be compressed to save space. would you like ente to compress them?", + "REPLACE_THUMBNAIL_NOT_STARTED": "Some of your videos thumbnails can be compressed to save space. would you like Ente to compress them?", "REPLACE_THUMBNAIL_COMPLETED": "Successfully compressed all thumbnails", "REPLACE_THUMBNAIL_NOOP": "You have no thumbnails that can be compressed further", "REPLACE_THUMBNAIL_COMPLETED_WITH_ERROR": "Could not compress some of your thumbnails, please retry", @@ -421,8 +421,8 @@ "AUTHENTICATOR_SECTION": "Authenticator", "NO_DUPLICATES_FOUND": "You've no duplicate files that can be cleared", "CLUB_BY_CAPTURE_TIME": "Club by capture time", - "FILES": "Files", - "EACH": "Each", + "FILES": "files", + "EACH": "each", "DEDUPLICATE_BASED_ON_SIZE": "The following files were clubbed based on their sizes, please review and delete items you believe are duplicates", "STOP_ALL_UPLOADS_MESSAGE": "Are you sure that you want to stop all the uploads in progress?", "STOP_UPLOADS_HEADER": "Stop uploads?", @@ -455,12 +455,12 @@ "WATCHED_FOLDERS": "Watched folders", "NO_FOLDERS_ADDED": "No folders added yet!", "FOLDERS_AUTOMATICALLY_MONITORED": "The folders you add here will monitored to automatically", - "UPLOAD_NEW_FILES_TO_ENTE": "Upload new files to ente", - "REMOVE_DELETED_FILES_FROM_ENTE": "Remove deleted files from ente", + "UPLOAD_NEW_FILES_TO_ENTE": "Upload new files to Ente", + "REMOVE_DELETED_FILES_FROM_ENTE": "Remove deleted files from Ente", "ADD_FOLDER": "Add folder", "STOP_WATCHING": "Stop watching", "STOP_WATCHING_FOLDER": "Stop watching folder?", - "STOP_WATCHING_DIALOG_MESSAGE": "Your existing files will not be deleted, but ente will stop automatically updating the linked ente album on changes in this folder.", + "STOP_WATCHING_DIALOG_MESSAGE": "Your existing files will not be deleted, but Ente will stop automatically updating the linked Ente album on changes in this folder.", "YES_STOP": "Yes, stop", "MONTH_SHORT": "mo", "YEAR": "year", @@ -474,18 +474,18 @@ "FREE_PLAN_OPTION_LABEL": "Continue with free trial", "FREE_PLAN_DESCRIPTION": "1 GB 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", + "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", "CONFIRM_ACCOUNT_DELETION_MESSAGE": "Your uploaded data will be scheduled for deletion, and your account will be permanently deleted.

This action is not reversible.", "AUTHENTICATE": "Authenticate", "UPLOADED_TO_SINGLE_COLLECTION": "Uploaded to single collection", "UPLOADED_TO_SEPARATE_COLLECTIONS": "Uploaded to separate collections", "NEVERMIND": "Nevermind", "UPDATE_AVAILABLE": "Update available", - "UPDATE_INSTALLABLE_MESSAGE": "A new version of ente is ready to be installed.", + "UPDATE_INSTALLABLE_MESSAGE": "A new version of Ente is ready to be installed.", "INSTALL_NOW": "Install now", "INSTALL_ON_NEXT_LAUNCH": "Install on next launch", - "UPDATE_AVAILABLE_MESSAGE": "A new version of ente has been released, but it cannot be automatically downloaded and installed.", + "UPDATE_AVAILABLE_MESSAGE": "A new version of Ente has been released, but it cannot be automatically downloaded and installed.", "DOWNLOAD_AND_INSTALL": "Download and install", "IGNORE_THIS_VERSION": "Ignore this version", "TODAY": "Today", @@ -499,13 +499,13 @@ "ML_MORE_DETAILS": "More details", "ENABLE_FACE_SEARCH": "Enable face recognition", "ENABLE_FACE_SEARCH_TITLE": "Enable face recognition?", - "ENABLE_FACE_SEARCH_DESCRIPTION": "

If you enable face recognition, ente will extract face geometry from your photos. This will happen on your device, and any generated biometric data will be end-to-encrypted.

Please click here for more details about this feature in our privacy policy

", + "ENABLE_FACE_SEARCH_DESCRIPTION": "

If you enable face recognition, Ente will extract face geometry from your photos. This will happen on your device, and any generated biometric data will be end-to-encrypted.

Please click here for more details about this feature in our privacy policy

", "DISABLE_BETA": "Pause recognition", "DISABLE_FACE_SEARCH": "Disable face recognition", "DISABLE_FACE_SEARCH_TITLE": "Disable face recognition?", "DISABLE_FACE_SEARCH_DESCRIPTION": "

Ente will stop processing face geometry.

You can reenable face recognition again if you wish, so this operation is safe.

", "ADVANCED": "Advanced", - "FACE_SEARCH_CONFIRMATION": "I understand, and wish to allow ente to process face geometry", + "FACE_SEARCH_CONFIRMATION": "I understand, and wish to allow Ente to process face geometry", "LABS": "Labs", "YOURS": "yours", "PASSPHRASE_STRENGTH_WEAK": "Password strength: Weak", @@ -597,7 +597,7 @@ "LIVE_PHOTO": "Live Photo", "CONVERT": "Convert", "CONFIRM_EDITOR_CLOSE_MESSAGE": "Are you sure you want to close the editor?", - "CONFIRM_EDITOR_CLOSE_DESCRIPTION": "Download your edited image or save a copy to ente to persist your changes.", + "CONFIRM_EDITOR_CLOSE_DESCRIPTION": "Download your edited image or save a copy to Ente to persist your changes.", "BRIGHTNESS": "Brightness", "CONTRAST": "Contrast", "SATURATION": "Saturation", @@ -610,7 +610,7 @@ "FLIP_VERTICALLY": "Flip Vertically", "FLIP_HORIZONTALLY": "Flip Horizontally", "DOWNLOAD_EDITED": "Download Edited", - "SAVE_A_COPY_TO_ENTE": "Save a copy to ente", + "SAVE_A_COPY_TO_ENTE": "Save a copy to Ente", "RESTORE_ORIGINAL": "Restore Original", "TRANSFORM": "Transform", "COLORS": "Colors", diff --git a/web/apps/photos/public/locales/es-ES/translation.json b/web/apps/photos/public/locales/es-ES/translation.json index a29165e4ee..ca288c6922 100644 --- a/web/apps/photos/public/locales/es-ES/translation.json +++ b/web/apps/photos/public/locales/es-ES/translation.json @@ -7,7 +7,7 @@ "HERO_SLIDE_3": "Android, iOS, web, computadora", "LOGIN": "Conectar", "SIGN_UP": "Registro", - "NEW_USER": "Nuevo en ente", + "NEW_USER": "Nuevo en Ente", "EXISTING_USER": "Usuario existente", "ENTER_NAME": "Introducir nombre", "PUBLIC_UPLOADER_NAME_MESSAGE": "¡Añade un nombre para que tus amigos sepan a quién dar las gracias por estas fotos geniales!", @@ -421,8 +421,8 @@ "AUTHENTICATOR_SECTION": "Autenticación", "NO_DUPLICATES_FOUND": "No tienes archivos duplicados que puedan ser borrados", "CLUB_BY_CAPTURE_TIME": "Club por tiempo de captura", - "FILES": "Archivos", - "EACH": "Cada", + "FILES": "archivos", + "EACH": "cada", "DEDUPLICATE_BASED_ON_SIZE": "Los siguientes archivos fueron organizados en base a sus tamaños, por favor revise y elimine elementos que cree que son duplicados", "STOP_ALL_UPLOADS_MESSAGE": "¿Está seguro que desea detener todas las subidas en curso?", "STOP_UPLOADS_HEADER": "Detener las subidas?", diff --git a/web/apps/photos/public/locales/fr-FR/translation.json b/web/apps/photos/public/locales/fr-FR/translation.json index 43d9590691..9ae9a3bdc2 100644 --- a/web/apps/photos/public/locales/fr-FR/translation.json +++ b/web/apps/photos/public/locales/fr-FR/translation.json @@ -7,7 +7,7 @@ "HERO_SLIDE_3": "Android, iOS, Web, Ordinateur", "LOGIN": "Connexion", "SIGN_UP": "Inscription", - "NEW_USER": "Nouveau sur ente", + "NEW_USER": "Nouveau sur Ente", "EXISTING_USER": "Utilisateur existant", "ENTER_NAME": "Saisir un nom", "PUBLIC_UPLOADER_NAME_MESSAGE": "Ajouter un nom afin que vos amis sachent qui remercier pour ces magnifiques photos!", @@ -93,7 +93,7 @@ "TRASH_FILES_TITLE": "Supprimer les fichiers ?", "TRASH_FILE_TITLE": "Supprimer le fichier ?", "DELETE_FILES_TITLE": "Supprimer immédiatement?", - "DELETE_FILES_MESSAGE": "Les fichiers sélectionnés seront définitivement supprimés de votre compte ente.", + "DELETE_FILES_MESSAGE": "Les fichiers sélectionnés seront définitivement supprimés de votre compte Ente.", "DELETE": "Supprimer", "DELETE_OPTION": "Supprimer (DEL)", "FAVORITE_OPTION": "Favori (L)", @@ -105,7 +105,7 @@ "UPLOAD_STRATEGY_COLLECTION_PER_FOLDER": "Albums séparés", "SESSION_EXPIRED_MESSAGE": "Votre session a expiré, veuillez vous reconnecter pour poursuivre", "SESSION_EXPIRED": "Session expiré", - "PASSWORD_GENERATION_FAILED": "Votre navigateur ne permet pas de générer une clé forte correspondant aux standards de chiffrement de ente, veuillez réessayer en utilisant l'appli mobile ou un autre navigateur", + "PASSWORD_GENERATION_FAILED": "Votre navigateur ne permet pas de générer une clé forte correspondant aux standards de chiffrement de Ente, veuillez réessayer en utilisant l'appli mobile ou un autre navigateur", "CHANGE_PASSWORD": "Modifier le mot de passe", "GO_BACK": "Retour", "RECOVERY_KEY": "Clé de récupération", @@ -278,10 +278,10 @@ "LAST_EXPORT_TIME": "Horaire du dernier export", "EXPORT_AGAIN": "Resynchro", "LOCAL_STORAGE_NOT_ACCESSIBLE": "Stockage local non accessible", - "LOCAL_STORAGE_NOT_ACCESSIBLE_MESSAGE": "Votre navigateur ou un complément bloque ente qui ne peut sauvegarder les données sur votre stockage local. Veuillez relancer cette page après avoir changé de mode de navigation.", + "LOCAL_STORAGE_NOT_ACCESSIBLE_MESSAGE": "Votre navigateur ou un complément bloque Ente qui ne peut sauvegarder les données sur votre stockage local. Veuillez relancer cette page après avoir changé de mode de navigation.", "SEND_OTT": "Envoyer l'OTP", "EMAIl_ALREADY_OWNED": "Cet e-mail est déjà pris", - "ETAGS_BLOCKED": "

Nosu n'avons pas pu charger les fichiers suivants à cause de la configuration de votre navigateur.

Veuillez désactiver tous les compléments qui pourraient empêcher ente d'utiliser les eTags pour charger de larges fichiers, ou bien utilisez notre appli pour ordinateurpour une meilleure expérience lors des chargements.

", + "ETAGS_BLOCKED": "

Nosu n'avons pas pu charger les fichiers suivants à cause de la configuration de votre navigateur.

Veuillez désactiver tous les compléments qui pourraient empêcher Ente d'utiliser les eTags pour charger de larges fichiers, ou bien utilisez notre appli pour ordinateurpour une meilleure expérience lors des chargements.

", "SKIPPED_VIDEOS_INFO": "

Actuellement, nous ne supportons pas l'ajout de videos via des liens publics.

Pour partager des vidéos, veuillez vous connecter àente et partager en utilisant l'e-mail concerné.

", "LIVE_PHOTOS_DETECTED": "Les fichiers photos et vidéos depuis votre espace Live Photos ont été fusionnés en un seul fichier", "RETRY_FAILED": "Réessayer les chargements ayant échoués", @@ -291,7 +291,7 @@ "UNSUPPORTED_FILES": "Fichiers non supportés", "SUCCESSFUL_UPLOADS": "Chargements réussis", "SKIPPED_INFO": "Ignorés car il y a des fichiers avec des noms identiques dans le même album", - "UNSUPPORTED_INFO": "ente ne supporte pas encore ces formats de fichiers", + "UNSUPPORTED_INFO": "Ente ne supporte pas encore ces formats de fichiers", "BLOCKED_UPLOADS": "Chargements bloqués", "SKIPPED_VIDEOS": "Vidéos ignorées", "INPROGRESS_METADATA_EXTRACTION": "En cours", @@ -327,7 +327,7 @@ "RESTORE_TO_COLLECTION": "Restaurer vers l'album", "EMPTY_TRASH": "Corbeille vide", "EMPTY_TRASH_TITLE": "Vider la corbeille ?", - "EMPTY_TRASH_MESSAGE": "Ces fichiers seront définitivement supprimés de votre compte ente.", + "EMPTY_TRASH_MESSAGE": "Ces fichiers seront définitivement supprimés de votre compte Ente.", "LEAVE_SHARED_ALBUM": "Oui, quitter", "LEAVE_ALBUM": "Quitter l'album", "LEAVE_SHARED_ALBUM_TITLE": "Quitter l'album partagé?", @@ -342,7 +342,7 @@ "THUMBNAIL_REPLACED": "Les miniatures sont compressées", "FIX_THUMBNAIL": "Compresser", "FIX_THUMBNAIL_LATER": "Compresser plus tard", - "REPLACE_THUMBNAIL_NOT_STARTED": "Certaines miniatures de vidéos peuvent être compressées pour gagner de la place. Voulez-vous que ente les compresse?", + "REPLACE_THUMBNAIL_NOT_STARTED": "Certaines miniatures de vidéos peuvent être compressées pour gagner de la place. Voulez-vous que Ente les compresse?", "REPLACE_THUMBNAIL_COMPLETED": "Toutes les miniatures ont été compressées", "REPLACE_THUMBNAIL_NOOP": "Vous n'avez aucune miniature qui peut être encore plus compressée", "REPLACE_THUMBNAIL_COMPLETED_WITH_ERROR": "Impossible de compresser certaines miniatures, veuillez réessayer", @@ -421,8 +421,8 @@ "AUTHENTICATOR_SECTION": "Authentificateur", "NO_DUPLICATES_FOUND": "Vous n'avez aucun fichier dédupliqué pouvant être nettoyé", "CLUB_BY_CAPTURE_TIME": "Durée de la capture par club", - "FILES": "Fichiers", - "EACH": "Chacun", + "FILES": "fichiers", + "EACH": "chacun", "DEDUPLICATE_BASED_ON_SIZE": "Les fichiers suivants ont été clubbed, basé sur leurs tailles, veuillez corriger et supprimer les objets que vous pensez être dupliqués", "STOP_ALL_UPLOADS_MESSAGE": "Êtes-vous certains de vouloir arrêter tous les chargements en cours?", "STOP_UPLOADS_HEADER": "Arrêter les chargements ?", @@ -455,12 +455,12 @@ "WATCHED_FOLDERS": "Voir les dossiers", "NO_FOLDERS_ADDED": "Aucun dossiers d'ajouté!", "FOLDERS_AUTOMATICALLY_MONITORED": "Les dossiers que vous ajoutez ici seront supervisés automatiquement", - "UPLOAD_NEW_FILES_TO_ENTE": "Charger de nouveaux fichiers sur ente", - "REMOVE_DELETED_FILES_FROM_ENTE": "Retirer de ente les fichiers supprimés", + "UPLOAD_NEW_FILES_TO_ENTE": "Charger de nouveaux fichiers sur Ente", + "REMOVE_DELETED_FILES_FROM_ENTE": "Retirer de Ente les fichiers supprimés", "ADD_FOLDER": "Ajouter un dossier", "STOP_WATCHING": "Arrêter de voir", "STOP_WATCHING_FOLDER": "Arrêter de voir le dossier?", - "STOP_WATCHING_DIALOG_MESSAGE": "Vos fichiers existants ne seront pas supprimés, mais ente arrêtera automatiquement de mettre à jour le lien de l'album à chaque changements sur ce dossier.", + "STOP_WATCHING_DIALOG_MESSAGE": "Vos fichiers existants ne seront pas supprimés, mais Ente arrêtera automatiquement de mettre à jour le lien de l'album à chaque changements sur ce dossier.", "YES_STOP": "Oui, arrêter", "MONTH_SHORT": "mo", "YEAR": "année", @@ -474,18 +474,18 @@ "FREE_PLAN_OPTION_LABEL": "Poursuivre avec la version d'essai gratuite", "FREE_PLAN_DESCRIPTION": "1 Go 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", + "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", "CONFIRM_ACCOUNT_DELETION_MESSAGE": "

Vos données chargées seront programmées pour suppression, et votre comptre sera supprimé définitivement .

Cette action n'est pas reversible.

", "AUTHENTICATE": "Authentification", "UPLOADED_TO_SINGLE_COLLECTION": "Chargé dans une seule collection", "UPLOADED_TO_SEPARATE_COLLECTIONS": "Chargé dans des collections séparées", "NEVERMIND": "Peu-importe", "UPDATE_AVAILABLE": "Une mise à jour est disponible", - "UPDATE_INSTALLABLE_MESSAGE": "Une nouvelle version de ente est prête à être installée.", + "UPDATE_INSTALLABLE_MESSAGE": "Une nouvelle version de Ente est prête à être installée.", "INSTALL_NOW": "Installer maintenant", "INSTALL_ON_NEXT_LAUNCH": "Installer au prochain démarrage", - "UPDATE_AVAILABLE_MESSAGE": "Une nouvelle version de ente est sortie, mais elle ne peut pas être automatiquement téléchargée puis installée.", + "UPDATE_AVAILABLE_MESSAGE": "Une nouvelle version de Ente est sortie, mais elle ne peut pas être automatiquement téléchargée puis installée.", "DOWNLOAD_AND_INSTALL": "Télécharger et installer", "IGNORE_THIS_VERSION": "Ignorer cette version", "TODAY": "Aujourd'hui", @@ -499,13 +499,13 @@ "ML_MORE_DETAILS": "Plus de détails", "ENABLE_FACE_SEARCH": "Activer la recherche faciale", "ENABLE_FACE_SEARCH_TITLE": "Activer la recherche faciale ?", - "ENABLE_FACE_SEARCH_DESCRIPTION": "

If you enable face search, ente will extract face geometry from your photos. This will happen on your device, and any generated biometric data will be end-to-encrypted.

Please click here for more details about this feature in our privacy policy

", + "ENABLE_FACE_SEARCH_DESCRIPTION": "

If you enable face search, Ente will extract face geometry from your photos. This will happen on your device, and any generated biometric data will be end-to-encrypted.

Please click here for more details about this feature in our privacy policy

", "DISABLE_BETA": "Désactiver la bêta", "DISABLE_FACE_SEARCH": "Désactiver la recherche faciale", "DISABLE_FACE_SEARCH_TITLE": "Désactiver la recherche faciale ?", - "DISABLE_FACE_SEARCH_DESCRIPTION": "

ente will stop processing face geometry, and will also disable ML search (beta)

You can reenable face search again if you wish, so this operation is safe

", + "DISABLE_FACE_SEARCH_DESCRIPTION": "

Ente will stop processing face geometry, and will also disable ML search (beta)

You can reenable face search again if you wish, so this operation is safe

", "ADVANCED": "Avancé", - "FACE_SEARCH_CONFIRMATION": "Je comprends, et je souhaite permettre à ente de traiter la géométrie faciale", + "FACE_SEARCH_CONFIRMATION": "Je comprends, et je souhaite permettre à Ente de traiter la géométrie faciale", "LABS": "Labs", "YOURS": "Le vôtre", "PASSPHRASE_STRENGTH_WEAK": "Sécurité du mot de passe : faible", @@ -597,7 +597,7 @@ "LIVE_PHOTO": "Photos en direct", "CONVERT": "Convertir", "CONFIRM_EDITOR_CLOSE_MESSAGE": "Êtes-vous sûr de vouloir fermer l'éditeur ?", - "CONFIRM_EDITOR_CLOSE_DESCRIPTION": "Téléchargez votre image modifiée ou enregistrez une copie sur ente pour maintenir vos modifications.", + "CONFIRM_EDITOR_CLOSE_DESCRIPTION": "Téléchargez votre image modifiée ou enregistrez une copie sur Ente pour maintenir vos modifications.", "BRIGHTNESS": "Luminosité", "CONTRAST": "Contraste", "SATURATION": "Saturation", @@ -610,7 +610,7 @@ "FLIP_VERTICALLY": "Basculer verticalement", "FLIP_HORIZONTALLY": "Retourner horizontalement", "DOWNLOAD_EDITED": "Téléchargement modifié", - "SAVE_A_COPY_TO_ENTE": "Enregistrer une copie dans ente", + "SAVE_A_COPY_TO_ENTE": "Enregistrer une copie dans Ente", "RESTORE_ORIGINAL": "Restaurer l'original", "TRANSFORM": "Transformer", "COLORS": "Couleurs", diff --git a/web/apps/photos/public/locales/it-IT/translation.json b/web/apps/photos/public/locales/it-IT/translation.json index ae450e5fef..2989ba4ce7 100644 --- a/web/apps/photos/public/locales/it-IT/translation.json +++ b/web/apps/photos/public/locales/it-IT/translation.json @@ -7,7 +7,7 @@ "HERO_SLIDE_3": "Android, iOS, Web, Desktop", "LOGIN": "Accedi", "SIGN_UP": "Registrati", - "NEW_USER": "Nuovo utente", + "NEW_USER": "", "EXISTING_USER": "Accedi", "ENTER_NAME": "Inserisci il nome", "PUBLIC_UPLOADER_NAME_MESSAGE": "Aggiungi un nome in modo che i tuoi amici sappiano chi ringraziare per queste fantastiche foto!", @@ -93,7 +93,7 @@ "TRASH_FILES_TITLE": "Elimina file?", "TRASH_FILE_TITLE": "Eliminare il file?", "DELETE_FILES_TITLE": "Eliminare immediatamente?", - "DELETE_FILES_MESSAGE": "I file selezionati verranno eliminati definitivamente dal tuo account ente.", + "DELETE_FILES_MESSAGE": "I file selezionati verranno eliminati definitivamente dal tuo account Ente.", "DELETE": "Cancella", "DELETE_OPTION": "Cancella (DEL)", "FAVORITE_OPTION": "Preferito (L)", @@ -105,7 +105,7 @@ "UPLOAD_STRATEGY_COLLECTION_PER_FOLDER": "Album separati", "SESSION_EXPIRED_MESSAGE": "La sessione è scaduta. Per continuare, esegui nuovamente l'accesso", "SESSION_EXPIRED": "Sessione scaduta", - "PASSWORD_GENERATION_FAILED": "Il tuo browser non è stato in grado di generare una chiave forte che soddisfa gli standard di crittografia ente, prova ad usare l'app per dispositivi mobili o un altro browser", + "PASSWORD_GENERATION_FAILED": "Il tuo browser non è stato in grado di generare una chiave forte che soddisfa gli standard di crittografia Ente, prova ad usare l'app per dispositivi mobili o un altro browser", "CHANGE_PASSWORD": "Cambia password", "GO_BACK": "Torna indietro", "RECOVERY_KEY": "Chiave di recupero", @@ -327,7 +327,7 @@ "RESTORE_TO_COLLECTION": "Ripristina nell'album", "EMPTY_TRASH": "Svuota il cestino", "EMPTY_TRASH_TITLE": "Vuoi svuotare il cestino?", - "EMPTY_TRASH_MESSAGE": "I file selezionati verranno eliminati definitivamente dal tuo account ente.", + "EMPTY_TRASH_MESSAGE": "I file selezionati verranno eliminati definitivamente dal tuo account Ente.", "LEAVE_SHARED_ALBUM": "Sì, esci", "LEAVE_ALBUM": "Abbandona l'album", "LEAVE_SHARED_ALBUM_TITLE": "Abbandonare l'album condiviso?", diff --git a/web/apps/photos/public/locales/ko-KR/translation.json b/web/apps/photos/public/locales/ko-KR/translation.json index 4fbe6c0777..9454a0356e 100644 --- a/web/apps/photos/public/locales/ko-KR/translation.json +++ b/web/apps/photos/public/locales/ko-KR/translation.json @@ -1,14 +1,14 @@ { - "HERO_SLIDE_1_TITLE": "추억을 안전하게 백업하세요", - "HERO_SLIDE_1": "종단간 암호화가 기본지원입니다", - "HERO_SLIDE_2_TITLE": "낙진대피소에 안전하게 보관됩니다", - "HERO_SLIDE_2": "오랫동안 보존할 수 있도록한 설계", - "HERO_SLIDE_3_TITLE": "
어디에서나
이용가능
", + "HERO_SLIDE_1_TITLE": "
당신의 추억을 위한
비공개 백업
", + "HERO_SLIDE_1": "종단간 암호화를 기본적으로 지원합니다", + "HERO_SLIDE_2_TITLE": "
낙진 대피소에
안전하게 보관됨
", + "HERO_SLIDE_2": "장기 보존을 위해 설계되었습니다", + "HERO_SLIDE_3_TITLE": "
모든 기기에서
사용 가능
", "HERO_SLIDE_3": "안드로이드, iOS, 웹, 데스크탑", "LOGIN": "로그인", "SIGN_UP": "회원가입", - "NEW_USER": "ente의 새소식", - "EXISTING_USER": "기존 사용자", + "NEW_USER": "Ente 의 새소식", + "EXISTING_USER": "기존 회원 로그인", "ENTER_NAME": "이름 입력", "PUBLIC_UPLOADER_NAME_MESSAGE": "친구들이 이 멋진 사진에 대해 고마워할 수 있도록 이름을 추가하세요!", "ENTER_EMAIL": "이메일 주소를 입력하세요", @@ -76,42 +76,42 @@ "DOWNLOAD": "다운로드", "DOWNLOAD_OPTION": "다운로드 (D)", "DOWNLOAD_FAVORITES": "즐겨찾기 다운로드", - "DOWNLOAD_UNCATEGORIZED": "", - "DOWNLOAD_HIDDEN_ITEMS": "", - "COPY_OPTION": "", - "TOGGLE_FULLSCREEN": "", - "ZOOM_IN_OUT": "", - "PREVIOUS": "", - "NEXT": "", - "TITLE_PHOTOS": "", - "TITLE_ALBUMS": "", - "TITLE_AUTH": "", - "UPLOAD_FIRST_PHOTO": "", - "IMPORT_YOUR_FOLDERS": "", - "UPLOAD_DROPZONE_MESSAGE": "", - "WATCH_FOLDER_DROPZONE_MESSAGE": "", - "TRASH_FILES_TITLE": "", - "TRASH_FILE_TITLE": "", - "DELETE_FILES_TITLE": "", - "DELETE_FILES_MESSAGE": "", - "DELETE": "", - "DELETE_OPTION": "", - "FAVORITE_OPTION": "", - "UNFAVORITE_OPTION": "", - "MULTI_FOLDER_UPLOAD": "", - "UPLOAD_STRATEGY_CHOICE": "", - "UPLOAD_STRATEGY_SINGLE_COLLECTION": "", - "OR": "", - "UPLOAD_STRATEGY_COLLECTION_PER_FOLDER": "", - "SESSION_EXPIRED_MESSAGE": "", - "SESSION_EXPIRED": "", - "PASSWORD_GENERATION_FAILED": "", - "CHANGE_PASSWORD": "", - "GO_BACK": "", - "RECOVERY_KEY": "", - "SAVE_LATER": "", - "SAVE": "", - "RECOVERY_KEY_DESCRIPTION": "", + "DOWNLOAD_UNCATEGORIZED": "미분류 항목 다운로드", + "DOWNLOAD_HIDDEN_ITEMS": "숨겨진 항목 다운로드", + "COPY_OPTION": "PNG로 복사 (Ctrl/Cmd - C)", + "TOGGLE_FULLSCREEN": "전체 화면 열기 (F)", + "ZOOM_IN_OUT": "확대/축소", + "PREVIOUS": "이전 항목 (←)", + "NEXT": "다음 항목 (→)", + "TITLE_PHOTOS": "Ente Photos", + "TITLE_ALBUMS": "Ente Photos", + "TITLE_AUTH": "Ente Auth", + "UPLOAD_FIRST_PHOTO": "첫 번째 사진을 업로드하세요", + "IMPORT_YOUR_FOLDERS": "폴더 가져오기", + "UPLOAD_DROPZONE_MESSAGE": "백업하려는 파일을 올려놓기", + "WATCH_FOLDER_DROPZONE_MESSAGE": "추가하려는 감시 폴더 올려놓기", + "TRASH_FILES_TITLE": "파일들을 삭제할까요?", + "TRASH_FILE_TITLE": "파일을 삭제할까요?", + "DELETE_FILES_TITLE": "즉시 삭제할까요?", + "DELETE_FILES_MESSAGE": "선택된 파일들은 당신의 Ente계정에서 영구적으로 삭제됩니다.", + "DELETE": "삭제", + "DELETE_OPTION": "삭제 (DEL)", + "FAVORITE_OPTION": "즐겨찾기 (L)", + "UNFAVORITE_OPTION": "즐겨찾기해제 (L)", + "MULTI_FOLDER_UPLOAD": "여러 폴더들이 탐지되었다", + "UPLOAD_STRATEGY_CHOICE": "그것들을 업로드 하겠습니까", + "UPLOAD_STRATEGY_SINGLE_COLLECTION": "싱글 앨범", + "OR": "또는", + "UPLOAD_STRATEGY_COLLECTION_PER_FOLDER": "앨범들 분리", + "SESSION_EXPIRED_MESSAGE": "세션이 만료되었습니다. 계속하려면 다시 로그인하세요", + "SESSION_EXPIRED": "세션 만료", + "PASSWORD_GENERATION_FAILED": "당신의 브라우저는 Ente의 암호화 표준을 충족하는 강력한 키를 생성할 수 없습니다. 모바일 앱이나 다른 브라우저를 사용하세요.", + "CHANGE_PASSWORD": "암호 변경하기", + "GO_BACK": "뒤로 가기", + "RECOVERY_KEY": "키 복구하기", + "SAVE_LATER": "나중에 저장하기", + "SAVE": "키 저장하기", + "RECOVERY_KEY_DESCRIPTION": "암호 분실시, 오직 이 키를 이용해야만 데이터를 복구할 수 있습니다.", "RECOVER_KEY_GENERATION_FAILED": "", "KEY_NOT_STORED_DISCLAIMER": "", "FORGOT_PASSWORD": "", @@ -126,15 +126,15 @@ "CONTACT_SUPPORT": "", "REQUEST_FEATURE": "", "SUPPORT": "", - "CONFIRM": "", - "CANCEL": "", - "LOGOUT": "", - "DELETE_ACCOUNT": "", - "DELETE_ACCOUNT_MESSAGE": "", - "LOGOUT_MESSAGE": "", - "CHANGE_EMAIL": "", - "OK": "", - "SUCCESS": "", + "CONFIRM": "확인", + "CANCEL": "취소", + "LOGOUT": "로그아웃", + "DELETE_ACCOUNT": "계정 삭제", + "DELETE_ACCOUNT_MESSAGE": "

회원가입에 사용한 이메일을 통해 {{emailID}} (으)로 메일을 보내주세요.

귀하의 요청은 72시간 내로 처리됩니다.

", + "LOGOUT_MESSAGE": "정말로 로그아웃 하시겠습니까?", + "CHANGE_EMAIL": "이메일 주소 변경", + "OK": "확인", + "SUCCESS": "성공", "ERROR": "", "MESSAGE": "", "INSTALL_MOBILE_APP": "", @@ -144,7 +144,7 @@ "SUBSCRIPTION": "", "SUBSCRIBE": "", "MANAGEMENT_PORTAL": "", - "MANAGE_FAMILY_PORTAL": "", + "MANAGE_FAMILY_PORTAL": "가족 관리", "LEAVE_FAMILY_PLAN": "", "LEAVE": "", "LEAVE_FAMILY_CONFIRM": "", diff --git a/web/apps/photos/public/locales/nl-NL/translation.json b/web/apps/photos/public/locales/nl-NL/translation.json index 15d9bfdba0..01fe5f9c8a 100644 --- a/web/apps/photos/public/locales/nl-NL/translation.json +++ b/web/apps/photos/public/locales/nl-NL/translation.json @@ -7,7 +7,7 @@ "HERO_SLIDE_3": "Android, iOS, Web, Desktop", "LOGIN": "Inloggen", "SIGN_UP": "Registreren", - "NEW_USER": "Nieuw bij ente", + "NEW_USER": "Nieuw bij Ente", "EXISTING_USER": "Bestaande gebruiker", "ENTER_NAME": "Naam invoeren", "PUBLIC_UPLOADER_NAME_MESSAGE": "Voeg een naam toe zodat je vrienden weten wie ze moeten bedanken voor deze geweldige foto's!", @@ -93,7 +93,7 @@ "TRASH_FILES_TITLE": "Bestanden verwijderen?", "TRASH_FILE_TITLE": "Verwijder bestand?", "DELETE_FILES_TITLE": "Onmiddellijk verwijderen?", - "DELETE_FILES_MESSAGE": "Geselecteerde bestanden zullen permanent worden verwijderd van je ente account.", + "DELETE_FILES_MESSAGE": "Geselecteerde bestanden zullen permanent worden verwijderd van je Ente account.", "DELETE": "Verwijderen", "DELETE_OPTION": "Verwijderen (DEL)", "FAVORITE_OPTION": "Favoriet (L)", @@ -278,11 +278,11 @@ "LAST_EXPORT_TIME": "Tijd laatste export", "EXPORT_AGAIN": "Opnieuw synchroniseren", "LOCAL_STORAGE_NOT_ACCESSIBLE": "Lokale opslag niet toegankelijk", - "LOCAL_STORAGE_NOT_ACCESSIBLE_MESSAGE": "Je browser of een extensie blokkeert ente om gegevens op te slaan in de lokale opslag. Probeer deze pagina te laden na het aanpassen van de browser surfmodus.", + "LOCAL_STORAGE_NOT_ACCESSIBLE_MESSAGE": "Je browser of een extensie blokkeert Ente om gegevens op te slaan in de lokale opslag. Probeer deze pagina te laden na het aanpassen van de browser surfmodus.", "SEND_OTT": "Stuur OTP", "EMAIl_ALREADY_OWNED": "E-mail al in gebruik", - "ETAGS_BLOCKED": "

We kunnen de volgende bestanden niet uploaden vanwege uw browserconfiguratie.

Schakel alle extensies uit die mogelijk voorkomen dat ente eTags kan gebruiken om grote bestanden te uploaden, of gebruik onze desktop app voor een betrouwbaardere import ervaring.

", - "SKIPPED_VIDEOS_INFO": "

We ondersteunen het toevoegen van video's via openbare links momenteel niet.

Om video's te delen, meld je aan bij ente en deel met de beoogde ontvangers via hun e-mail

", + "ETAGS_BLOCKED": "

We kunnen de volgende bestanden niet uploaden vanwege uw browserconfiguratie.

Schakel alle extensies uit die mogelijk voorkomen dat Ente eTags kan gebruiken om grote bestanden te uploaden, of gebruik onze desktop app voor een betrouwbaardere import ervaring.

", + "SKIPPED_VIDEOS_INFO": "

We ondersteunen het toevoegen van video's via openbare links momenteel niet.

Om video's te delen, meld je aan bij Ente en deel met de beoogde ontvangers via hun e-mail

", "LIVE_PHOTOS_DETECTED": "De foto en video bestanden van je Live Photos zijn samengevoegd tot één enkel bestand", "RETRY_FAILED": "Probeer mislukte uploads nogmaals", "FAILED_UPLOADS": "Mislukte uploads ", @@ -291,7 +291,7 @@ "UNSUPPORTED_FILES": "Niet-ondersteunde bestanden", "SUCCESSFUL_UPLOADS": "Succesvolle uploads", "SKIPPED_INFO": "Deze zijn overgeslagen omdat er bestanden zijn met overeenkomende namen in hetzelfde album", - "UNSUPPORTED_INFO": "ente ondersteunt deze bestandsformaten nog niet", + "UNSUPPORTED_INFO": "Ente ondersteunt deze bestandsformaten nog niet", "BLOCKED_UPLOADS": "Geblokkeerde uploads", "SKIPPED_VIDEOS": "Overgeslagen video's", "INPROGRESS_METADATA_EXTRACTION": "In behandeling", @@ -327,7 +327,7 @@ "RESTORE_TO_COLLECTION": "Terugzetten naar album", "EMPTY_TRASH": "Prullenbak leegmaken", "EMPTY_TRASH_TITLE": "Prullenbak leegmaken?", - "EMPTY_TRASH_MESSAGE": "Geselecteerde bestanden zullen permanent worden verwijderd van uw ente account.", + "EMPTY_TRASH_MESSAGE": "Geselecteerde bestanden zullen permanent worden verwijderd van uw Ente account.", "LEAVE_SHARED_ALBUM": "Ja, verwijderen", "LEAVE_ALBUM": "Album verlaten", "LEAVE_SHARED_ALBUM_TITLE": "Gedeeld album verwijderen?", @@ -342,7 +342,7 @@ "THUMBNAIL_REPLACED": "Thumbnails gecomprimeerd", "FIX_THUMBNAIL": "Comprimeren", "FIX_THUMBNAIL_LATER": "Later comprimeren", - "REPLACE_THUMBNAIL_NOT_STARTED": "Sommige van uw video thumbnails kunnen worden gecomprimeerd om ruimte te besparen. Wilt u dat ente ze comprimeert?", + "REPLACE_THUMBNAIL_NOT_STARTED": "Sommige van uw video thumbnails kunnen worden gecomprimeerd om ruimte te besparen. Wilt u dat Ente ze comprimeert?", "REPLACE_THUMBNAIL_COMPLETED": "Alle thumbnails zijn gecomprimeerd", "REPLACE_THUMBNAIL_NOOP": "Je hebt geen thumbnails die verder gecomprimeerd kunnen worden", "REPLACE_THUMBNAIL_COMPLETED_WITH_ERROR": "Kon sommige van uw thumbnails niet comprimeren, probeer het opnieuw", @@ -421,8 +421,8 @@ "AUTHENTICATOR_SECTION": "Verificatie apparaat", "NO_DUPLICATES_FOUND": "Je hebt geen dubbele bestanden die kunnen worden gewist", "CLUB_BY_CAPTURE_TIME": "Samenvoegen op tijd", - "FILES": "Bestanden", - "EACH": "Elke", + "FILES": "bestanden", + "EACH": "elke", "DEDUPLICATE_BASED_ON_SIZE": "De volgende bestanden zijn samengevoegd op basis van hun groottes. Controleer en verwijder items waarvan je denkt dat ze dubbel zijn", "STOP_ALL_UPLOADS_MESSAGE": "Weet u zeker dat u wilt stoppen met alle uploads die worden uitgevoerd?", "STOP_UPLOADS_HEADER": "Stoppen met uploaden?", @@ -455,12 +455,12 @@ "WATCHED_FOLDERS": "Gemonitorde mappen", "NO_FOLDERS_ADDED": "Nog geen mappen toegevoegd!", "FOLDERS_AUTOMATICALLY_MONITORED": "De mappen die u hier toevoegt worden automatisch gemonitord", - "UPLOAD_NEW_FILES_TO_ENTE": "Nieuwe bestanden uploaden naar ente", - "REMOVE_DELETED_FILES_FROM_ENTE": "Verwijderde bestanden van ente opruimen", + "UPLOAD_NEW_FILES_TO_ENTE": "Nieuwe bestanden uploaden naar Ente", + "REMOVE_DELETED_FILES_FROM_ENTE": "Verwijderde bestanden van Ente opruimen", "ADD_FOLDER": "Map toevoegen", "STOP_WATCHING": "Stop monitoren", "STOP_WATCHING_FOLDER": "Stop monitoren van map?", - "STOP_WATCHING_DIALOG_MESSAGE": "Uw bestaande bestanden zullen niet worden verwijderd, maar ente stopt met het automatisch bijwerken van het gekoppelde ente album bij wijzigingen in deze map.", + "STOP_WATCHING_DIALOG_MESSAGE": "Uw bestaande bestanden zullen niet worden verwijderd, maar Ente stopt met het automatisch bijwerken van het gekoppelde Ente album bij wijzigingen in deze map.", "YES_STOP": "Ja, stop", "MONTH_SHORT": "mo", "YEAR": "jaar", @@ -474,18 +474,18 @@ "FREE_PLAN_OPTION_LABEL": "Doorgaan met gratis account", "FREE_PLAN_DESCRIPTION": "1 GB 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", + "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", "CONFIRM_ACCOUNT_DELETION_MESSAGE": "Uw geüploade gegevens worden gepland voor verwijdering, en uw account zal permanent worden verwijderd.

Deze actie is onomkeerbaar.", "AUTHENTICATE": "Verifiëren", "UPLOADED_TO_SINGLE_COLLECTION": "Geüpload naar enkele collectie", "UPLOADED_TO_SEPARATE_COLLECTIONS": "Geüpload naar verschillende collecties", "NEVERMIND": "Laat maar", "UPDATE_AVAILABLE": "Update beschikbaar", - "UPDATE_INSTALLABLE_MESSAGE": "Er staat een nieuwe versie van ente klaar om te worden geïnstalleerd.", + "UPDATE_INSTALLABLE_MESSAGE": "Er staat een nieuwe versie van Ente klaar om te worden geïnstalleerd.", "INSTALL_NOW": "Nu installeren", "INSTALL_ON_NEXT_LAUNCH": "Installeren bij volgende start", - "UPDATE_AVAILABLE_MESSAGE": "Er is een nieuwe versie van ente vrijgegeven, maar deze kan niet automatisch worden gedownload en geïnstalleerd.", + "UPDATE_AVAILABLE_MESSAGE": "Er is een nieuwe versie van Ente vrijgegeven, maar deze kan niet automatisch worden gedownload en geïnstalleerd.", "DOWNLOAD_AND_INSTALL": "Downloaden en installeren", "IGNORE_THIS_VERSION": "Negeer deze versie", "TODAY": "Vandaag", @@ -499,13 +499,13 @@ "ML_MORE_DETAILS": "Meer details", "ENABLE_FACE_SEARCH": "Zoeken op gezichten inschakelen", "ENABLE_FACE_SEARCH_TITLE": "Zoeken op gezichten inschakelen?", - "ENABLE_FACE_SEARCH_DESCRIPTION": "

Als u zoeken op gezichten inschakelt, analyseert ente de gezichtsgeometrie uit uw foto's. Dit gebeurt op uw apparaat en alle gegenereerde biometrische gegevens worden end-to-end versleuteld.

Klik hier voor meer informatie over deze functie in ons privacybeleid

", + "ENABLE_FACE_SEARCH_DESCRIPTION": "

Als u zoeken op gezichten inschakelt, analyseert Ente de gezichtsgeometrie uit uw foto's. Dit gebeurt op uw apparaat en alle gegenereerde biometrische gegevens worden end-to-end versleuteld.

Klik hier voor meer informatie over deze functie in ons privacybeleid

", "DISABLE_BETA": "Bèta uitschakelen", "DISABLE_FACE_SEARCH": "Zoeken op gezichten uitschakelen", "DISABLE_FACE_SEARCH_TITLE": "Zoeken op gezichten uitschakelen?", - "DISABLE_FACE_SEARCH_DESCRIPTION": "

ente zal stoppen met het analyseren van de gezichtsgeometrie, en zal ML zoeken (beta) uitschakelen

U kan zoeken op gezichten opnieuw inschakelen wanneer u wilt, dus deze handeling is veilig.

", + "DISABLE_FACE_SEARCH_DESCRIPTION": "

Ente zal stoppen met het analyseren van de gezichtsgeometrie, en zal ML zoeken (beta) uitschakelen

U kan zoeken op gezichten opnieuw inschakelen wanneer u wilt, dus deze handeling is veilig.

", "ADVANCED": "Geavanceerd", - "FACE_SEARCH_CONFIRMATION": "Ik begrijp het, en wil ente toestaan om gezichten te analyseren", + "FACE_SEARCH_CONFIRMATION": "Ik begrijp het, en wil Ente toestaan om gezichten te analyseren", "LABS": "Lab's", "YOURS": "jouw", "PASSPHRASE_STRENGTH_WEAK": "Wachtwoord sterkte: Zwak", @@ -597,7 +597,7 @@ "LIVE_PHOTO": "Live foto", "CONVERT": "Converteren", "CONFIRM_EDITOR_CLOSE_MESSAGE": "Weet u zeker dat u de editor wilt afsluiten?", - "CONFIRM_EDITOR_CLOSE_DESCRIPTION": "Download uw bewerkte afbeelding of sla een kopie op in ente om uw wijzigingen te behouden.", + "CONFIRM_EDITOR_CLOSE_DESCRIPTION": "Download uw bewerkte afbeelding of sla een kopie op in Ente om uw wijzigingen te behouden.", "BRIGHTNESS": "Helderheid", "CONTRAST": "Contrast", "SATURATION": "Saturatie", @@ -610,7 +610,7 @@ "FLIP_VERTICALLY": "Verticaal spiegelen", "FLIP_HORIZONTALLY": "Horizontaal spiegelen", "DOWNLOAD_EDITED": "Download Bewerkt", - "SAVE_A_COPY_TO_ENTE": "Kopie in ente opslaan", + "SAVE_A_COPY_TO_ENTE": "Kopie in Ente opslaan", "RESTORE_ORIGINAL": "Origineel herstellen", "TRANSFORM": "Transformeer", "COLORS": "Kleuren", @@ -622,33 +622,33 @@ "FASTER_UPLOAD_DESCRIPTION": "Uploaden door nabije servers", "MAGIC_SEARCH_STATUS": "Magische Zoekfunctie Status", "INDEXED_ITEMS": "Geïndexeerde bestanden", - "CAST_ALBUM_TO_TV": "", - "ENTER_CAST_PIN_CODE": "", - "PAIR_DEVICE_TO_TV": "", - "TV_NOT_FOUND": "", - "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": "", + "CAST_ALBUM_TO_TV": "Album afspelen op TV", + "ENTER_CAST_PIN_CODE": "Voer de code in die u op de TV ziet om dit apparaat te koppelen.", + "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.", + "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.", + "VISIT_CAST_ENTE_IO": "Bezoek cast.ente.io op het apparaat dat je wilt koppelen.", + "CAST_AUTO_PAIR_FAILED": "Auto koppelen van Chromecast is mislukt. Probeer het opnieuw.", "CACHE_DIRECTORY": "Cache map", "FREEHAND": "Losse hand", "APPLY_CROP": "Bijsnijden toepassen", "PHOTO_EDIT_REQUIRED_TO_SAVE": "Tenminste één transformatie of kleuraanpassing moet worden uitgevoerd voordat u opslaat.", - "PASSKEYS": "", - "DELETE_PASSKEY": "", - "DELETE_PASSKEY_CONFIRMATION": "", - "RENAME_PASSKEY": "", - "ADD_PASSKEY": "", - "ENTER_PASSKEY_NAME": "", - "PASSKEYS_DESCRIPTION": "", - "CREATED_AT": "", - "PASSKEY_LOGIN_FAILED": "", - "PASSKEY_LOGIN_URL_INVALID": "", - "PASSKEY_LOGIN_ERRORED": "", - "TRY_AGAIN": "", - "PASSKEY_FOLLOW_THE_STEPS_FROM_YOUR_BROWSER": "", - "LOGIN_WITH_PASSKEY": "" + "PASSKEYS": "Passkeys", + "DELETE_PASSKEY": "Passkeys verwijderen", + "DELETE_PASSKEY_CONFIRMATION": "Weet je zeker dat je deze passkey wilt verwijderen? Deze actie is onomkeerbaar.", + "RENAME_PASSKEY": "Hernoem passkeys", + "ADD_PASSKEY": "Passkeys toevoegen", + "ENTER_PASSKEY_NAME": "Voer passkey naam in", + "PASSKEYS_DESCRIPTION": "Passkeys zijn een moderne en veilige tweede factor beveiliging voor je Ente account. Ze gebruiken biometrische authenticatie op je apparaat voor gemak en veiligheid.", + "CREATED_AT": "Aangemaakt op", + "PASSKEY_LOGIN_FAILED": "Passkey login mislukt", + "PASSKEY_LOGIN_URL_INVALID": "De inlog-URL is ongeldig.", + "PASSKEY_LOGIN_ERRORED": "Er is een fout opgetreden tijdens het inloggen met een passkey.", + "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" } diff --git a/web/apps/photos/public/locales/pt-BR/translation.json b/web/apps/photos/public/locales/pt-BR/translation.json index 0da001742f..0eb7a9f329 100644 --- a/web/apps/photos/public/locales/pt-BR/translation.json +++ b/web/apps/photos/public/locales/pt-BR/translation.json @@ -7,7 +7,7 @@ "HERO_SLIDE_3": "Android, iOS, Web, Desktop", "LOGIN": "Entrar", "SIGN_UP": "Registrar", - "NEW_USER": "Novo no ente", + "NEW_USER": "Novo no Ente", "EXISTING_USER": "Usuário existente", "ENTER_NAME": "Insira o nome", "PUBLIC_UPLOADER_NAME_MESSAGE": "Adicione um nome para que os seus amigos saibam a quem agradecer por estas ótimas fotos!", @@ -31,7 +31,7 @@ "VERIFY_PASSPHRASE": "Iniciar sessão", "INCORRECT_PASSPHRASE": "Palavra-passe incorreta", "ENTER_ENC_PASSPHRASE": "Por favor, digite uma senha que podemos usar para criptografar seus dados", - "PASSPHRASE_DISCLAIMER": "Não armazenamos sua senha, portanto, se você esquecê-la, não poderemos ajudarna recuperação de seus dados sem uma chave de recuperação.", + "PASSPHRASE_DISCLAIMER": "Não armazenamos sua senha, portanto, se você esquecê-la, não poderemos ajudar na recuperação de seus dados sem uma chave de recuperação.", "WELCOME_TO_ENTE_HEADING": "Bem-vindo ao ", "WELCOME_TO_ENTE_SUBHEADING": "Armazenamento criptografado de ponta a ponta de fotos e compartilhamento", "WHERE_YOUR_BEST_PHOTOS_LIVE": "Onde suas melhores fotos vivem", @@ -124,7 +124,7 @@ "NO_RECOVERY_KEY_MESSAGE": "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", "NO_TWO_FACTOR_RECOVERY_KEY_MESSAGE": "Por favor, envie um e-mail para {{emailID}} a partir do seu endereço de e-mail registrado", "CONTACT_SUPPORT": "Falar com o suporte", - "REQUEST_FEATURE": "Solicitar Funcionalidade", + "REQUEST_FEATURE": "Solicitar recurso", "SUPPORT": "Suporte", "CONFIRM": "Confirmar", "CANCEL": "Cancelar", @@ -231,8 +231,8 @@ "UNIDENTIFIED_FACES": "rostos não identificados", "OBJECTS": "objetos", "TEXT": "texto", - "INFO": "Informação ", - "INFO_OPTION": "Informação (I)", + "INFO": "Informações ", + "INFO_OPTION": "Informações (I)", "FILE_NAME": "Nome do arquivo", "CAPTION_PLACEHOLDER": "Adicionar uma descrição", "LOCATION": "Local", @@ -306,7 +306,7 @@ "ARCHIVE": "Arquivar", "FAVORITES": "Favoritos", "ARCHIVE_COLLECTION": "Arquivar álbum", - "ARCHIVE_SECTION_NAME": "Arquivar", + "ARCHIVE_SECTION_NAME": "Arquivado", "ALL_SECTION_NAME": "Todos", "MOVE_TO_COLLECTION": "Mover para álbum", "UNARCHIVE": "Desarquivar", @@ -417,12 +417,12 @@ "UPLOAD_FILES": "Arquivo", "UPLOAD_DIRS": "Pasta", "UPLOAD_GOOGLE_TAKEOUT": "Google Takeout", - "DEDUPLICATE_FILES": "Arquivos Deduplicados", + "DEDUPLICATE_FILES": "Arquivos duplicados", "AUTHENTICATOR_SECTION": "Autenticação", "NO_DUPLICATES_FOUND": "Você não tem arquivos duplicados que possam ser limpos", "CLUB_BY_CAPTURE_TIME": "Agrupar por tempo de captura", - "FILES": "Arquivos", - "EACH": "Cada", + "FILES": "arquivos", + "EACH": "cada", "DEDUPLICATE_BASED_ON_SIZE": "Os seguintes arquivos foram listados com base em seus tamanhos, por favor, reveja e exclua os itens que você acredita que são duplicados", "STOP_ALL_UPLOADS_MESSAGE": "Tem certeza que deseja parar todos os envios em andamento?", "STOP_UPLOADS_HEADER": "Parar envios?", @@ -573,7 +573,7 @@ "at": "em", "AUTH_NEXT": "próximo", "AUTH_DOWNLOAD_MOBILE_APP": "Baixe nosso aplicativo móvel para gerenciar seus segredos", - "HIDDEN": "Escondido", + "HIDDEN": "Oculto", "HIDE": "Ocultar", "UNHIDE": "Desocultar", "UNHIDE_TO_COLLECTION": "Reexibir para o álbum", @@ -609,7 +609,7 @@ "ROTATE_RIGHT": "Girar para a Direita", "FLIP_VERTICALLY": "Inverter verticalmente", "FLIP_HORIZONTALLY": "Inverter horizontalmente", - "DOWNLOAD_EDITED": "Transferência Editada", + "DOWNLOAD_EDITED": "Baixar Editado", "SAVE_A_COPY_TO_ENTE": "Salvar uma cópia para o ente", "RESTORE_ORIGINAL": "Restaurar original", "TRANSFORM": "Transformar", @@ -638,17 +638,17 @@ "APPLY_CROP": "Aplicar Recorte", "PHOTO_EDIT_REQUIRED_TO_SAVE": "Pelo menos uma transformação ou ajuste de cor deve ser feito antes de salvar.", "PASSKEYS": "Chaves de acesso", - "DELETE_PASSKEY": "", - "DELETE_PASSKEY_CONFIRMATION": "", - "RENAME_PASSKEY": "", - "ADD_PASSKEY": "", - "ENTER_PASSKEY_NAME": "", - "PASSKEYS_DESCRIPTION": "", - "CREATED_AT": "", - "PASSKEY_LOGIN_FAILED": "", - "PASSKEY_LOGIN_URL_INVALID": "", - "PASSKEY_LOGIN_ERRORED": "", - "TRY_AGAIN": "", - "PASSKEY_FOLLOW_THE_STEPS_FROM_YOUR_BROWSER": "", - "LOGIN_WITH_PASSKEY": "" + "DELETE_PASSKEY": "Excluir chave de acesso", + "DELETE_PASSKEY_CONFIRMATION": "Tem certeza de que deseja excluir esta chave de acesso? Esta ação é irreversível.", + "RENAME_PASSKEY": "Renomear chave de acesso", + "ADD_PASSKEY": "Adicionar chave de acesso", + "ENTER_PASSKEY_NAME": "Inserir nome da chave de acesso", + "PASSKEYS_DESCRIPTION": "As chaves de acesso são um moderno e seguro segundo fator de sua conta Ente. Elas usam a autenticação biométrica no dispositivo para fins de conveniência e segurança.", + "CREATED_AT": "Criado em", + "PASSKEY_LOGIN_FAILED": "Falha ao iniciar sessão com a chave de acesso", + "PASSKEY_LOGIN_URL_INVALID": "URL de login inválida.", + "PASSKEY_LOGIN_ERRORED": "Ocorreu um erro ao entrar com a chave de acesso.", + "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" } diff --git a/web/apps/photos/public/locales/pt-PT/translation.json b/web/apps/photos/public/locales/pt-PT/translation.json index 2309803267..cf06665fe3 100644 --- a/web/apps/photos/public/locales/pt-PT/translation.json +++ b/web/apps/photos/public/locales/pt-PT/translation.json @@ -7,7 +7,7 @@ "HERO_SLIDE_3": "Android, iOS, Web, Desktop", "LOGIN": "Entrar", "SIGN_UP": "Registar", - "NEW_USER": "Novo no ente", + "NEW_USER": "Novo no Ente", "EXISTING_USER": "Utilizador existente", "ENTER_NAME": "Insira o nome", "PUBLIC_UPLOADER_NAME_MESSAGE": "Adicione um nome para que os seus amigos saibam a quem agradecer por estas ótimas fotos!", diff --git a/web/apps/photos/public/locales/ru-RU/translation.json b/web/apps/photos/public/locales/ru-RU/translation.json index c85db22361..9757ae53b1 100644 --- a/web/apps/photos/public/locales/ru-RU/translation.json +++ b/web/apps/photos/public/locales/ru-RU/translation.json @@ -7,7 +7,7 @@ "HERO_SLIDE_3": "Android, iOS, Веб, ПК", "LOGIN": "Авторизоваться", "SIGN_UP": "Регистрация", - "NEW_USER": "Новенький в ente", + "NEW_USER": "Новенький в Ente", "EXISTING_USER": "Существующий пользователь", "ENTER_NAME": "Введите имя", "PUBLIC_UPLOADER_NAME_MESSAGE": "Добавьте имя, чтобы ваши друзья знали, кого благодарить за эти замечательные фотографии!", diff --git a/web/apps/photos/public/locales/sv-SE/translation.json b/web/apps/photos/public/locales/sv-SE/translation.json index f88535795a..6963261efb 100644 --- a/web/apps/photos/public/locales/sv-SE/translation.json +++ b/web/apps/photos/public/locales/sv-SE/translation.json @@ -421,7 +421,7 @@ "AUTHENTICATOR_SECTION": "", "NO_DUPLICATES_FOUND": "", "CLUB_BY_CAPTURE_TIME": "", - "FILES": "Filer", + "FILES": "filer", "EACH": "", "DEDUPLICATE_BASED_ON_SIZE": "", "STOP_ALL_UPLOADS_MESSAGE": "", diff --git a/web/apps/photos/public/locales/zh-CN/translation.json b/web/apps/photos/public/locales/zh-CN/translation.json index ed304f4ac9..95d93ebb87 100644 --- a/web/apps/photos/public/locales/zh-CN/translation.json +++ b/web/apps/photos/public/locales/zh-CN/translation.json @@ -7,7 +7,7 @@ "HERO_SLIDE_3": "安卓, iOS, 网页端, 桌面端", "LOGIN": "登录", "SIGN_UP": "注册", - "NEW_USER": "刚来到 ente", + "NEW_USER": "刚来到 Ente", "EXISTING_USER": "现有用户", "ENTER_NAME": "输入名字", "PUBLIC_UPLOADER_NAME_MESSAGE": "请添加一个名字,以便您的朋友知晓该感谢谁拍摄了这些精美的照片!", @@ -65,13 +65,13 @@ "5": "备份完成" }, "FILE_NOT_UPLOADED_LIST": "以下文件未上传", - "SUBSCRIPTION_EXPIRED": "您的订阅已过期", + "SUBSCRIPTION_EXPIRED": "订阅已过期", "SUBSCRIPTION_EXPIRED_MESSAGE": "您的订阅已过期,请 续期", "STORAGE_QUOTA_EXCEEDED": "已超出存储限制", "INITIAL_LOAD_DELAY_WARNING": "第一次加载可能需要一些时间", - "USER_DOES_NOT_EXIST": "抱歉,找不到该电子邮件的用户", + "USER_DOES_NOT_EXIST": "抱歉,找不到使用该电子邮件的用户", "NO_ACCOUNT": "没有账号", - "ACCOUNT_EXISTS": "已有账户", + "ACCOUNT_EXISTS": "已有账号", "CREATE": "创建", "DOWNLOAD": "下载", "DOWNLOAD_OPTION": "下载 (D)", @@ -105,7 +105,7 @@ "UPLOAD_STRATEGY_COLLECTION_PER_FOLDER": "独立相册", "SESSION_EXPIRED_MESSAGE": "您的会话已过期,请重新登录以继续", "SESSION_EXPIRED": "会话已过期", - "PASSWORD_GENERATION_FAILED": "您的浏览器无法生成一个符合ente加密标准的强密钥,请尝试使用移动应用程序或其他浏览器", + "PASSWORD_GENERATION_FAILED": "您的浏览器无法生成一个符合Ente加密标准的强密钥,请尝试使用移动应用程序或其他浏览器", "CHANGE_PASSWORD": "修改密码", "GO_BACK": "返回", "RECOVERY_KEY": "恢复密钥", @@ -122,7 +122,7 @@ "INCORRECT_RECOVERY_KEY": "不正确的恢复密钥", "SORRY": "抱歉", "NO_RECOVERY_KEY_MESSAGE": "由于我们端到端加密协议的性质,如果没有您的密码或恢复密钥,您的数据将无法解密", - "NO_TWO_FACTOR_RECOVERY_KEY_MESSAGE": "请用您注册ente账户的电子邮箱发一封邮件给 {{emailID}}", + "NO_TWO_FACTOR_RECOVERY_KEY_MESSAGE": "请用您注册Ente账户的电子邮箱发一封邮件给 {{emailID}}", "CONTACT_SUPPORT": "联系支持", "REQUEST_FEATURE": "功能建议", "SUPPORT": "支持", @@ -278,10 +278,10 @@ "LAST_EXPORT_TIME": "最后一次导出时间", "EXPORT_AGAIN": "重新同步", "LOCAL_STORAGE_NOT_ACCESSIBLE": "无法访问本地存储", - "LOCAL_STORAGE_NOT_ACCESSIBLE_MESSAGE": "您的浏览器或插件阻止 ente 将数据保存到本地存储。 请在切换浏览模式后再尝试加载此页面。", + "LOCAL_STORAGE_NOT_ACCESSIBLE_MESSAGE": "您的浏览器或插件阻止 Ente 将数据保存到本地存储。 请在切换浏览模式后再尝试加载此页面。", "SEND_OTT": "发送 OTP", "EMAIl_ALREADY_OWNED": "电子邮箱已被注册", - "ETAGS_BLOCKED": "

由于您的浏览器配置,我们无法上传以下文件。

请禁用任何可能阻止ente 使用 eTags 上传大文件的附加组件, 或者使用我们的 桌面应用程序 获取更可靠的导入体验。

", + "ETAGS_BLOCKED": "

由于您的浏览器配置,我们无法上传以下文件。

请禁用任何可能阻止Ente 使用 eTags 上传大文件的附加组件, 或者使用我们的 桌面应用程序 获取更可靠的导入体验。

", "SKIPPED_VIDEOS_INFO": "

目前,我们不支持在公共链接内添加视频。

若要分享视频,请 注册 并通过电子邮件与预定收件人分享。

", "LIVE_PHOTOS_DETECTED": "Live Photos 中的照片和视频文件已合并为一个文件", "RETRY_FAILED": "重试上传失败的文件", @@ -291,7 +291,7 @@ "UNSUPPORTED_FILES": "不支持的文件", "SUCCESSFUL_UPLOADS": "上传成功", "SKIPPED_INFO": "跳过这些,因为在同一相册中有具有匹配名称的文件", - "UNSUPPORTED_INFO": "ente 尚不支持这些文件格式", + "UNSUPPORTED_INFO": "Ente 尚不支持这些文件格式", "BLOCKED_UPLOADS": "已阻止上传", "SKIPPED_VIDEOS": "已跳过的视频", "INPROGRESS_METADATA_EXTRACTION": "进行中", @@ -327,7 +327,7 @@ "RESTORE_TO_COLLECTION": "恢复到相册", "EMPTY_TRASH": "清空回收站", "EMPTY_TRASH_TITLE": "要清空回收站吗?", - "EMPTY_TRASH_MESSAGE": "这些文件将从您的 ente 账户中永久删除。", + "EMPTY_TRASH_MESSAGE": "这些文件将从您的 Ente 账户中永久删除。", "LEAVE_SHARED_ALBUM": "是,离开", "LEAVE_ALBUM": "离开相册", "LEAVE_SHARED_ALBUM_TITLE": "要离开共享相册吗?", @@ -342,7 +342,7 @@ "THUMBNAIL_REPLACED": "缩略图已压缩", "FIX_THUMBNAIL": "压缩", "FIX_THUMBNAIL_LATER": "稍后压缩", - "REPLACE_THUMBNAIL_NOT_STARTED": "您的一些视频缩略图可以被压缩以节省空间,您想要ente 压缩它们吗?", + "REPLACE_THUMBNAIL_NOT_STARTED": "您的一些视频缩略图可以被压缩以节省空间,您想要Ente 压缩它们吗?", "REPLACE_THUMBNAIL_COMPLETED": "已成功压缩所有缩略图", "REPLACE_THUMBNAIL_NOOP": "您没有可以进一步压缩的缩略图", "REPLACE_THUMBNAIL_COMPLETED_WITH_ERROR": "无法压缩您的一些缩略图,请重试", @@ -455,12 +455,12 @@ "WATCHED_FOLDERS": "观看文件夹", "NO_FOLDERS_ADDED": "尚未添加任何文件夹!", "FOLDERS_AUTOMATICALLY_MONITORED": "您在此处添加的文件夹将自动监控", - "UPLOAD_NEW_FILES_TO_ENTE": "上传新文件至 ente", - "REMOVE_DELETED_FILES_FROM_ENTE": "从ente 移除已删除的文件", + "UPLOAD_NEW_FILES_TO_ENTE": "上传新文件至 Ente", + "REMOVE_DELETED_FILES_FROM_ENTE": "从Ente 移除已删除的文件", "ADD_FOLDER": "添加文件夹", "STOP_WATCHING": "停止监控", "STOP_WATCHING_FOLDER": "要停止监控文件夹?", - "STOP_WATCHING_DIALOG_MESSAGE": "您现有的文件不会被删除,但 ente 将停止自动更新链接的 ente 相册在此文件夹中的更改。", + "STOP_WATCHING_DIALOG_MESSAGE": "您现有的文件不会被删除,但 Ente 将停止自动更新链接的 Ente 相册在此文件夹中的更改。", "YES_STOP": "是的,停止", "MONTH_SHORT": "月", "YEAR": "年", @@ -474,18 +474,18 @@ "FREE_PLAN_OPTION_LABEL": "继续免费试用", "FREE_PLAN_DESCRIPTION": "1 GB 1年", "CURRENT_USAGE": "当前使用量是 {{usage}}", - "WEAK_DEVICE": "您使用的网络浏览器功能不够强大,无法加密您的照片。 请尝试在电脑上登录ente,或下载ente移动/桌面应用程序。", - "DRAG_AND_DROP_HINT": "或者拖动并拖动到 ente 窗口", + "WEAK_DEVICE": "您使用的网络浏览器功能不够强大,无法加密您的照片。 请尝试在电脑上登录Ente,或下载Ente移动/桌面应用程序。", + "DRAG_AND_DROP_HINT": "或者拖动并拖动到 Ente 窗口", "CONFIRM_ACCOUNT_DELETION_MESSAGE": "您上传的数据将被安排删除,您的账户将被永久删除。

此操作不可逆。", "AUTHENTICATE": "身份认证", "UPLOADED_TO_SINGLE_COLLECTION": "已上传到单个收藏", "UPLOADED_TO_SEPARATE_COLLECTIONS": "已上传到单独收藏", "NEVERMIND": "没关系", "UPDATE_AVAILABLE": "有可用的更新", - "UPDATE_INSTALLABLE_MESSAGE": "新版本的 ente 已准备好安装。", + "UPDATE_INSTALLABLE_MESSAGE": "新版本的 Ente 已准备好安装。", "INSTALL_NOW": "立即安装", "INSTALL_ON_NEXT_LAUNCH": "在下次启动时安装", - "UPDATE_AVAILABLE_MESSAGE": "新版本的 ente 已发布,但无法自动下载和安装。", + "UPDATE_AVAILABLE_MESSAGE": "新版本的 Ente 已发布,但无法自动下载和安装。", "DOWNLOAD_AND_INSTALL": "下载并安装", "IGNORE_THIS_VERSION": "忽略该版本", "TODAY": "今天", @@ -499,13 +499,13 @@ "ML_MORE_DETAILS": "更多详情", "ENABLE_FACE_SEARCH": "启用面部搜索", "ENABLE_FACE_SEARCH_TITLE": "要启用面部搜索吗?", - "ENABLE_FACE_SEARCH_DESCRIPTION": "

如果您启用面部搜索,ente 将从照片中提取脸部几何形状。 这将发生在您的设备上,任何生成的生物测定数据都将是端到端加密的。

请单击此处以在我们的隐私政策中了解有关此功能的更多详细信息

", + "ENABLE_FACE_SEARCH_DESCRIPTION": "

如果您启用面部搜索,Ente 将从照片中提取脸部几何形状。 这将发生在您的设备上,任何生成的生物测定数据都将是端到端加密的。

请单击此处以在我们的隐私政策中了解有关此功能的更多详细信息

", "DISABLE_BETA": "禁用beta", "DISABLE_FACE_SEARCH": "禁用面部搜索", "DISABLE_FACE_SEARCH_TITLE": "要禁用面部搜索吗?", - "DISABLE_FACE_SEARCH_DESCRIPTION": "

ente 将停止处理面部的几何形状, 并将禁用 ML 搜索 (测试版)

如果您愿意,您可以重新启用面部搜索,因此该操作是安全的。

", + "DISABLE_FACE_SEARCH_DESCRIPTION": "

Ente 将停止处理面部的几何形状, 并将禁用 ML 搜索 (测试版)

如果您愿意,您可以重新启用面部搜索,因此该操作是安全的。

", "ADVANCED": "高级设置", - "FACE_SEARCH_CONFIRMATION": "我理解,并希望允许ente处理面部几何形状", + "FACE_SEARCH_CONFIRMATION": "我理解,并希望允许Ente处理面部几何形状", "LABS": "实验室", "YOURS": "你的", "PASSPHRASE_STRENGTH_WEAK": "密码强度:较弱", @@ -597,7 +597,7 @@ "LIVE_PHOTO": "实况照片", "CONVERT": "转换", "CONFIRM_EDITOR_CLOSE_MESSAGE": "您确定要关闭编辑器吗?", - "CONFIRM_EDITOR_CLOSE_DESCRIPTION": "下载已编辑的图片或将副本保存到 ente 以保留您的更改。", + "CONFIRM_EDITOR_CLOSE_DESCRIPTION": "下载已编辑的图片或将副本保存到 Ente 以保留您的更改。", "BRIGHTNESS": "亮度", "CONTRAST": "对比度", "SATURATION": "饱和度", @@ -610,7 +610,7 @@ "FLIP_VERTICALLY": "垂直翻转", "FLIP_HORIZONTALLY": "水平翻转", "DOWNLOAD_EDITED": "下载已编辑图片", - "SAVE_A_COPY_TO_ENTE": "保存副本到 ente", + "SAVE_A_COPY_TO_ENTE": "保存副本到 Ente", "RESTORE_ORIGINAL": "复原", "TRANSFORM": "转换", "COLORS": "颜色", diff --git a/web/apps/photos/src/components/Collections/CollectionOptions/AlbumCastDialog.tsx b/web/apps/photos/src/components/Collections/CollectionOptions/AlbumCastDialog.tsx index f477ae9bf6..05384895f1 100644 --- a/web/apps/photos/src/components/Collections/CollectionOptions/AlbumCastDialog.tsx +++ b/web/apps/photos/src/components/Collections/CollectionOptions/AlbumCastDialog.tsx @@ -225,7 +225,8 @@ export default function AlbumCastDialog(props: Props) { diff --git a/web/apps/photos/src/components/ExportInProgress.tsx b/web/apps/photos/src/components/ExportInProgress.tsx index 3324be5c41..ce2da895c0 100644 --- a/web/apps/photos/src/components/ExportInProgress.tsx +++ b/web/apps/photos/src/components/ExportInProgress.tsx @@ -7,11 +7,11 @@ import { Button, DialogActions, DialogContent, + LinearProgress, styled, } from "@mui/material"; import { ExportStage } from "constants/export"; import { t } from "i18next"; -import { ProgressBar } from "react-bootstrap"; import { Trans } from "react-i18next"; import { ExportProgress } from "types/export"; @@ -69,21 +69,19 @@ export default function ExportInProgress(props: Props) { )} - + {showIndeterminateProgress() ? ( + + ) : ( + + )} diff --git a/web/apps/photos/src/components/ExportModal.tsx b/web/apps/photos/src/components/ExportModal.tsx index 142c00743d..211a70b1b1 100644 --- a/web/apps/photos/src/components/ExportModal.tsx +++ b/web/apps/photos/src/components/ExportModal.tsx @@ -98,8 +98,8 @@ export default function ExportModal(props: Props) { // HELPER FUNCTIONS // ======================= - const verifyExportFolderExists = () => { - if (!exportService.exportFolderExists(exportFolder)) { + const verifyExportFolderExists = async () => { + if (!(await exportService.exportFolderExists(exportFolder))) { appContext.setDialogMessage( getExportDirectoryDoesNotExistMessage(), ); @@ -109,7 +109,7 @@ export default function ExportModal(props: Props) { const syncExportRecord = async (exportFolder: string): Promise => { try { - if (!exportService.exportFolderExists(exportFolder)) { + if (!(await exportService.exportFolderExists(exportFolder))) { const pendingExports = await exportService.getPendingExports(null); setPendingExports(pendingExports); @@ -145,9 +145,9 @@ export default function ExportModal(props: Props) { } }; - const toggleContinuousExport = () => { + const toggleContinuousExport = async () => { try { - verifyExportFolderExists(); + await verifyExportFolderExists(); const newContinuousExport = !continuousExport; if (newContinuousExport) { exportService.enableContinuousExport(); @@ -162,7 +162,7 @@ export default function ExportModal(props: Props) { const startExport = async () => { try { - verifyExportFolderExists(); + await verifyExportFolderExists(); await exportService.scheduleExport(); } catch (e) { if (e.message !== CustomError.EXPORT_FOLDER_DOES_NOT_EXIST) { diff --git a/web/apps/photos/src/components/FixCreationTime.tsx b/web/apps/photos/src/components/FixCreationTime.tsx new file mode 100644 index 0000000000..62c31539a7 --- /dev/null +++ b/web/apps/photos/src/components/FixCreationTime.tsx @@ -0,0 +1,264 @@ +import DialogBox from "@ente/shared/components/DialogBox/"; +import { + Button, + FormControl, + FormControlLabel, + FormLabel, + LinearProgress, + Radio, + RadioGroup, +} from "@mui/material"; +import { ComfySpan } from "components/ExportInProgress"; +import { useFormik } from "formik"; +import { t } from "i18next"; +import { GalleryContext } from "pages/gallery"; +import React, { useContext, useEffect, useState } from "react"; +import { updateCreationTimeWithExif } from "services/updateCreationTimeWithExif"; +import { EnteFile } from "types/file"; +import EnteDateTimePicker from "./EnteDateTimePicker"; + +export interface FixCreationTimeAttributes { + files: EnteFile[]; +} + +type Step = "running" | "completed" | "completed-with-errors"; + +export type FixOption = + | "date-time-original" + | "date-time-digitized" + | "metadata-date" + | "custom-time"; + +interface FormValues { + option: FixOption; + /** + * Date.toISOString() + * + * Formik doesn't have native support for JS dates, so we instead keep the + * corresponding date's ISO string representation as the form state. + */ + customTimeString: string; +} + +interface FixCreationTimeProps { + isOpen: boolean; + show: () => void; + hide: () => void; + attributes: FixCreationTimeAttributes; +} + +const FixCreationTime: React.FC = (props) => { + const [step, setStep] = useState(); + const [progressTracker, setProgressTracker] = useState({ + current: 0, + total: 0, + }); + + const galleryContext = useContext(GalleryContext); + + useEffect(() => { + // TODO (MR): Not sure why this is needed + if (props.attributes && props.isOpen && step !== "running") { + setStep(undefined); + } + }, [props.isOpen]); + + const onSubmit = async (values: FormValues) => { + console.log({ values }); + setStep("running"); + const completedWithErrors = await updateCreationTimeWithExif( + props.attributes.files, + values.option, + new Date(values.customTimeString), + setProgressTracker, + ); + setStep(completedWithErrors ? "completed-with-errors" : "completed"); + await galleryContext.syncWithRemote(); + }; + + const title = + step === "running" + ? t("FIX_CREATION_TIME_IN_PROGRESS") + : t("FIX_CREATION_TIME"); + + const message = messageForStep(step); + + if (!props.attributes) { + return <>; + } + + return ( + +
+ {message &&
{message}
} + + {step === "running" && ( + + )} + + +
+
+ ); +}; + +export default FixCreationTime; + +const messageForStep = (step?: Step) => { + switch (step) { + case undefined: + return undefined; + case "running": + return undefined; + case "completed": + return t("UPDATE_CREATION_TIME_COMPLETED"); + case "completed-with-errors": + return t("UPDATE_CREATION_TIME_COMPLETED_WITH_ERROR"); + } +}; + +interface OptionsFormProps { + step?: Step; + onSubmit: (values: FormValues) => void | Promise; + hide: () => void; +} + +const OptionsForm: React.FC = ({ step, onSubmit, hide }) => { + const { values, handleChange, handleSubmit } = useFormik({ + initialValues: { + option: "date-time-original", + customTimeString: new Date().toISOString(), + }, + validateOnBlur: false, + onSubmit, + }); + + return ( + <> + {(step === undefined || step === "completed-with-errors") && ( +
+
+ + + {t("UPDATE_CREATION_TIME_NOT_STARTED")} + + + + } + label={t("DATE_TIME_ORIGINAL")} + /> + } + label={t("DATE_TIME_DIGITIZED")} + /> + } + label={t("METADATA_DATE")} + /> + } + label={t("CUSTOM_TIME")} + /> + + {values.option === "custom-time" && ( + + handleChange("customTimeString")( + d.toISOString(), + ) + } + /> + )} + +
+ )} +