diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..57680f9dc --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +# Set line endings of shell scripts to LF, even on Windows, otherwise execution +# within Docker fails. +*.sh text eol=lf diff --git a/.github/workflows/auth-crowdin.yml b/.github/workflows/auth-crowdin.yml index ea67dec7c..f269a8c8d 100644 --- a/.github/workflows/auth-crowdin.yml +++ b/.github/workflows/auth-crowdin.yml @@ -3,15 +3,16 @@ name: "Sync Crowdin translations (auth)" on: push: paths: - # Run action when auth's intl_en.arb is changed + # Run workflow when auth's intl_en.arb is changed - "mobile/lib/l10n/arb/app_en.arb" # Or the workflow itself is changed - ".github/workflows/auth-crowdin.yml" branches: [main] schedule: - # Run every 24 hours - https://crontab.guru/#0_*/24_*_*_* - - cron: "0 */24 * * *" - workflow_dispatch: # Allow manually running the action + # See: [Note: Run workflow on specific days of the week] + - cron: "50 1 * * 2,5" + # Also allow manually running the workflow + workflow_dispatch: jobs: synchronize-with-crowdin: diff --git a/.github/workflows/auth-lint.yml b/.github/workflows/auth-lint.yml index fff7fb75f..1b45a2d32 100644 --- a/.github/workflows/auth-lint.yml +++ b/.github/workflows/auth-lint.yml @@ -1,11 +1,9 @@ name: "Lint (auth)" on: - # Run on every push to branches (this also covers pull requests) + # Run on every push to a branch other than main that changes auth/ push: - # See: [Note: Specify branch when specifying a path filter] - branches: ["**"] - # Only run if something changes in these paths + branches-ignore: [main] paths: - "auth/**" - ".github/workflows/auth-lint.yml" diff --git a/.github/workflows/auth-release.yml b/.github/workflows/auth-release.yml index b95b347cf..7f56f1220 100644 --- a/.github/workflows/auth-release.yml +++ b/.github/workflows/auth-release.yml @@ -122,7 +122,7 @@ jobs: with: serviceAccountJsonPlainText: ${{ secrets.SERVICE_ACCOUNT_JSON }} packageName: io.ente.auth - releaseFiles: build/app/outputs/bundle/playstoreRelease/app-playstore-release.aab + releaseFiles: auth/build/app/outputs/bundle/playstoreRelease/app-playstore-release.aab track: internal build-windows: diff --git a/.github/workflows/cli-release.yml b/.github/workflows/cli-release.yml index dabfc8dcf..e1793fa8d 100644 --- a/.github/workflows/cli-release.yml +++ b/.github/workflows/cli-release.yml @@ -47,5 +47,8 @@ jobs: release_name: ${{ github.ref_name }} goversion: "1.20" project_path: "./cli" + pre_command: export CGO_ENABLED=0 + build_flags: "-trimpath" + ldflags: "-X main.AppVersion=${{ github.ref_name }} -s -w" md5sum: false sha256sum: true diff --git a/.github/workflows/docs-deploy.yml b/.github/workflows/docs-deploy.yml new file mode 100644 index 000000000..01b0c2254 --- /dev/null +++ b/.github/workflows/docs-deploy.yml @@ -0,0 +1,47 @@ +name: "Deploy (docs)" + +on: + # Run on every push to main that changes docs/ + push: + branches: [main] + paths: + - "docs/**" + - ".github/workflows/docs-deploy.yml" + # Also allow manually running the workflow + workflow_dispatch: + +jobs: + deploy: + runs-on: ubuntu-latest + + defaults: + run: + working-directory: docs + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup node and enable yarn caching + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: "yarn" + cache-dependency-path: "docs/yarn.lock" + + - name: Install dependencies + run: yarn install + + - name: Build production site + # Will create docs/.vitepress/dist + run: yarn build + + - name: Publish + uses: cloudflare/pages-action@1 + with: + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + projectName: ente + branch: help + directory: docs/docs/.vitepress/dist + wranglerVersion: "3" diff --git a/.github/workflows/docs-verify-build.yml b/.github/workflows/docs-verify-build.yml new file mode 100644 index 000000000..addb52a05 --- /dev/null +++ b/.github/workflows/docs-verify-build.yml @@ -0,0 +1,37 @@ +name: "Verify build (docs)" + +# Preflight build of docs. This allows us to ensure that yarn build is +# succeeding before we merge the PR into main. + +on: + # Run on every push to a branch other than main that changes docs/ + push: + branches-ignore: [main] + paths: + - "docs/**" + - ".github/workflows/docs-verify-build.yml" + +jobs: + verify-build: + runs-on: ubuntu-latest + + defaults: + run: + working-directory: docs + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup node and enable yarn caching + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: "yarn" + cache-dependency-path: "docs/yarn.lock" + + - name: Install dependencies + run: yarn install + + - name: Build production site + run: yarn build diff --git a/.github/workflows/mobile-crowdin.yml b/.github/workflows/mobile-crowdin.yml index dbd978745..35b4c3876 100644 --- a/.github/workflows/mobile-crowdin.yml +++ b/.github/workflows/mobile-crowdin.yml @@ -3,15 +3,16 @@ name: "Sync Crowdin translations (mobile)" on: push: paths: - # Run action when mobiles's intl_en.arb is changed + # Run workflow when mobiles's intl_en.arb is changed - "mobile/lib/l10n/intl_en.arb" # Or the workflow itself is changed - ".github/workflows/mobile-crowdin.yml" branches: [main] schedule: - # Run every 24 hours - https://crontab.guru/#0_*/24_*_*_* - - cron: "0 */24 * * *" - workflow_dispatch: # Allow manually running the action + # See: [Note: Run workflow on specific days of the week] + - cron: "40 1 * * 2,5" + # Also allow manually running the workflow + workflow_dispatch: jobs: synchronize-with-crowdin: diff --git a/.github/workflows/mobile-lint.yml b/.github/workflows/mobile-lint.yml index c2e54d38a..cbbbbcfbb 100644 --- a/.github/workflows/mobile-lint.yml +++ b/.github/workflows/mobile-lint.yml @@ -1,11 +1,9 @@ name: "Lint (mobile)" on: - # Run on every push (this also covers pull requests) + # Run on every push to a branch other than main that changes mobile/ push: - # See: [Note: Specify branch when specifying a path filter] - branches: ["**"] - # Only run if something changes in these paths + branches-ignore: [main, f-droid] paths: - "mobile/**" - ".github/workflows/mobile-lint.yml" diff --git a/.github/workflows/mobile-release.yml b/.github/workflows/mobile-release.yml index 3868da64a..3ce4db8d2 100644 --- a/.github/workflows/mobile-release.yml +++ b/.github/workflows/mobile-release.yml @@ -39,7 +39,9 @@ jobs: encodedString: ${{ secrets.SIGNING_KEY_PHOTOS }} - name: Build independent APK - run: flutter build apk --release --flavor independent && mv build/app/outputs/flutter-apk/app-independent-release.apk build/app/outputs/flutter-apk/ente.apk + run: | + flutter build apk --release --flavor independent + mv build/app/outputs/flutter-apk/app-independent-release.apk build/app/outputs/flutter-apk/ente-${{ github.ref_name }}.apk env: SIGNING_KEY_PATH: "/home/runner/work/_temp/keystore/ente_photos_key.jks" SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS_PHOTOS }} @@ -47,10 +49,10 @@ jobs: SIGNING_STORE_PASSWORD: ${{ secrets.SIGNING_STORE_PASSWORD_PHOTOS }} - name: Checksum - run: sha256sum build/app/outputs/flutter-apk/ente.apk > build/app/outputs/flutter-apk/sha256sum + run: sha256sum build/app/outputs/flutter-apk/ente-${{ github.ref_name }}.apk > build/app/outputs/flutter-apk/sha256sum - name: Create a draft GitHub release uses: ncipollo/release-action@v1 with: - artifacts: "mobile/build/app/outputs/flutter-apk/ente.apk,mobile/build/app/outputs/flutter-apk/sha256sum" + artifacts: "mobile/build/app/outputs/flutter-apk/ente-${{ github.ref_name }}.apk,mobile/build/app/outputs/flutter-apk/sha256sum" draft: true diff --git a/.github/workflows/server-lint.yml b/.github/workflows/server-lint.yml index e23036a6e..c051d0290 100644 --- a/.github/workflows/server-lint.yml +++ b/.github/workflows/server-lint.yml @@ -1,11 +1,9 @@ name: "Lint (server)" on: - # Run on every push (this also covers pull requests) + # Run on every push to a branch other than main that changes server/ push: - # See: [Note: Specify branch when specifying a path filter] - branches: ["**"] - # Only run if something changes in these paths + branches-ignore: [main] paths: - "server/**" - ".github/workflows/server-lint.yml" diff --git a/.github/workflows/web-crowdin.yml b/.github/workflows/web-crowdin.yml index b55aad55d..8733167d6 100644 --- a/.github/workflows/web-crowdin.yml +++ b/.github/workflows/web-crowdin.yml @@ -3,15 +3,22 @@ name: "Sync Crowdin translations (web)" on: push: paths: - # Run action when web's en-US/translation.json is changed + # Run workflow when web's en-US/translation.json is changed - "web/apps/photos/public/locales/en-US/translation.json" # Or the workflow itself is changed - ".github/workflows/web-crowdin.yml" branches: [main] schedule: - # Run every 24 hours - https://crontab.guru/#0_*/24_*_*_* - - cron: "0 */24 * * *" - workflow_dispatch: # Allow manually running the action + # [Note: Run workflow on specific days of the week] + # + # The last (5th) component of the cron syntax denotes the day of the + # week, with 0 == SUN and 6 == SAT. So, for example, to run on every TUE + # and FRI, this can be set to `2,5`. + # + # See also: [Note: Run workflow every 24 hours] + - cron: "20 1 * * 2,5" + # Also allow manually running the workflow + workflow_dispatch: jobs: synchronize-with-crowdin: diff --git a/.github/workflows/web-deploy-accounts.yml b/.github/workflows/web-deploy-accounts.yml new file mode 100644 index 000000000..8164aea44 --- /dev/null +++ b/.github/workflows/web-deploy-accounts.yml @@ -0,0 +1,43 @@ +name: "Deploy (accounts)" + +on: + push: + # Run workflow on pushes to the deploy/accounts + branches: [deploy/accounts] + +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 accounts + run: yarn build:accounts + + - name: Publish accounts + uses: cloudflare/pages-action@1 + with: + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + projectName: ente + branch: deploy/accounts + directory: web/apps/accounts/out + wranglerVersion: "3" diff --git a/.github/workflows/web-deploy-auth.yml b/.github/workflows/web-deploy-auth.yml new file mode 100644 index 000000000..63a56b95b --- /dev/null +++ b/.github/workflows/web-deploy-auth.yml @@ -0,0 +1,43 @@ +name: "Deploy (auth)" + +on: + push: + # Run workflow on pushes to the deploy/auth + branches: [deploy/auth] + +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 auth + run: yarn build:auth + + - name: Publish auth + uses: cloudflare/pages-action@1 + with: + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + projectName: ente + branch: deploy/auth + directory: web/apps/auth/out + wranglerVersion: "3" diff --git a/.github/workflows/web-deploy-cast.yml b/.github/workflows/web-deploy-cast.yml new file mode 100644 index 000000000..be4861c71 --- /dev/null +++ b/.github/workflows/web-deploy-cast.yml @@ -0,0 +1,43 @@ +name: "Deploy (cast)" + +on: + push: + # Run workflow on pushes to the deploy/cast + branches: [deploy/cast] + +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 cast + run: yarn build:cast + + - name: Publish cast + uses: cloudflare/pages-action@1 + with: + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + projectName: ente + branch: deploy/cast + directory: web/apps/cast/out + wranglerVersion: "3" diff --git a/.github/workflows/web-deploy-photos.yml b/.github/workflows/web-deploy-photos.yml new file mode 100644 index 000000000..64a88421d --- /dev/null +++ b/.github/workflows/web-deploy-photos.yml @@ -0,0 +1,43 @@ +name: "Deploy (photos)" + +on: + push: + # Run workflow on pushes to the deploy/photos + branches: [deploy/photos] + +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 photos + run: yarn build:photos + + - name: Publish photos + uses: cloudflare/pages-action@1 + with: + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + projectName: ente + branch: deploy/photos + directory: web/apps/photos/out + wranglerVersion: "3" diff --git a/.github/workflows/web-lint.yml b/.github/workflows/web-lint.yml index a905069f6..7f5d27002 100644 --- a/.github/workflows/web-lint.yml +++ b/.github/workflows/web-lint.yml @@ -1,20 +1,9 @@ name: "Lint (web)" on: - # Run on every push (this also covers pull requests) + # Run on every push to a branch other than main that changes web/ push: - # [Note: Specify branch when specifying a path filter] - # - # Path filters are ignored for tag pushes, which causes this workflow to - # always run when we push a tag. Defining an explicit branch solves the - # issue. From GitHub's docs: - # - # > if you define both branches/branches-ignore and paths/paths-ignore, - # > the workflow will only run when both filters are satisfied. - # - # https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions - branches: ["**"] - # Only run if something changes in these paths + branches-ignore: [main] paths: - "web/**" - ".github/workflows/web-lint.yml" diff --git a/.github/workflows/web-nightly.yml b/.github/workflows/web-nightly.yml new file mode 100644 index 000000000..a800a4b73 --- /dev/null +++ b/.github/workflows/web-nightly.yml @@ -0,0 +1,94 @@ +name: "Nightly (web)" + +on: + schedule: + # [Note: Run workflow every 24 hours] + # + # Run every 24 hours - First field is minute, second is hour of the day + # This runs 23:15 UTC everyday - 1 and 15 are just arbitrary offset to + # avoid scheduling it on the exact hour, as suggested by GitHub. + # + # https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#schedule + # https://crontab.guru/ + # + - cron: "15 23 * * *" + # Also allow manually running the workflow + workflow_dispatch: + +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 accounts + run: yarn build:accounts + + - name: Publish accounts + uses: cloudflare/pages-action@1 + with: + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + projectName: ente + branch: n-accounts + directory: web/apps/accounts/out + wranglerVersion: "3" + + - name: Build auth + run: yarn build:auth + + - name: Publish auth + uses: cloudflare/pages-action@1 + with: + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + projectName: ente + branch: n-auth + directory: web/apps/auth/out + wranglerVersion: "3" + + - name: Build cast + run: yarn build:cast + + - name: Publish cast + uses: cloudflare/pages-action@1 + with: + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + projectName: ente + branch: n-cast + directory: web/apps/cast/out + wranglerVersion: "3" + + - name: Build photos + run: yarn build:photos + env: + NEXT_PUBLIC_ENTE_ALBUMS_ENDPOINT: https://albums.ente.sh + + - name: Publish photos + uses: cloudflare/pages-action@1 + with: + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + projectName: ente + branch: n-photos + directory: web/apps/photos/out + wranglerVersion: "3" diff --git a/.github/workflows/web-preview.yml b/.github/workflows/web-preview.yml new file mode 100644 index 000000000..4e86d9a81 --- /dev/null +++ b/.github/workflows/web-preview.yml @@ -0,0 +1,52 @@ +name: "Preview (web)" + +on: + workflow_dispatch: + inputs: + app: + description: "App to build and deploy" + type: choice + required: true + default: "photos" + options: + - "accounts" + - "auth" + - "cast" + - "photos" + +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 ${{ inputs.app }} + run: yarn build:${{ inputs.app }} + + - name: Publish ${{ inputs.app }} to preview + uses: cloudflare/pages-action@1 + with: + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + projectName: ente + branch: preview + directory: web/apps/${{ inputs.app }}/out + wranglerVersion: "3" diff --git a/.gitmodules b/.gitmodules index a3a52e7be..cfea8359b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -9,20 +9,9 @@ [submodule "auth/assets/simple-icons"] path = auth/assets/simple-icons url = https://github.com/simple-icons/simple-icons.git -[submodule "desktop/thirdparty/next-electron-server"] - path = desktop/thirdparty/next-electron-server - url = https://github.com/ente-io/next-electron-server.git - branch = desktop -[submodule "mobile/thirdparty/flutter"] - path = mobile/thirdparty/flutter - url = https://github.com/flutter/flutter.git - branch = stable [submodule "mobile/plugins/clip_ggml"] path = mobile/plugins/clip_ggml url = https://github.com/ente-io/clip-ggml.git -[submodule "mobile/thirdparty/isar"] - path = mobile/thirdparty/isar - url = https://github.com/isar/isar [submodule "web/apps/photos/thirdparty/ffmpeg-wasm"] path = web/apps/photos/thirdparty/ffmpeg-wasm url = https://github.com/abhinavkgrd/ffmpeg.wasm.git diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 22fb8ba19..5042b6712 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -50,13 +50,13 @@ Thank you for your support. ## Document -_Coming soon!_ - The help guides and FAQs for users of Ente products are also open source, and can be edited in a wiki-esque manner by our community members. More than the quantity, we feel this helps improve the quality and approachability of the documentation by bringing in more diverse viewpoints and familiarity levels. +See [docs/](docs/README.md) for how to edit these documents. + ## Code contributions If you'd like to contribute code, it is best to start small. diff --git a/README.md b/README.md index e99a53e86..68632ea3b 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%3Av2.0.34&expanded=true) +[](https://github.com/ente-io/ente/releases?q=tag%3Aauth-v2) [](https://auth.ente.io) diff --git a/SUPPORT.md b/SUPPORT.md index 78de03fcf..b91d593c5 100644 --- a/SUPPORT.md +++ b/SUPPORT.md @@ -7,7 +7,7 @@ details as possible about whatever it is that you need help with, and we will get back to you as soon as possible. In some cases, your query might already have been answered in our help -documentation (_Coming soon!_). +documentation at [help.ente.io](https://help.ente.io). Other ways to get in touch are: diff --git a/auth/.gitignore b/auth/.gitignore index bcfd8fe6b..87abd6a1c 100644 --- a/auth/.gitignore +++ b/auth/.gitignore @@ -9,6 +9,9 @@ .history .svn/ +# Editors +.vscode/ + # IntelliJ related *.iml *.ipr diff --git a/auth/README.md b/auth/README.md index 334c7859e..71ae1f93b 100644 --- a/auth/README.md +++ b/auth/README.md @@ -12,7 +12,7 @@ multi-device sync. ### Android This repository's [GitHub -releases](https://github.com/ente-io/ente/releases/latest/download/ente-auth.apk) +releases](https://github.com/ente-io/ente/releases?q=tag%3Aauth-v2) contains APKs, built straight from source. These builds keep themselves updated, without relying on third party stores. diff --git a/auth/analysis_options.yaml b/auth/analysis_options.yaml index 59ff504e8..de5533ba1 100644 --- a/auth/analysis_options.yaml +++ b/auth/analysis_options.yaml @@ -27,6 +27,7 @@ linter: - use_rethrow_when_possible - directives_ordering - always_use_package_imports + - unawaited_futures analyzer: errors: @@ -45,6 +46,8 @@ analyzer: prefer_const_declarations: warning prefer_const_constructors_in_immutables: ignore # too many warnings + unawaited_futures: warning # convert to warning after fixing existing issues + avoid_renaming_method_parameters: ignore # incorrect warnings for `equals` overrides exclude: diff --git a/auth/assets/custom-icons/_data/custom-icons.json b/auth/assets/custom-icons/_data/custom-icons.json index 7231e7479..b248ad8cb 100644 --- a/auth/assets/custom-icons/_data/custom-icons.json +++ b/auth/assets/custom-icons/_data/custom-icons.json @@ -37,8 +37,7 @@ { "title": "BorgBase", "altNames": ["borg"], - "slug": "BorgBase", - "hex": "222C31" + "slug": "BorgBase" }, { "title": "Brave Creators", @@ -109,8 +108,7 @@ { "title": "Gosuslugi", "altNames": ["Госуслуги"], - "slug": "Gosuslugi", - "hex": "EE2F53" + "slug": "Gosuslugi" }, { "title": "Healthchecks.io", @@ -127,13 +125,11 @@ }, { "title": "IVPN", - "slug": "IVPN", - "hex": "FA3243" + "slug": "IVPN" }, { "title": "IceDrive", - "slug": "Icedrive", - "hex": "1F4FD0" + "slug": "Icedrive" }, { "title": "Jagex", @@ -154,8 +150,7 @@ "title": "Kite" }, { - "title": "Koofr", - "hex": "71BA05" + "title": "Koofr" }, { "title": "Kraken", @@ -184,8 +179,7 @@ { "title": "Murena", "altNames": ["eCloud"], - "slug": "ecloud", - "hex": "EC6A55" + "slug": "ecloud" }, { "title": "Microsoft" @@ -230,8 +224,7 @@ }, { "title": "pCloud", - "slug": "pCloud", - "hex": "1EBCC5" + "slug": "pCloud" }, { "title": "Peerberry", @@ -371,8 +364,7 @@ { "title": "Yandex", "altNames": ["Ya", "Яндекс"], - "slug": "Yandex", - "hex": "FC3F1D" + "slug": "Yandex" } ] } diff --git a/auth/docs/release.md b/auth/docs/release.md index afc80076a..4b31c72f0 100644 --- a/auth/docs/release.md +++ b/auth/docs/release.md @@ -1,7 +1,14 @@ # Releases -Create a PR to bump up the version in `pubspec.yaml`. Once that is merged, tag -main, and push the tag. +Create a PR to bump up the version in `pubspec.yaml`. + +> [!NOTE] +> +> Use [semver](https://semver.org/) for the tags, with `auth-` as a prefix. +> Multiple beta releases for the same upcoming version can be done by adding +> build metadata at the end, e.g. `auth-v1.2.3-beta+3`. + +Once that is merged, tag main, and push the tag. ```sh git tag auth-v1.2.3 @@ -16,6 +23,11 @@ This'll trigger a GitHub workflow that: * Creates a new release in the internal track on Play Store. Once the workflow completes, go to the draft GitHub release that was created. + +> [!NOTE] +> +> Keep the title of the release same as the tag. + Set "Previous tag" to the last release of auth and press "Generate release notes". The generated release note will contain all PRs and new contributors from all the releases in the monorepo, so you'll need to filter them to keep diff --git a/auth/flutter b/auth/flutter index ba3931984..41456452f 160000 --- a/auth/flutter +++ b/auth/flutter @@ -1 +1 @@ -Subproject commit ba393198430278b6595976de84fe170f553cc728 +Subproject commit 41456452f29d64e8deb623a3c927524bcf9f111b diff --git a/auth/ios/Podfile.lock b/auth/ios/Podfile.lock index c70156377..038487635 100644 --- a/auth/ios/Podfile.lock +++ b/auth/ios/Podfile.lock @@ -70,8 +70,6 @@ PODS: - move_to_background (0.0.1): - Flutter - MTBBarcodeScanner (5.0.11) - - open_filex (0.0.2): - - Flutter - OrderedSet (5.0.0) - package_info_plus (0.4.5): - Flutter @@ -143,7 +141,6 @@ DEPENDENCIES: - fluttertoast (from `.symlinks/plugins/fluttertoast/ios`) - local_auth_ios (from `.symlinks/plugins/local_auth_ios/ios`) - move_to_background (from `.symlinks/plugins/move_to_background/ios`) - - open_filex (from `.symlinks/plugins/open_filex/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - privacy_screen (from `.symlinks/plugins/privacy_screen/ios`) @@ -204,8 +201,6 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/local_auth_ios/ios" move_to_background: :path: ".symlinks/plugins/move_to_background/ios" - open_filex: - :path: ".symlinks/plugins/open_filex/ios" package_info_plus: :path: ".symlinks/plugins/package_info_plus/ios" path_provider_foundation: @@ -251,7 +246,6 @@ SPEC CHECKSUMS: local_auth_ios: 1ba1475238daa33a6ffa2a29242558437be435ac move_to_background: 39a5b79b26d577b0372cbe8a8c55e7aa9fcd3a2d MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb - open_filex: 6e26e659846ec990262224a12ef1c528bb4edbe4 OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c package_info_plus: 115f4ad11e0698c8c1c5d8a689390df880f47e85 path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c diff --git a/auth/lib/core/configuration.dart b/auth/lib/core/configuration.dart index 0dc84ba12..096fe91b2 100644 --- a/auth/lib/core/configuration.dart +++ b/auth/lib/core/configuration.dart @@ -6,6 +6,7 @@ import 'dart:typed_data'; import 'package:bip39/bip39.dart' as bip39; import 'package:ente_auth/core/constants.dart'; import 'package:ente_auth/core/event_bus.dart'; +import 'package:ente_auth/events/endpoint_updated_event.dart'; import 'package:ente_auth/events/signed_in_event.dart'; import 'package:ente_auth/events/signed_out_event.dart'; import 'package:ente_auth/models/key_attributes.dart'; @@ -42,6 +43,7 @@ class Configuration { static const userIDKey = "user_id"; static const hasMigratedSecureStorageKey = "has_migrated_secure_storage"; static const hasOptedForOfflineModeKey = "has_opted_for_offline_mode"; + static const endPointKey = "endpoint"; final List onlineSecureKeys = [ keyKey, secretKeyKey, @@ -318,7 +320,12 @@ class Configuration { } String getHttpEndpoint() { - return endpoint; + return _preferences.getString(endPointKey) ?? endpoint; + } + + Future setHttpEndpoint(String endpoint) async { + await _preferences.setString(endPointKey, endpoint); + Bus.instance.fire(EndpointUpdatedEvent()); } String? getToken() { diff --git a/auth/lib/core/logging/super_logging.dart b/auth/lib/core/logging/super_logging.dart index 224ba553a..08c3f475e 100644 --- a/auth/lib/core/logging/super_logging.dart +++ b/auth/lib/core/logging/super_logging.dart @@ -167,7 +167,7 @@ class SuperLogging { await setupLogDir(); } if (sentryIsEnabled) { - setupSentry(); + setupSentry().ignore(); } Logger.root.level = Level.ALL; @@ -250,7 +250,7 @@ class SuperLogging { // add error to sentry queue if (sentryIsEnabled && rec.error != null) { - _sendErrorToSentry(rec.error!, null); + _sendErrorToSentry(rec.error!, null).ignore(); } } @@ -289,7 +289,7 @@ class SuperLogging { SuperLogging.setUserID(await _getOrCreateAnonymousUserID()); await for (final error in sentryQueueControl.stream.asBroadcastStream()) { try { - Sentry.captureException( + await Sentry.captureException( error, ); } catch (e) { diff --git a/auth/lib/core/network.dart b/auth/lib/core/network.dart index 087d6acca..f9d3af41e 100644 --- a/auth/lib/core/network.dart +++ b/auth/lib/core/network.dart @@ -13,11 +13,6 @@ import 'package:uuid/uuid.dart'; int kConnectTimeout = 15000; class Network { - // apiEndpoint points to the Ente server's API endpoint - static const apiEndpoint = String.fromEnvironment( - "endpoint", - defaultValue: kDefaultProductionEndpoint, - ); late Dio _dio; late Dio _enteDio; @@ -41,7 +36,7 @@ class Network { }, ), ); - _dio.interceptors.add(RequestIdInterceptor()); + _enteDio = Dio( BaseOptions( baseUrl: apiEndpoint, @@ -56,7 +51,13 @@ class Network { }, ), ); - _enteDio.interceptors.add(EnteRequestInterceptor(preferences, apiEndpoint)); + _setupInterceptors(endpoint); + + Bus.instance.on().listen((event) { + final endpoint = Configuration.instance.getHttpEndpoint(); + _enteDio.options.baseUrl = endpoint; + _setupInterceptors(endpoint); + }); } Network._privateConstructor(); @@ -65,34 +66,41 @@ class Network { Dio getDio() => _dio; Dio get enteDio => _enteDio; + + void _setupInterceptors(String endpoint) { + _dio.interceptors.clear(); + _dio.interceptors.add(RequestIdInterceptor()); + + _enteDio.interceptors.clear(); + _enteDio.interceptors.add(EnteRequestInterceptor(endpoint)); + } } class RequestIdInterceptor extends Interceptor { @override void onRequest(RequestOptions options, RequestInterceptorHandler handler) { - // ignore: prefer_const_constructors - options.headers.putIfAbsent("x-request-id", () => Uuid().v4().toString()); + options.headers + .putIfAbsent("x-request-id", () => const Uuid().v4().toString()); return super.onRequest(options, handler); } } class EnteRequestInterceptor extends Interceptor { - final SharedPreferences _preferences; - final String enteEndpoint; + final String endpoint; - EnteRequestInterceptor(this._preferences, this.enteEndpoint); + EnteRequestInterceptor(this.endpoint); @override void onRequest(RequestOptions options, RequestInterceptorHandler handler) { if (kDebugMode) { assert( - options.baseUrl == enteEndpoint, + options.baseUrl == endpoint, "interceptor should only be used for API endpoint", ); } - // ignore: prefer_const_constructors - options.headers.putIfAbsent("x-request-id", () => Uuid().v4().toString()); - final String? tokenValue = _preferences.getString(Configuration.tokenKey); + options.headers + .putIfAbsent("x-request-id", () => const Uuid().v4().toString()); + final String? tokenValue = Configuration.instance.getToken(); if (tokenValue != null) { options.headers.putIfAbsent("X-Auth-Token", () => tokenValue); } diff --git a/auth/lib/events/endpoint_updated_event.dart b/auth/lib/events/endpoint_updated_event.dart new file mode 100644 index 000000000..0a9915479 --- /dev/null +++ b/auth/lib/events/endpoint_updated_event.dart @@ -0,0 +1,3 @@ +import 'package:ente_auth/events/event.dart'; + +class EndpointUpdatedEvent extends Event {} diff --git a/auth/lib/gateway/authenticator.dart b/auth/lib/gateway/authenticator.dart index c51b84db5..3f3c7c897 100644 --- a/auth/lib/gateway/authenticator.dart +++ b/auth/lib/gateway/authenticator.dart @@ -1,43 +1,29 @@ import 'package:dio/dio.dart'; -import 'package:ente_auth/core/configuration.dart'; import 'package:ente_auth/core/errors.dart'; +import 'package:ente_auth/core/network.dart'; import 'package:ente_auth/models/authenticator/auth_entity.dart'; import 'package:ente_auth/models/authenticator/auth_key.dart'; class AuthenticatorGateway { - final Dio _dio; - final Configuration _config; - late String _basedEndpoint; + late Dio _enteDio; - AuthenticatorGateway(this._dio, this._config) { - _basedEndpoint = "${_config.getHttpEndpoint()}/authenticator"; + AuthenticatorGateway() { + _enteDio = Network.instance.enteDio; } Future createKey(String encKey, String header) async { - await _dio.post( - "$_basedEndpoint/key", + await _enteDio.post( + "/authenticator/key", data: { "encryptedKey": encKey, "header": header, }, - options: Options( - headers: { - "X-Auth-Token": _config.getToken(), - }, - ), ); } Future getKey() async { try { - final response = await _dio.get( - "$_basedEndpoint/key", - options: Options( - headers: { - "X-Auth-Token": _config.getToken(), - }, - ), - ); + final response = await _enteDio.get("/authenticator/key"); return AuthKey.fromMap(response.data); } on DioException catch (e) { if (e.response != null && (e.response!.statusCode ?? 0) == 404) { @@ -51,17 +37,12 @@ class AuthenticatorGateway { } Future createEntity(String encryptedData, String header) async { - final response = await _dio.post( - "$_basedEndpoint/entity", + final response = await _enteDio.post( + "/authenticator/entity", data: { "encryptedData": encryptedData, "header": header, }, - options: Options( - headers: { - "X-Auth-Token": _config.getToken(), - }, - ), ); return AuthEntity.fromMap(response.data); } @@ -71,50 +52,35 @@ class AuthenticatorGateway { String encryptedData, String header, ) async { - await _dio.put( - "$_basedEndpoint/entity", + await _enteDio.put( + "/authenticator/entity", data: { "id": id, "encryptedData": encryptedData, "header": header, }, - options: Options( - headers: { - "X-Auth-Token": _config.getToken(), - }, - ), ); } Future deleteEntity( String id, ) async { - await _dio.delete( - "$_basedEndpoint/entity", + await _enteDio.delete( + "/authenticator/entity", queryParameters: { "id": id, }, - options: Options( - headers: { - "X-Auth-Token": _config.getToken(), - }, - ), ); } Future> getDiff(int sinceTime, {int limit = 500}) async { try { - final response = await _dio.get( - "$_basedEndpoint/entity/diff", + final response = await _enteDio.get( + "/authenticator/entity/diff", queryParameters: { "sinceTime": sinceTime, "limit": limit, }, - options: Options( - headers: { - "X-Auth-Token": _config.getToken(), - }, - ), ); final List authEntities = []; final diff = response.data["diff"] as List; diff --git a/auth/lib/l10n/arb/app_bg.arb b/auth/lib/l10n/arb/app_bg.arb new file mode 100644 index 000000000..9e26dfeeb --- /dev/null +++ b/auth/lib/l10n/arb/app_bg.arb @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/auth/lib/l10n/arb/app_de.arb b/auth/lib/l10n/arb/app_de.arb index 6f9382540..a05c7ca0d 100644 --- a/auth/lib/l10n/arb/app_de.arb +++ b/auth/lib/l10n/arb/app_de.arb @@ -144,6 +144,7 @@ "enterCodeHint": "Geben Sie den 6-stelligen Code \naus Ihrer Authentifikator-App ein.", "lostDeviceTitle": "Gerät verloren?", "twoFactorAuthTitle": "Zwei-Faktor-Authentifizierung", + "passkeyAuthTitle": "Passkey Authentifizierung", "recoverAccount": "Konto wiederherstellen", "enterRecoveryKeyHint": "Geben Sie Ihren Wiederherstellungsschlüssel ein", "recover": "Wiederherstellen", @@ -404,5 +405,15 @@ "signOutOtherDevices": "Andere Geräte abmelden", "doNotSignOut": "Nicht abmelden", "hearUsWhereTitle": "Wie hast du von Ente erfahren? (optional)", - "hearUsExplanation": "Wir tracken keine App-Installationen. Es würde uns jedoch helfen, wenn du uns mitteilst, wie du von uns erfahren hast!" + "hearUsExplanation": "Wir tracken keine App-Installationen. Es würde uns jedoch helfen, wenn du uns mitteilst, wie du von uns erfahren hast!", + "waitingForBrowserRequest": "Warten auf Browseranfrage...", + "launchPasskeyUrlAgain": "Passwort-URL erneut starten", + "passkey": "Passkey", + "developerSettingsWarning": "Sind Sie sicher, dass Sie die Entwicklereinstellungen ändern möchten?", + "developerSettings": "Entwicklereinstellungen", + "serverEndpoint": "Server Endpunkt", + "invalidEndpoint": "Ungültiger Endpunkt", + "invalidEndpointMessage": "Der eingegebene Endpunkt ist ungültig. Bitte geben Sie einen gültigen Endpunkt ein und versuchen Sie es erneut.", + "endpointUpdatedMessage": "Endpunkt erfolgreich aktualisiert", + "customEndpoint": "Mit {endpoint} verbunden" } \ No newline at end of file diff --git a/auth/lib/l10n/arb/app_en.arb b/auth/lib/l10n/arb/app_en.arb index 7af9ce901..a2f6e77ed 100644 --- a/auth/lib/l10n/arb/app_en.arb +++ b/auth/lib/l10n/arb/app_en.arb @@ -144,7 +144,8 @@ "enterCodeHint": "Enter the 6-digit code from\nyour authenticator app", "lostDeviceTitle": "Lost device?", "twoFactorAuthTitle": "Two-factor authentication", - "passkeyAuthTitle": "Passkey authentication", + "passkeyAuthTitle": "Passkey verification", + "verifyPasskey": "Verify passkey", "recoverAccount": "Recover account", "enterRecoveryKeyHint": "Enter your recovery key", "recover": "Recover", @@ -412,6 +413,13 @@ "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...", - "launchPasskeyUrlAgain": "Launch passkey URL again", - "passkey": "Passkey" + "waitingForVerification": "Waiting for verification...", + "passkey": "Passkey", + "developerSettingsWarning":"Are you sure that you want to modify Developer settings?", + "developerSettings": "Developer settings", + "serverEndpoint": "Server endpoint", + "invalidEndpoint": "Invalid endpoint", + "invalidEndpointMessage": "Sorry, the endpoint you entered is invalid. Please enter a valid endpoint and try again.", + "endpointUpdatedMessage": "Endpoint updated successfully", + "customEndpoint": "Connected to {endpoint}" } \ No newline at end of file diff --git a/auth/lib/l10n/arb/app_ja.arb b/auth/lib/l10n/arb/app_ja.arb index 104da4a22..ed1786f71 100644 --- a/auth/lib/l10n/arb/app_ja.arb +++ b/auth/lib/l10n/arb/app_ja.arb @@ -144,6 +144,7 @@ "enterCodeHint": "認証アプリに表示された 6 桁のコードを入力してください", "lostDeviceTitle": "デバイスを紛失しましたか?", "twoFactorAuthTitle": "2 要素認証", + "passkeyAuthTitle": "パスキー認証", "recoverAccount": "アカウントを回復", "enterRecoveryKeyHint": "回復キーを入力", "recover": "回復", @@ -404,5 +405,15 @@ "signOutOtherDevices": "他のデバイスからサインアウトする", "doNotSignOut": "サインアウトしない", "hearUsWhereTitle": "Ente についてどのようにお聞きになりましたか?(任意)", - "hearUsExplanation": "私たちはアプリのインストールを追跡していません。私たちをお知りになった場所を教えてください!" + "hearUsExplanation": "私たちはアプリのインストールを追跡していません。私たちをお知りになった場所を教えてください!", + "waitingForBrowserRequest": "ブラウザのリクエストを待っています...", + "launchPasskeyUrlAgain": "パスキーのURLを再度起動する", + "passkey": "パスキー", + "developerSettingsWarning": "開発者向け設定を変更してもよろしいですか?", + "developerSettings": "開発者向け設定", + "serverEndpoint": "サーバーエンドポイント", + "invalidEndpoint": "無効なエンドポイントです", + "invalidEndpointMessage": "入力されたエンドポイントは無効です。有効なエンドポイントを入力して再試行してください。", + "endpointUpdatedMessage": "エンドポイントの更新に成功しました", + "customEndpoint": "{endpoint} に接続しました" } \ 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 a3f5262e7..10c34ab29 100644 --- a/auth/lib/l10n/arb/app_pt.arb +++ b/auth/lib/l10n/arb/app_pt.arb @@ -144,6 +144,7 @@ "enterCodeHint": "Digite o código de 6 dígitos de\nseu aplicativo autenticador", "lostDeviceTitle": "Perdeu seu dispositivo?", "twoFactorAuthTitle": "Autenticação de dois fatores", + "passkeyAuthTitle": "Autenticação via Chave de acesso", "recoverAccount": "Recuperar conta", "enterRecoveryKeyHint": "Digite sua chave de recuperação", "recover": "Recuperar", @@ -404,5 +405,15 @@ "signOutOtherDevices": "Terminar sessão em outros dispositivos", "doNotSignOut": "Não encerrar sessão", "hearUsWhereTitle": "Como você ouviu sobre o Ente? (opcional)", - "hearUsExplanation": "Não rastreamos instalações do aplicativo. Seria útil se você nos contasse onde nos encontrou!" + "hearUsExplanation": "Não rastreamos instalações do aplicativo. Seria útil se você nos contasse onde nos encontrou!", + "waitingForBrowserRequest": "Aguardando solicitação do navegador...", + "launchPasskeyUrlAgain": "Iniciar a URL de chave de acesso novamente", + "passkey": "Chave de acesso", + "developerSettingsWarning": "Tem certeza de que deseja modificar as configurações de Desenvolvedor?", + "developerSettings": "Configurações de desenvolvedor", + "serverEndpoint": "Endpoint do servidor", + "invalidEndpoint": "Endpoint inválido", + "invalidEndpointMessage": "Desculpe, o endpoint que você inseriu é inválido. Por favor, insira um endpoint válido e tente novamente.", + "endpointUpdatedMessage": "Endpoint atualizado com sucesso", + "customEndpoint": "Conectado a {endpoint}" } \ No newline at end of file diff --git a/auth/lib/l10n/arb/app_sv.arb b/auth/lib/l10n/arb/app_sv.arb index 13fc74fb8..d99ed5f5f 100644 --- a/auth/lib/l10n/arb/app_sv.arb +++ b/auth/lib/l10n/arb/app_sv.arb @@ -59,6 +59,12 @@ "recreatePassword": "Återskapa lösenord", "useRecoveryKey": "Använd återställningsnyckel", "incorrectPasswordTitle": "Felaktigt lösenord", + "welcomeBack": "Välkommen tillbaka!", + "changePassword": "Ändra lösenord", + "cancel": "Avbryt", + "yes": "Ja", + "no": "Nej", + "settings": "Inställningar", "pleaseTryAgain": "Försök igen", "existingUser": "Befintlig användare", "delete": "Radera", @@ -68,9 +74,23 @@ "suggestFeatures": "Föreslå funktionalitet", "faq": "FAQ", "faq_q_1": "Hur säkert är ente Auth?", + "scan": "Skanna", + "twoFactorAuthTitle": "Tvåfaktorsautentisering", + "enterRecoveryKeyHint": "Ange din återställningsnyckel", + "noRecoveryKeyTitle": "Ingen återställningsnyckel?", + "enterEmailHint": "Ange din e-postadress", + "invalidEmailTitle": "Ogiltig e-postadress", + "invalidEmailMessage": "Ange en giltig e-postadress.", + "deleteAccount": "Radera konto", + "yesSendFeedbackAction": "Ja, skicka feedback", + "noDeleteAccountAction": "Nej, radera konto", + "createNewAccount": "Skapa nytt konto", "weakStrength": "Svag", "strongStrength": "Stark", "moderateStrength": "Måttligt", + "confirmPassword": "Bekräfta lösenord", + "close": "Stäng", + "language": "Språk", "searchHint": "Sök...", "search": "Sök", "sorryUnableToGenCode": "Tyvärr, det gick inte att generera en kod för {issuerName}", @@ -83,5 +103,57 @@ "copiedNextToClipboard": "Kopierade nästa kod till urklipp", "error": "Fel", "recoveryKeyCopiedToClipboard": "Återställningsnyckel kopierad till urklipp", - "recoveryKeyOnForgotPassword": "Om du glömmer ditt lösenord är det enda sättet du kan återställa dina data med denna nyckel." + "recoveryKeyOnForgotPassword": "Om du glömmer ditt lösenord är det enda sättet du kan återställa dina data med denna nyckel.", + "saveKey": "Spara nyckel", + "back": "Tillbaka", + "createAccount": "Skapa konto", + "password": "Lösenord", + "privacyPolicyTitle": "Integritetspolicy", + "termsOfServicesTitle": "Villkor", + "encryption": "Kryptering", + "changePasswordTitle": "Ändra lösenord", + "resetPasswordTitle": "Återställ lösenord", + "encryptionKeys": "Krypteringsnycklar", + "continueLabel": "Fortsätt", + "logInLabel": "Logga in", + "logout": "Logga ut", + "areYouSureYouWantToLogout": "Är du säker på att du vill logga ut?", + "yesLogout": "Ja, logga ut", + "invalidKey": "Ogiltig nyckel", + "tryAgain": "Försök igen", + "viewRecoveryKey": "Visa återställningsnyckel", + "confirmRecoveryKey": "Bekräfta återställningsnyckel", + "confirmYourRecoveryKey": "Bekräfta din återställningsnyckel", + "confirm": "Bekräfta", + "copyEmailAddress": "Kopiera e-postadress", + "exportLogs": "Exportera loggar", + "enterYourRecoveryKey": "Ange din återställningsnyckel", + "about": "Om", + "terms": "Villkor", + "warning": "Varning", + "importSuccessDesc": "Du har importerat {count} koder!", + "@importSuccessDesc": { + "placeholders": { + "count": { + "description": "The number of codes imported", + "type": "int", + "example": "1" + } + } + }, + "pendingSyncs": "Varning", + "activeSessions": "Aktiva sessioner", + "enterPassword": "Ange lösenord", + "export": "Exportera", + "singIn": "Logga in", + "androidCancelButton": "Avbryt", + "@androidCancelButton": { + "description": "Message showed on a button that the user can click to leave the current dialog. It is used on Android side. Maximum 30 characters." + }, + "iOSOkButton": "OK", + "@iOSOkButton": { + "description": "Message showed on a button that the user can click to leave the current dialog. It is used on iOS side. Maximum 30 characters." + }, + "noInternetConnection": "Ingen internetanslutning", + "pleaseCheckYourInternetConnectionAndTryAgain": "Kontrollera din internetanslutning och försök igen." } \ No newline at end of file diff --git a/auth/lib/l10n/arb/app_zh.arb b/auth/lib/l10n/arb/app_zh.arb index 0c98dab29..b6d4ed244 100644 --- a/auth/lib/l10n/arb/app_zh.arb +++ b/auth/lib/l10n/arb/app_zh.arb @@ -144,6 +144,7 @@ "enterCodeHint": "从你的身份验证器应用中\n输入6位数字代码", "lostDeviceTitle": "丢失了设备吗?", "twoFactorAuthTitle": "双因素认证", + "passkeyAuthTitle": "通行密钥认证", "recoverAccount": "恢复账户", "enterRecoveryKeyHint": "输入您的恢复密钥", "recover": "恢复", @@ -404,5 +405,15 @@ "signOutOtherDevices": "登出其他设备", "doNotSignOut": "不要退登", "hearUsWhereTitle": "您是如何知道Ente的? (可选的)", - "hearUsExplanation": "我们不跟踪应用程序安装情况。如果您告诉我们您是在哪里找到我们的,将会有所帮助!" + "hearUsExplanation": "我们不跟踪应用程序安装情况。如果您告诉我们您是在哪里找到我们的,将会有所帮助!", + "waitingForBrowserRequest": "正在等待浏览器请求...", + "launchPasskeyUrlAgain": "再次启动 通行密钥 URL", + "passkey": "通行密钥", + "developerSettingsWarning": "您确定要修改开发者设置吗?", + "developerSettings": "开发者设置", + "serverEndpoint": "服务器端点", + "invalidEndpoint": "端点无效", + "invalidEndpointMessage": "抱歉,您输入的端点无效。请输入有效的端点,然后重试。", + "endpointUpdatedMessage": "端点更新成功", + "customEndpoint": "已连接至 {endpoint}" } \ No newline at end of file diff --git a/auth/lib/main.dart b/auth/lib/main.dart index c897e8461..3eb7a1bcb 100644 --- a/auth/lib/main.dart +++ b/auth/lib/main.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:io'; import 'package:adaptive_theme/adaptive_theme.dart'; @@ -59,7 +60,7 @@ Future _runInForeground() async { _logger.info("Starting app in foreground"); await _init(false, via: 'mainMethod'); final Locale locale = await getLocale(); - UpdateService.instance.showUpdateNotification(); + unawaited(UpdateService.instance.showUpdateNotification()); runApp( AppLock( builder: (args) => App(locale: locale), diff --git a/auth/lib/models/account/two_factor.dart b/auth/lib/models/account/two_factor.dart new file mode 100644 index 000000000..6a18f4277 --- /dev/null +++ b/auth/lib/models/account/two_factor.dart @@ -0,0 +1,13 @@ +enum TwoFactorType { totp, passkey } + +// ToString for TwoFactorType +String twoFactorTypeToString(TwoFactorType type) { + switch (type) { + case TwoFactorType.totp: + return "totp"; + case TwoFactorType.passkey: + return "passkey"; + default: + return type.name; + } +} diff --git a/auth/lib/onboarding/view/onboarding_page.dart b/auth/lib/onboarding/view/onboarding_page.dart index ab4d7b2af..ebc70a559 100644 --- a/auth/lib/onboarding/view/onboarding_page.dart +++ b/auth/lib/onboarding/view/onboarding_page.dart @@ -18,6 +18,8 @@ import 'package:ente_auth/ui/common/gradient_button.dart'; import 'package:ente_auth/ui/components/buttons/button_widget.dart'; import 'package:ente_auth/ui/components/models/button_result.dart'; import 'package:ente_auth/ui/home_page.dart'; +import 'package:ente_auth/ui/settings/developer_settings_page.dart'; +import 'package:ente_auth/ui/settings/developer_settings_widget.dart'; import 'package:ente_auth/ui/settings/language_picker.dart'; import 'package:ente_auth/utils/dialog_util.dart'; import 'package:ente_auth/utils/navigation_util.dart'; @@ -34,8 +36,12 @@ class OnboardingPage extends StatefulWidget { } class _OnboardingPageState extends State { + static const kDeveloperModeTapCountThreshold = 7; + late StreamSubscription _triggerLogoutEvent; + int _developerModeTapCount = 0; + @override void initState() { _triggerLogoutEvent = @@ -57,125 +63,152 @@ class _OnboardingPageState extends State { final l10n = context.l10n; return Scaffold( body: SafeArea( - 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"), - ), - onTap: () async { - final locale = await getLocale(); - 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, - // color: Theme.of(context) - // .colorScheme - // .mutedTextColor, + child: GestureDetector( + onTap: () async { + _developerModeTapCount++; + if (_developerModeTapCount >= kDeveloperModeTapCountThreshold) { + _developerModeTapCount = 0; + final result = await showChoiceDialog( + context, + title: l10n.developerSettings, + firstButtonLabel: l10n.yes, + body: l10n.developerSettingsWarning, + isDismissible: false, + ); + if (result?.action == ButtonAction.first) { + await Navigator.of(context).push( + MaterialPageRoute( + builder: (BuildContext context) { + return const DeveloperSettingsPage(); + }, + ), + ); + setState(() {}); + } + } + }, + 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: 24), - 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: 100), + Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(horizontal: 20), + child: GradientButton( + onTap: _navigateToSignUpPage, + text: l10n.newUser, + ), + ), + const SizedBox(height: 24), + 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(), + ], + ), ), ), ), @@ -210,6 +243,7 @@ class _OnboardingPageState extends State { } if (hasOptedBefore || result?.action == ButtonAction.first) { await Configuration.instance.optForOfflineMode(); + // ignore: unawaited_futures Navigator.of(context).push( MaterialPageRoute( builder: (BuildContext context) { diff --git a/auth/lib/services/authenticator_service.dart b/auth/lib/services/authenticator_service.dart index 4cb424440..5788294b9 100644 --- a/auth/lib/services/authenticator_service.dart +++ b/auth/lib/services/authenticator_service.dart @@ -5,7 +5,6 @@ import 'dart:math'; import 'package:ente_auth/core/configuration.dart'; import 'package:ente_auth/core/errors.dart'; import 'package:ente_auth/core/event_bus.dart'; -import 'package:ente_auth/core/network.dart'; import 'package:ente_auth/events/codes_updated_event.dart'; import 'package:ente_auth/events/signed_in_event.dart'; import 'package:ente_auth/events/trigger_logout_event.dart'; @@ -56,7 +55,7 @@ class AuthenticatorService { _prefs = await SharedPreferences.getInstance(); _db = AuthenticatorDB.instance; _offlineDb = OfflineAuthenticatorDB.instance; - _gateway = AuthenticatorGateway(Network.instance.getDio(), _config); + _gateway = AuthenticatorGateway(); if (Configuration.instance.hasConfiguredAccount()) { unawaited(onlineSync()); } @@ -210,8 +209,8 @@ class AuthenticatorService { if (deletedIDs.isNotEmpty) { await _db.deleteByIDs(ids: deletedIDs); } - _prefs.setInt(_lastEntitySyncTime, maxSyncTime); - _logger.info("Setting synctime to $maxSyncTime"); + await _prefs.setInt(_lastEntitySyncTime, maxSyncTime); + _logger.info("Setting synctime to " + maxSyncTime.toString()); if (result.length == fetchLimit) { _logger.info("Diff limit reached, pulling again"); await _remoteToLocalSync(); diff --git a/auth/lib/services/local_authentication_service.dart b/auth/lib/services/local_authentication_service.dart index 6b6889429..44c2a758a 100644 --- a/auth/lib/services/local_authentication_service.dart +++ b/auth/lib/services/local_authentication_service.dart @@ -60,6 +60,7 @@ class LocalAuthenticationService { .setEnabled(Configuration.instance.shouldShowLockScreen()); } } else { + // ignore: unawaited_futures showErrorDialog( context, errorDialogTitle, diff --git a/auth/lib/services/passkey_service.dart b/auth/lib/services/passkey_service.dart index fdc9719dc..825a19729 100644 --- a/auth/lib/services/passkey_service.dart +++ b/auth/lib/services/passkey_service.dart @@ -17,6 +17,28 @@ class PasskeyService { return response.data!["accountsToken"] as String; } + Future isPasskeyRecoveryEnabled() async { + final response = await _enteDio.get( + "/users/two-factor/recovery-status", + ); + return response.data!["isPasskeyRecoveryEnabled"] as bool; + } + + Future configurePasskeyRecovery( + String secret, + String userEncryptedSecret, + String userSecretNonce, + ) async { + await _enteDio.post( + "/users/two-factor/passkeys/configure-recovery", + data: { + "secret": secret, + "userSecretCipher": userEncryptedSecret, + "userSecretNonce": userSecretNonce, + }, + ); + } + Future openPasskeyPage(BuildContext context) async { try { final jwtToken = await getJwtToken(); diff --git a/auth/lib/services/update_service.dart b/auth/lib/services/update_service.dart index 89feecf53..e102a5a94 100644 --- a/auth/lib/services/update_service.dart +++ b/auth/lib/services/update_service.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:io'; import 'package:ente_auth/core/constants.dart'; @@ -71,9 +72,11 @@ class UpdateService { if (shouldUpdate && hasBeen3DaysSinceLastNotification && _latestVersion!.shouldNotify!) { - NotificationService.instance.showNotification( - "Update available", - "Click to install our best version yet", + unawaited( + NotificationService.instance.showNotification( + "Update available", + "Click to install our best version yet", + ), ); await _prefs.setInt(kUpdateAvailableShownTimeKey, now); } else { diff --git a/auth/lib/services/user_service.dart b/auth/lib/services/user_service.dart index 4c0cf1ebd..929468059 100644 --- a/auth/lib/services/user_service.dart +++ b/auth/lib/services/user_service.dart @@ -11,6 +11,7 @@ import 'package:ente_auth/core/event_bus.dart'; import 'package:ente_auth/core/network.dart'; import 'package:ente_auth/events/user_details_changed_event.dart'; import 'package:ente_auth/l10n/l10n.dart'; +import 'package:ente_auth/models/account/two_factor.dart'; import 'package:ente_auth/models/api/user/srp.dart'; import 'package:ente_auth/models/delete_account.dart'; import 'package:ente_auth/models/key_attributes.dart'; @@ -147,18 +148,18 @@ class UserService { final userDetails = UserDetails.fromMap(response.data); if (shouldCache) { if (userDetails.profileData != null) { - _preferences.setBool( + await _preferences.setBool( kIsEmailMFAEnabled, userDetails.profileData!.isEmailMFAEnabled, ); - _preferences.setBool( + await _preferences.setBool( kCanDisableEmailMFA, userDetails.profileData!.canDisableEmailMFA, ); } // handle email change from different client if (userDetails.email != _config.getEmail()) { - setEmail(userDetails.email); + await setEmail(userDetails.email); } } return userDetails; @@ -282,6 +283,7 @@ class UserService { throw Exception("unexpected response during passkey verification"); } + // ignore: unawaited_futures Navigator.of(context).pushAndRemoveUntil( MaterialPageRoute( builder: (BuildContext context) { @@ -331,6 +333,7 @@ class UserService { ); } } + // ignore: unawaited_futures Navigator.of(context).pushAndRemoveUntil( MaterialPageRoute( builder: (BuildContext context) { @@ -354,6 +357,7 @@ class UserService { ); Navigator.of(context).pop(); } else { + // ignore: unawaited_futures showErrorDialog( context, context.l10n.incorrectCode, @@ -363,6 +367,7 @@ class UserService { } catch (e) { await dialog.hide(); _logger.severe(e); + // ignore: unawaited_futures showErrorDialog( context, context.l10n.oops, @@ -399,6 +404,7 @@ class UserService { Bus.instance.fire(UserDetailsChangedEvent()); return; } + // ignore: unawaited_futures showErrorDialog( context, context.l10n.oops, @@ -407,12 +413,14 @@ class UserService { } on DioException catch (e) { await dialog.hide(); if (e.response != null && e.response!.statusCode == 403) { + // ignore: unawaited_futures showErrorDialog( context, context.l10n.oops, context.l10n.thisEmailIsAlreadyInUse, ); } else { + // ignore: unawaited_futures showErrorDialog( context, context.l10n.incorrectCode, @@ -422,6 +430,7 @@ class UserService { } catch (e) { await dialog.hide(); _logger.severe(e); + // ignore: unawaited_futures showErrorDialog( context, context.l10n.oops, @@ -632,6 +641,7 @@ class UserService { } } await dialog.hide(); + // ignore: unawaited_futures Navigator.of(context).pushAndRemoveUntil( MaterialPageRoute( builder: (BuildContext context) { @@ -709,6 +719,7 @@ class UserService { if (response.statusCode == 200) { showShortToast(context, context.l10n.authenticationSuccessful); await _saveConfiguration(response); + // ignore: unawaited_futures Navigator.of(context).pushAndRemoveUntil( MaterialPageRoute( builder: (BuildContext context) { @@ -723,6 +734,7 @@ class UserService { _logger.severe(e); if (e.response != null && e.response!.statusCode == 404) { showToast(context, "Session expired"); + // ignore: unawaited_futures Navigator.of(context).pushAndRemoveUntil( MaterialPageRoute( builder: (BuildContext context) { @@ -732,6 +744,7 @@ class UserService { (route) => route.isFirst, ); } else { + // ignore: unawaited_futures showErrorDialog( context, context.l10n.incorrectCode, @@ -741,6 +754,7 @@ class UserService { } catch (e) { await dialog.hide(); _logger.severe(e); + // ignore: unawaited_futures showErrorDialog( context, context.l10n.oops, @@ -749,7 +763,11 @@ class UserService { } } - Future recoverTwoFactor(BuildContext context, String sessionID) async { + Future recoverTwoFactor( + BuildContext context, + String sessionID, + TwoFactorType type, + ) async { final dialog = createProgressDialog(context, context.l10n.pleaseWait); await dialog.show(); try { @@ -757,13 +775,16 @@ class UserService { "${_config.getHttpEndpoint()}/users/two-factor/recover", queryParameters: { "sessionID": sessionID, + "twoFactorType": twoFactorTypeToString(type), }, ); if (response.statusCode == 200) { + // ignore: unawaited_futures Navigator.of(context).pushAndRemoveUntil( MaterialPageRoute( builder: (BuildContext context) { return TwoFactorRecoveryPage( + type, sessionID, response.data["encryptedSecret"], response.data["secretDecryptionNonce"], @@ -774,9 +795,11 @@ class UserService { ); } } on DioException catch (e) { + await dialog.hide(); _logger.severe(e); if (e.response != null && e.response!.statusCode == 404) { showToast(context, context.l10n.sessionExpired); + // ignore: unawaited_futures Navigator.of(context).pushAndRemoveUntil( MaterialPageRoute( builder: (BuildContext context) { @@ -786,6 +809,7 @@ class UserService { (route) => route.isFirst, ); } else { + // ignore: unawaited_futures showErrorDialog( context, context.l10n.oops, @@ -793,7 +817,9 @@ class UserService { ); } } catch (e) { + await dialog.hide(); _logger.severe(e); + // ignore: unawaited_futures showErrorDialog( context, context.l10n.oops, @@ -806,6 +832,7 @@ class UserService { Future removeTwoFactor( BuildContext context, + TwoFactorType type, String sessionID, String recoveryKey, String encryptedSecret, @@ -845,6 +872,7 @@ class UserService { data: { "sessionID": sessionID, "secret": secret, + "twoFactorType": twoFactorTypeToString(type), }, ); if (response.statusCode == 200) { @@ -853,6 +881,7 @@ class UserService { context.l10n.twofactorAuthenticationSuccessfullyReset, ); await _saveConfiguration(response); + // ignore: unawaited_futures Navigator.of(context).pushAndRemoveUntil( MaterialPageRoute( builder: (BuildContext context) { @@ -863,9 +892,11 @@ class UserService { ); } } on DioException catch (e) { + await dialog.hide(); _logger.severe(e); if (e.response != null && e.response!.statusCode == 404) { showToast(context, "Session expired"); + // ignore: unawaited_futures Navigator.of(context).pushAndRemoveUntil( MaterialPageRoute( builder: (BuildContext context) { @@ -875,6 +906,7 @@ class UserService { (route) => route.isFirst, ); } else { + // ignore: unawaited_futures showErrorDialog( context, context.l10n.oops, @@ -882,7 +914,9 @@ class UserService { ); } } catch (e) { + await dialog.hide(); _logger.severe(e); + // ignore: unawaited_futures showErrorDialog( context, context.l10n.oops, @@ -925,7 +959,7 @@ class UserService { "isEnabled": isEnabled, }, ); - _preferences.setBool(kIsEmailMFAEnabled, isEnabled); + await _preferences.setBool(kIsEmailMFAEnabled, isEnabled); } catch (e) { _logger.severe("Failed to update email mfa", e); rethrow; diff --git a/auth/lib/store/user_store.dart b/auth/lib/store/user_store.dart index 20f3ead72..b191d169d 100644 --- a/auth/lib/store/user_store.dart +++ b/auth/lib/store/user_store.dart @@ -7,10 +7,6 @@ class UserStore { late SharedPreferences _preferences; static final UserStore instance = UserStore._privateConstructor(); - static const endpoint = String.fromEnvironment( - "endpoint", - defaultValue: "https://api.ente.io", - ); Future init() async { _preferences = await SharedPreferences.getInstance(); diff --git a/auth/lib/ui/account/delete_account_page.dart b/auth/lib/ui/account/delete_account_page.dart index b6a314ec7..b8779e8cb 100644 --- a/auth/lib/ui/account/delete_account_page.dart +++ b/auth/lib/ui/account/delete_account_page.dart @@ -240,7 +240,7 @@ class DeleteAccountPage extends StatelessWidget { ), ], ); - + // ignore: unawaited_futures showDialog( context: context, builder: (BuildContext context) { diff --git a/auth/lib/ui/account/logout_dialog.dart b/auth/lib/ui/account/logout_dialog.dart index 43eed0fd2..5604f0ae3 100644 --- a/auth/lib/ui/account/logout_dialog.dart +++ b/auth/lib/ui/account/logout_dialog.dart @@ -23,6 +23,7 @@ Future autoLogoutAlert(BuildContext context) async { int pendingSyncCount = await AuthenticatorDB.instance.getNeedSyncCount(); if (pendingSyncCount > 0) { + // ignore: unawaited_futures showChoiceActionSheet( context, title: l10n.pendingSyncs, diff --git a/auth/lib/ui/account/password_entry_page.dart b/auth/lib/ui/account/password_entry_page.dart index e7714fb4a..21fd34407 100644 --- a/auth/lib/ui/account/password_entry_page.dart +++ b/auth/lib/ui/account/password_entry_page.dart @@ -394,6 +394,7 @@ class _PasswordEntryPageState extends State { } catch (e, s) { _logger.severe(e, s); await dialog.hide(); + // ignore: unawaited_futures showGenericErrorDialog(context: context); } } @@ -441,6 +442,7 @@ class _PasswordEntryPageState extends State { await UserService.instance.setAttributes(result); await dialog.hide(); Configuration.instance.resetVolatilePassword(); + // ignore: unawaited_futures Navigator.of(context).pushAndRemoveUntil( MaterialPageRoute( builder: (BuildContext context) { @@ -452,10 +454,11 @@ class _PasswordEntryPageState extends State { } catch (e, s) { _logger.severe(e, s); await dialog.hide(); + // ignore: unawaited_futures showGenericErrorDialog(context: context); } } - + // ignore: unawaited_futures routeToPage( context, RecoveryKeyPage( @@ -471,12 +474,14 @@ class _PasswordEntryPageState extends State { _logger.severe(e); await dialog.hide(); if (e is UnsupportedError) { + // ignore: unawaited_futures showErrorDialog( context, context.l10n.insecureDevice, context.l10n.sorryWeCouldNotGenerateSecureKeysOnThisDevicennplease, ); } else { + // ignore: unawaited_futures showGenericErrorDialog(context: context); } } diff --git a/auth/lib/ui/account/password_reentry_page.dart b/auth/lib/ui/account/password_reentry_page.dart index 7a53e4a8d..261f41db5 100644 --- a/auth/lib/ui/account/password_reentry_page.dart +++ b/auth/lib/ui/account/password_reentry_page.dart @@ -116,6 +116,7 @@ class _PasswordReentryPageState extends State { firstButtonLabel: context.l10n.useRecoveryKey, ); if (dialogChoice!.action == ButtonAction.first) { + // ignore: unawaited_futures Navigator.of(context).push( MaterialPageRoute( builder: (BuildContext context) { diff --git a/auth/lib/ui/account/request_pwd_verification_page.dart b/auth/lib/ui/account/request_pwd_verification_page.dart index 0e557bb80..5901d3bd4 100644 --- a/auth/lib/ui/account/request_pwd_verification_page.dart +++ b/auth/lib/ui/account/request_pwd_verification_page.dart @@ -80,7 +80,7 @@ class _RequestPasswordVerificationPageState onPressedFunction: () async { FocusScope.of(context).unfocus(); final dialog = createProgressDialog(context, context.l10n.pleaseWait); - dialog.show(); + await dialog.show(); try { final attributes = Configuration.instance.getKeyAttributes()!; final Uint8List keyEncryptionKey = await CryptoUtil.deriveKey( @@ -94,17 +94,18 @@ class _RequestPasswordVerificationPageState keyEncryptionKey, CryptoUtil.base642bin(attributes.keyDecryptionNonce), ); - dialog.show(); + await dialog.show(); // pop await widget.onPasswordVerified(keyEncryptionKey); - dialog.hide(); + await dialog.hide(); Navigator.of(context).pop(true); } catch (e, s) { _logger.severe("Error while verifying password", e, s); - dialog.hide(); + await dialog.hide(); if (widget.onPasswordError != null) { widget.onPasswordError!(); } else { + // ignore: unawaited_futures showErrorDialog( context, context.l10n.incorrectPasswordTitle, diff --git a/auth/lib/ui/account/sessions_page.dart b/auth/lib/ui/account/sessions_page.dart index d5e45a8e9..1815b20e2 100644 --- a/auth/lib/ui/account/sessions_page.dart +++ b/auth/lib/ui/account/sessions_page.dart @@ -121,6 +121,7 @@ class _SessionsPageState extends State { } catch (e) { await dialog.hide(); _logger.severe('failed to terminate'); + // ignore: unawaited_futures showErrorDialog( context, context.l10n.oops, @@ -184,7 +185,7 @@ class _SessionsPageState extends State { if (isLoggingOutFromThisDevice) { await UserService.instance.logout(context); } else { - _terminateSession(session); + await _terminateSession(session); } }, ), diff --git a/auth/lib/ui/account/verify_recovery_page.dart b/auth/lib/ui/account/verify_recovery_page.dart index e4b3fbdb2..06d489b4a 100644 --- a/auth/lib/ui/account/verify_recovery_page.dart +++ b/auth/lib/ui/account/verify_recovery_page.dart @@ -106,6 +106,7 @@ class _VerifyRecoveryPageState extends State { ), ); } catch (e) { + // ignore: unawaited_futures showGenericErrorDialog(context: context); return; } diff --git a/auth/lib/ui/code_widget.dart b/auth/lib/ui/code_widget.dart index da656803f..999fa0781 100644 --- a/auth/lib/ui/code_widget.dart +++ b/auth/lib/ui/code_widget.dart @@ -357,6 +357,7 @@ class _CodeWidgetState extends State { await FlutterClipboard.copy(content); showToast(context, confirmationMessage); if (Platform.isAndroid && shouldMinimizeOnCopy) { + // ignore: unawaited_futures MoveToBackground.moveTaskToBack(); } } @@ -387,7 +388,7 @@ class _CodeWidgetState extends State { ), ); if (code != null) { - CodeStore.instance.addCode(code); + await CodeStore.instance.addCode(code); } } diff --git a/auth/lib/ui/common/progress_dialog.dart b/auth/lib/ui/common/progress_dialog.dart index ecd96ae1d..5c2d4365b 100644 --- a/auth/lib/ui/common/progress_dialog.dart +++ b/auth/lib/ui/common/progress_dialog.dart @@ -146,6 +146,7 @@ class ProgressDialog { try { if (!_isShowing) { _dialog = _Body(); + // ignore: unawaited_futures showDialog( context: _context!, barrierDismissible: _barrierDismissible, diff --git a/auth/lib/ui/home_page.dart b/auth/lib/ui/home_page.dart index dfa9a8376..d543d1248 100644 --- a/auth/lib/ui/home_page.dart +++ b/auth/lib/ui/home_page.dart @@ -125,7 +125,7 @@ class _HomePageState extends State { ), ); if (code != null) { - CodeStore.instance.addCode(code); + await CodeStore.instance.addCode(code); // Focus the new code by searching if (_codes.length > 2) { _focusNewCode(code); @@ -142,7 +142,7 @@ class _HomePageState extends State { ), ); if (code != null) { - CodeStore.instance.addCode(code); + await CodeStore.instance.addCode(code); } } diff --git a/auth/lib/ui/passkey_page.dart b/auth/lib/ui/passkey_page.dart index 5b4cbf18d..8c2e54e98 100644 --- a/auth/lib/ui/passkey_page.dart +++ b/auth/lib/ui/passkey_page.dart @@ -2,9 +2,12 @@ import 'dart:convert'; import 'package:app_links/app_links.dart'; import 'package:ente_auth/core/configuration.dart'; -import 'package:ente_auth/ente_theme_data.dart'; import 'package:ente_auth/l10n/l10n.dart'; +import 'package:ente_auth/models/account/two_factor.dart'; import 'package:ente_auth/services/user_service.dart'; +import 'package:ente_auth/ui/components/buttons/button_widget.dart'; +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:url_launcher/url_launcher_string.dart'; @@ -49,14 +52,27 @@ class _PasskeyPageState extends State { if (!context.mounted || Configuration.instance.hasConfiguredAccount() || link == null) { + _logger.warning( + 'ignored deeplink: contextMounted ${context.mounted} hasConfiguredAccount ${Configuration.instance.hasConfiguredAccount()}', + ); return; } - if (mounted && link.toLowerCase().startsWith("enteauth://passkey")) { - final uri = Uri.parse(link).queryParameters['response']; - // response to json - final res = utf8.decode(base64.decode(uri!)); - final json = jsonDecode(res) as Map; - await UserService.instance.onPassKeyVerified(context, json); + try { + if (mounted && link.toLowerCase().startsWith("enteauth://passkey")) { + final String? uri = Uri.parse(link).queryParameters['response']; + String base64String = uri!.toString(); + while (base64String.length % 4 != 0) { + base64String += '='; + } + final res = utf8.decode(base64.decode(base64String)); + final json = jsonDecode(res) as Map; + await UserService.instance.onPassKeyVerified(context, json); + } else { + _logger.info('ignored deeplink: $link mounted $mounted'); + } + } catch (e, s) { + _logger.severe('passKey: failed to handle deeplink', e, s); + showGenericErrorDialog(context: context).ignore(); } } @@ -86,30 +102,50 @@ class _PasskeyPageState extends State { } Widget _getBody() { - final l10n = context.l10n; - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - l10n.waitingForBrowserRequest, - style: const TextStyle( - height: 1.4, - fontSize: 16, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 32), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + context.l10n.waitingForVerification, + style: const TextStyle( + height: 1.4, + fontSize: 16, + ), ), - ), - const SizedBox(height: 16), - Container( - width: double.infinity, - padding: const EdgeInsets.symmetric(horizontal: 32), - child: ElevatedButton( - style: Theme.of(context).colorScheme.optionalActionButtonStyle, - onPressed: launchPasskey, - child: Text(l10n.launchPasskeyUrlAgain), + const SizedBox(height: 16), + ButtonWidget( + buttonType: ButtonType.primary, + labelText: context.l10n.verifyPasskey, + onTap: () => launchPasskey(), ), - ), - ], + const Padding(padding: EdgeInsets.all(30)), + GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + UserService.instance.recoverTwoFactor( + context, + widget.sessionID, + TwoFactorType.passkey, + ); + }, + child: Container( + padding: const EdgeInsets.all(10), + child: Center( + child: Text( + context.l10n.recoverAccount, + style: const TextStyle( + decoration: TextDecoration.underline, + fontSize: 12, + ), + ), + ), + ), + ), + ], + ), ), ); } diff --git a/auth/lib/ui/settings/about_section_widget.dart b/auth/lib/ui/settings/about_section_widget.dart index 98a24d816..a96e1f0ad 100644 --- a/auth/lib/ui/settings/about_section_widget.dart +++ b/auth/lib/ui/settings/about_section_widget.dart @@ -36,7 +36,8 @@ class AboutSectionWidget extends StatelessWidget { trailingIcon: Icons.chevron_right_outlined, trailingIconIsMuted: true, onTap: () async { - launchUrl(Uri.parse("https://github.com/ente-io/auth")); + // ignore: unawaited_futures + launchUrl(Uri.parse("https://github.com/ente-io/ente")); }, ), sectionOptionSpacing, @@ -68,6 +69,7 @@ class AboutSectionWidget extends StatelessWidget { await UpdateService.instance.shouldUpdate(); await dialog.hide(); if (shouldUpdate) { + // ignore: unawaited_futures showDialog( context: context, builder: (BuildContext context) { diff --git a/auth/lib/ui/settings/account_section_widget.dart b/auth/lib/ui/settings/account_section_widget.dart index f29e1f296..d51b2dd87 100644 --- a/auth/lib/ui/settings/account_section_widget.dart +++ b/auth/lib/ui/settings/account_section_widget.dart @@ -50,6 +50,7 @@ class AccountSectionWidget extends StatelessWidget { ); await PlatformUtil.refocusWindows(); if (hasAuthenticated) { + // ignore: unawaited_futures showDialog( context: context, builder: (BuildContext context) { @@ -76,6 +77,7 @@ class AccountSectionWidget extends StatelessWidget { l10n.authToChangeYourPassword, ); if (hasAuthenticated) { + // ignore: unawaited_futures Navigator.of(context).push( MaterialPageRoute( builder: (BuildContext context) { @@ -108,9 +110,11 @@ class AccountSectionWidget extends StatelessWidget { recoveryKey = CryptoUtil.bin2hex(Configuration.instance.getRecoveryKey()); } catch (e) { + // ignore: unawaited_futures showGenericErrorDialog(context: context); return; } + // ignore: unawaited_futures routeToPage( context, RecoveryKeyPage( @@ -144,6 +148,7 @@ class AccountSectionWidget extends StatelessWidget { trailingIcon: Icons.chevron_right_outlined, trailingIconIsMuted: true, onTap: () async { + // ignore: unawaited_futures routeToPage(context, const DeleteAccountPage()); }, ), diff --git a/auth/lib/ui/settings/app_update_dialog.dart b/auth/lib/ui/settings/app_update_dialog.dart index c396184ea..176abc4b1 100644 --- a/auth/lib/ui/settings/app_update_dialog.dart +++ b/auth/lib/ui/settings/app_update_dialog.dart @@ -1,15 +1,8 @@ -import 'dart:io'; - -import 'package:ente_auth/core/configuration.dart'; -import 'package:ente_auth/core/network.dart'; -import 'package:ente_auth/ente_theme_data.dart'; 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:logging/logging.dart'; -import 'package:open_filex/open_filex.dart'; import 'package:url_launcher/url_launcher_string.dart'; class AppUpdateDialog extends StatefulWidget { @@ -117,113 +110,3 @@ class _AppUpdateDialogState extends State { ); } } - -class ApkDownloaderDialog extends StatefulWidget { - final LatestVersionInfo? versionInfo; - - const ApkDownloaderDialog(this.versionInfo, {super.key}); - - @override - State createState() => _ApkDownloaderDialogState(); -} - -class _ApkDownloaderDialogState extends State { - String? _saveUrl; - double? _downloadProgress; - - @override - void initState() { - super.initState(); - _saveUrl = - "${Configuration.instance.getTempDirectory()}ente-${widget.versionInfo!.name!}.apk"; - _downloadApk(); - } - - @override - Widget build(BuildContext context) { - return PopScope( - canPop: false, - child: AlertDialog( - title: const Text( - "Downloading...", - style: TextStyle( - fontSize: 16, - ), - textAlign: TextAlign.center, - ), - content: LinearProgressIndicator( - value: _downloadProgress, - valueColor: AlwaysStoppedAnimation( - Theme.of(context).colorScheme.alternativeColor, - ), - ), - ), - ); - } - - Future _downloadApk() async { - try { - if (!File(_saveUrl!).existsSync()) { - await Network.instance.getDio().download( - widget.versionInfo!.url!, - _saveUrl, - onReceiveProgress: (count, _) { - setState(() { - _downloadProgress = count / widget.versionInfo!.size!; - }); - }, - ); - } - Navigator.of(context, rootNavigator: true).pop('dialog'); - OpenFilex.open(_saveUrl); - } catch (e) { - Logger("ApkDownloader").severe(e); - final AlertDialog alert = AlertDialog( - title: const Text("Sorry"), - content: const Text("The download could not be completed"), - actions: [ - TextButton( - child: const Text( - "Ignore", - style: TextStyle( - color: Colors.white, - ), - ), - onPressed: () { - Navigator.of(context, rootNavigator: true).pop('dialog'); - Navigator.of(context, rootNavigator: true).pop('dialog'); - }, - ), - TextButton( - child: Text( - "Retry", - style: TextStyle( - color: Theme.of(context).colorScheme.alternativeColor, - ), - ), - onPressed: () { - Navigator.of(context, rootNavigator: true).pop('dialog'); - Navigator.of(context, rootNavigator: true).pop('dialog'); - showDialog( - context: context, - builder: (BuildContext context) { - return ApkDownloaderDialog(widget.versionInfo); - }, - barrierDismissible: false, - ); - }, - ), - ], - ); - - showDialog( - context: context, - builder: (BuildContext context) { - return alert; - }, - barrierColor: Colors.black87, - ); - return; - } - } -} diff --git a/auth/lib/ui/settings/danger_section_widget.dart b/auth/lib/ui/settings/danger_section_widget.dart index ae9953917..4f8160c38 100644 --- a/auth/lib/ui/settings/danger_section_widget.dart +++ b/auth/lib/ui/settings/danger_section_widget.dart @@ -46,6 +46,7 @@ class DangerSectionWidget extends StatelessWidget { trailingIcon: Icons.chevron_right_outlined, trailingIconIsMuted: true, onTap: () async { + // ignore: unawaited_futures routeToPage(context, const DeleteAccountPage()); }, ), 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 60638f91c..12df41a14 100644 --- a/auth/lib/ui/settings/data/import/google_auth_import.dart +++ b/auth/lib/ui/settings/data/import/google_auth_import.dart @@ -58,6 +58,7 @@ Future showGoogleAuthInstruction(BuildContext context) async { await CodeStore.instance.addCode(code, shouldSync: false); } unawaited(AuthenticatorService.instance.onlineSync()); + // ignore: unawaited_futures importSuccessDialog(context, codes.length); } } diff --git a/auth/lib/ui/settings/data/import/import_service.dart b/auth/lib/ui/settings/data/import/import_service.dart index a510ec88b..a315c1679 100644 --- a/auth/lib/ui/settings/data/import/import_service.dart +++ b/auth/lib/ui/settings/data/import/import_service.dart @@ -19,29 +19,29 @@ class ImportService { Future initiateImport(BuildContext context, ImportType type) async { switch (type) { case ImportType.plainText: - showImportInstructionDialog(context); + await showImportInstructionDialog(context); break; case ImportType.encrypted: - showEncryptedImportInstruction(context); + await showEncryptedImportInstruction(context); break; case ImportType.ravio: - showRaivoImportInstruction(context); + await showRaivoImportInstruction(context); break; case ImportType.googleAuthenticator: - showGoogleAuthInstruction(context); + await showGoogleAuthInstruction(context); // showToast(context, 'coming soon'); break; case ImportType.aegis: - showAegisImportInstruction(context); + await showAegisImportInstruction(context); break; case ImportType.twoFas: - show2FasImportInstruction(context); + await show2FasImportInstruction(context); break; case ImportType.bitwarden: - showBitwardenImportInstruction(context); + await showBitwardenImportInstruction(context); break; case ImportType.lastpass: - showLastpassImportInstruction(context); + await showLastpassImportInstruction(context); break; } } diff --git a/auth/lib/ui/settings/data/import_page.dart b/auth/lib/ui/settings/data/import_page.dart index b7ac09846..c5ad2206e 100644 --- a/auth/lib/ui/settings/data/import_page.dart +++ b/auth/lib/ui/settings/data/import_page.dart @@ -105,7 +105,7 @@ class ImportCodePage extends StatelessWidget { index != importOptions.length - 1, isTopBorderRadiusRemoved: index != 0, onTap: () async { - ImportService().initiateImport(context, type); + await ImportService().initiateImport(context, type); // routeToPage(context, ImportCodePage()); // _showImportInstructionDialog(context); }, diff --git a/auth/lib/ui/settings/developer_settings_page.dart b/auth/lib/ui/settings/developer_settings_page.dart new file mode 100644 index 000000000..96139e982 --- /dev/null +++ b/auth/lib/ui/settings/developer_settings_page.dart @@ -0,0 +1,90 @@ +import 'package:dio/dio.dart'; +import 'package:ente_auth/core/configuration.dart'; +import 'package:ente_auth/l10n/l10n.dart'; +import 'package:ente_auth/ui/common/gradient_button.dart'; +import 'package:ente_auth/utils/dialog_util.dart'; +import 'package:ente_auth/utils/toast_util.dart'; +import 'package:flutter/material.dart'; +import 'package:logging/logging.dart'; + +class DeveloperSettingsPage extends StatefulWidget { + const DeveloperSettingsPage({super.key}); + + @override + _DeveloperSettingsPageState createState() => _DeveloperSettingsPageState(); +} + +class _DeveloperSettingsPageState extends State { + final _logger = Logger('DeveloperSettingsPage'); + final _urlController = TextEditingController(); + + @override + void dispose() { + _urlController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + _logger.info( + "Current endpoint is: " + Configuration.instance.getHttpEndpoint(), + ); + return Scaffold( + appBar: AppBar( + title: Text(context.l10n.developerSettings), + ), + body: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + children: [ + TextField( + controller: _urlController, + decoration: InputDecoration( + labelText: context.l10n.serverEndpoint, + hintText: Configuration.instance.getHttpEndpoint(), + ), + autofocus: true, + ), + const SizedBox(height: 40), + GradientButton( + onTap: () async { + String url = _urlController.text; + _logger.info("Entered endpoint: " + url); + try { + final uri = Uri.parse(url); + if ((uri.scheme == "http" || uri.scheme == "https")) { + await _ping(url); + await Configuration.instance.setHttpEndpoint(url); + showToast(context, context.l10n.endpointUpdatedMessage); + Navigator.of(context).pop(); + } else { + throw const FormatException(); + } + } catch (e) { + // ignore: unawaited_futures + showErrorDialog( + context, + context.l10n.invalidEndpoint, + context.l10n.invalidEndpointMessage, + ); + } + }, + text: context.l10n.saveAction, + ), + ], + ), + ), + ); + } + + Future _ping(String endpoint) async { + try { + final response = await Dio().get(endpoint + '/ping'); + if (response.data['message'] != 'pong') { + throw Exception('Invalid response'); + } + } catch (e) { + throw Exception('Error occurred: $e'); + } + } +} diff --git a/auth/lib/ui/settings/developer_settings_widget.dart b/auth/lib/ui/settings/developer_settings_widget.dart new file mode 100644 index 000000000..0fb32301c --- /dev/null +++ b/auth/lib/ui/settings/developer_settings_widget.dart @@ -0,0 +1,27 @@ +import 'package:ente_auth/core/configuration.dart'; +import 'package:ente_auth/core/constants.dart'; +import 'package:ente_auth/l10n/l10n.dart'; +import 'package:flutter/material.dart'; + +class DeveloperSettingsWidget extends StatelessWidget { + const DeveloperSettingsWidget({super.key}); + + @override + Widget build(BuildContext context) { + final endpoint = Configuration.instance.getHttpEndpoint(); + if (endpoint != kDefaultProductionEndpoint) { + final endpointURI = Uri.parse(endpoint); + return Padding( + padding: const EdgeInsets.only(bottom: 20), + child: Text( + context.l10n.customEndpoint( + endpointURI.host + ":" + endpointURI.port.toString(), + ), + style: Theme.of(context).textTheme.bodySmall, + ), + ); + } else { + return const SizedBox.shrink(); + } + } +} diff --git a/auth/lib/ui/settings/general_section_widget.dart b/auth/lib/ui/settings/general_section_widget.dart index 7172da13c..2b74cbf80 100644 --- a/auth/lib/ui/settings/general_section_widget.dart +++ b/auth/lib/ui/settings/general_section_widget.dart @@ -48,6 +48,7 @@ class _AdvancedSectionWidgetState extends State { trailingIconIsMuted: true, onTap: () async { final locale = await getLocale(); + // ignore: unawaited_futures routeToPage( context, LanguageSelectorPage( diff --git a/auth/lib/ui/settings/security_section_widget.dart b/auth/lib/ui/settings/security_section_widget.dart index a100792bb..4e50d38fb 100644 --- a/auth/lib/ui/settings/security_section_widget.dart +++ b/auth/lib/ui/settings/security_section_widget.dart @@ -22,6 +22,7 @@ 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({super.key}); @@ -33,6 +34,7 @@ class SecuritySectionWidget extends StatefulWidget { class _SecuritySectionWidgetState extends State { final _config = Configuration.instance; late bool _hasLoggedIn; + final Logger _logger = Logger('SecuritySectionWidget'); @override void initState() { @@ -76,7 +78,7 @@ class _SecuritySectionWidgetState extends State { pressedColor: getEnteColorScheme(context).fillFaint, trailingIcon: Icons.chevron_right_outlined, trailingIconIsMuted: true, - onTap: () => PasskeyService.instance.openPasskeyPage(context), + onTap: () async => await onPasskeyClick(context), ), sectionOptionSpacing, MenuItemWidget( @@ -119,6 +121,7 @@ class _SecuritySectionWidgetState extends State { ); await PlatformUtil.refocusWindows(); if (hasAuthenticated) { + // ignore: unawaited_futures Navigator.of(context).push( MaterialPageRoute( builder: (BuildContext context) { @@ -162,6 +165,31 @@ class _SecuritySectionWidgetState extends State { ); } + Future onPasskeyClick(BuildContext buildContext) async { + try { + final isPassKeyResetEnabled = + await PasskeyService.instance.isPasskeyRecoveryEnabled(); + if (!isPassKeyResetEnabled) { + final Uint8List recoveryKey = Configuration.instance.getRecoveryKey(); + final resetKey = CryptoUtil.generateKey(); + final resetKeyBase64 = CryptoUtil.bin2base64(resetKey); + final encryptionResult = CryptoUtil.encryptSync( + resetKey, + recoveryKey, + ); + await PasskeyService.instance.configurePasskeyRecovery( + resetKeyBase64, + CryptoUtil.bin2base64(encryptionResult.encryptedData!), + CryptoUtil.bin2base64(encryptionResult.nonce!), + ); + } + PasskeyService.instance.openPasskeyPage(buildContext).ignore(); + } catch (e, s) { + _logger.severe("failed to open passkey page", e, s); + await showGenericErrorDialog(context: context); + } + } + Future updateEmailMFA(bool enableEmailMFA) async { try { final UserDetails details = diff --git a/auth/lib/ui/settings/social_section_widget.dart b/auth/lib/ui/settings/social_section_widget.dart index 8e1655ac7..af8997ea4 100644 --- a/auth/lib/ui/settings/social_section_widget.dart +++ b/auth/lib/ui/settings/social_section_widget.dart @@ -81,6 +81,7 @@ class SocialsMenuItemWidget extends StatelessWidget { trailingIcon: Icons.chevron_right_outlined, trailingIconIsMuted: true, onTap: () async { + // ignore: unawaited_futures launchUrlString( url, mode: launchInExternalApp diff --git a/auth/lib/ui/settings/support_section_widget.dart b/auth/lib/ui/settings/support_section_widget.dart index 77497be2a..1343d2347 100644 --- a/auth/lib/ui/settings/support_section_widget.dart +++ b/auth/lib/ui/settings/support_section_widget.dart @@ -42,6 +42,7 @@ class _SupportSectionWidgetState extends State { trailingIcon: Icons.chevron_right_outlined, trailingIconIsMuted: true, onTap: () async { + // ignore: unawaited_futures showModalBottomSheet( backgroundColor: Theme.of(context).colorScheme.background, barrierColor: Colors.black87, @@ -61,6 +62,7 @@ class _SupportSectionWidgetState extends State { trailingIcon: Icons.chevron_right_outlined, trailingIconIsMuted: true, onTap: () async { + // ignore: unawaited_futures launchUrlString( githubIssuesUrl, mode: LaunchMode.externalApplication, diff --git a/auth/lib/ui/settings_page.dart b/auth/lib/ui/settings_page.dart index 5f516d7cc..48fd6467c 100644 --- a/auth/lib/ui/settings_page.dart +++ b/auth/lib/ui/settings_page.dart @@ -16,6 +16,7 @@ import 'package:ente_auth/ui/settings/account_section_widget.dart'; import 'package:ente_auth/ui/settings/app_version_widget.dart'; import 'package:ente_auth/ui/settings/data/data_section_widget.dart'; import 'package:ente_auth/ui/settings/data/export_widget.dart'; +import 'package:ente_auth/ui/settings/developer_settings_widget.dart'; import 'package:ente_auth/ui/settings/general_section_widget.dart'; import 'package:ente_auth/ui/settings/security_section_widget.dart'; import 'package:ente_auth/ui/settings/social_section_widget.dart'; @@ -156,6 +157,7 @@ class SettingsPage extends StatelessWidget { sectionSpacing, const AboutSectionWidget(), const AppVersionWidget(), + const DeveloperSettingsWidget(), const SupportDevWidget(), const Padding( padding: EdgeInsets.only(bottom: 60), diff --git a/auth/lib/ui/tools/lock_screen.dart b/auth/lib/ui/tools/lock_screen.dart index 712aa233b..b6e2126e1 100644 --- a/auth/lib/ui/tools/lock_screen.dart +++ b/auth/lib/ui/tools/lock_screen.dart @@ -56,6 +56,7 @@ class _LockScreenState extends State with WidgetsBindingObserver { text: context.l10n.unlock, iconData: Icons.lock_open_outlined, onTap: () async { + // ignore: unawaited_futures _showLockScreen(source: "tapUnlock"); }, ), diff --git a/auth/lib/ui/two_factor_authentication_page.dart b/auth/lib/ui/two_factor_authentication_page.dart index a83a66baa..58a462286 100644 --- a/auth/lib/ui/two_factor_authentication_page.dart +++ b/auth/lib/ui/two_factor_authentication_page.dart @@ -1,4 +1,5 @@ import 'package:ente_auth/l10n/l10n.dart'; +import 'package:ente_auth/models/account/two_factor.dart'; import 'package:ente_auth/services/user_service.dart'; import 'package:ente_auth/ui/lifecycle_event_handler.dart'; import 'package:flutter/material.dart'; @@ -121,7 +122,7 @@ class _TwoFactorAuthenticationPageState child: OutlinedButton( onPressed: _code.length == 6 ? () async { - _verifyTwoFactorCode(_code); + await _verifyTwoFactorCode(_code); } : null, child: Text(l10n.verify), @@ -131,7 +132,11 @@ class _TwoFactorAuthenticationPageState GestureDetector( behavior: HitTestBehavior.opaque, onTap: () { - UserService.instance.recoverTwoFactor(context, widget.sessionID); + UserService.instance.recoverTwoFactor( + context, + widget.sessionID, + TwoFactorType.totp, + ); }, child: Container( padding: const EdgeInsets.all(10), diff --git a/auth/lib/ui/two_factor_recovery_page.dart b/auth/lib/ui/two_factor_recovery_page.dart index 12ace414f..5743f0246 100644 --- a/auth/lib/ui/two_factor_recovery_page.dart +++ b/auth/lib/ui/two_factor_recovery_page.dart @@ -1,4 +1,5 @@ import 'package:ente_auth/l10n/l10n.dart'; +import 'package:ente_auth/models/account/two_factor.dart'; import 'package:ente_auth/services/user_service.dart'; import 'package:ente_auth/utils/dialog_util.dart'; import 'package:flutter/material.dart'; @@ -7,8 +8,10 @@ class TwoFactorRecoveryPage extends StatefulWidget { final String sessionID; final String encryptedSecret; final String secretDecryptionNonce; + final TwoFactorType type; const TwoFactorRecoveryPage( + this.type, this.sessionID, this.encryptedSecret, this.secretDecryptionNonce, { @@ -70,6 +73,7 @@ class _TwoFactorRecoveryPageState extends State { ? () async { await UserService.instance.removeTwoFactor( context, + widget.type, widget.sessionID, _recoveryKey.text, widget.encryptedSecret, diff --git a/auth/lib/utils/email_util.dart b/auth/lib/utils/email_util.dart index dce1c7aef..7679dbf91 100644 --- a/auth/lib/utils/email_util.dart +++ b/auth/lib/utils/email_util.dart @@ -101,14 +101,71 @@ Future sendLogs( ); }, ), - ButtonWidget( - isInAlert: true, - buttonType: ButtonType.secondary, - labelText: l10n.cancel, - buttonAction: ButtonAction.cancel, + 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, ), ], ); + 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( diff --git a/auth/lib/utils/toast_util.dart b/auth/lib/utils/toast_util.dart index 319c0eb91..942274bc8 100644 --- a/auth/lib/utils/toast_util.dart +++ b/auth/lib/utils/toast_util.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:fluttertoast/fluttertoast.dart'; -Future showToast( +void showToast( BuildContext context, String message, { toastLength = Toast.LENGTH_LONG, @@ -11,7 +11,7 @@ Future showToast( }) async { try { await Fluttertoast.cancel(); - return Fluttertoast.showToast( + await Fluttertoast.showToast( msg: message, toastLength: toastLength, gravity: ToastGravity.BOTTOM, @@ -47,6 +47,6 @@ Future showToast( } } -Future showShortToast(context, String message) { - return showToast(context, message, toastLength: Toast.LENGTH_SHORT); +void showShortToast(context, String message) { + showToast(context, message, toastLength: Toast.LENGTH_SHORT); } diff --git a/auth/migration-guides/README.md b/auth/migration-guides/README.md new file mode 100644 index 000000000..56d8983d0 --- /dev/null +++ b/auth/migration-guides/README.md @@ -0,0 +1,4 @@ +Migration guides have moved to the [help +docs](https://help.ente.io/auth/migration-guides/). This folder just contains +redirects for old links. + diff --git a/auth/migration-guides/authy.md b/auth/migration-guides/authy.md index 4fbc7377f..630bc83c7 100644 --- a/auth/migration-guides/authy.md +++ b/auth/migration-guides/authy.md @@ -1,62 +1,2 @@ -# Migrating from Authy -A guide written by Green, an ente.io lover - ---- - -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. But do not fear, as there is a much simpler way to migrate from Authy to ente! - -A user on GitHub has written a guide to export our data from Authy (morpheus on Discord found this and showed it to us), so we are going to be using that for the migration. - -## Exporting from Authy -To export your data, please follow [this guide](https://gist.github.com/gboudreau/94bb0c11a6209c82418d01a59d958c93). This will create a new JSON file with all your Authy TOTP data in it. **Do not share this file with anyone!** - -Or, you can [use this tool by Neeraj](https://github.com/ua741/authy-export/releases/tag/v0.0.4) to simplify things and skip directly to importing to ente Authenticator. -### *Do note that these tools may not export ALL of your codes. Make sure that all your accounts have been imported successfully before deleting any codes from your Authy account!* - -## Converting the export for ente Authenticator -### Update: You can now directly import from Bitwarden JSON export, meaning you can skip this step! If it doesn't work for some reason, though, then continue with this step. -So now that you have the JSON file, does that mean it can be imported into ente Authenticator? Yes, but if it doesn't work for some reason, then nope. (If you have a TXT file in the format ente Authenticator asked you for instead, then you probably used Neeraj's tool, so you can skip this step.) - -This is because the code in the guide exports your Authy data for Bitwarden, not ente Authenticator. If you have opened the JSON file, you might have noticed that the file created is not in a format that ente Authenticator asks for: - -ente Authenticator Screenshot - -So, this means that even if you try to import this file, nothing will happen. But don't worry, I've written a program in Python that converts the JSON file into a TXT file that ente Authenticator can use! (It's definitely not written **professionaly**, but hey it gets the job done so I'm happy with that.) - -You can download my program [here](https://github.com/gweeeen/ducky/blob/main/duckys_other_stuff/authy_to_ente.py). Or if you **really like making life hard**, then you can make a new Python file and copy this code to it: - -```py -import json -import os - -totp = [] - -accounts = json.load(open('authy-to-bitwarden-export.json','r',encoding='utf-8')) - -for account in accounts['items']: - totp.append(account['login']['totp']+'\n') - -writer = open('auth_codes.txt','w+',encoding='utf-8') -writer.writelines(totp) -writer.close() - -print('Saved to ' + os.getcwd() + '/auth_codes.txt') -``` - -To convert the file with this program, you will need to install [Python](https://www.python.org/downloads/) on your computer. - -Before you run the program, make sure that both the Python program and the JSON file are in the same directory, otherwise this will not work! - -To run the Python program, open it in IDLE and press F5, or open your terminal and type `python3 authy_to_ente.py` or `py -3 authy_to_ente.py`, depending on which OS you have. Once you run it, a new TXT file called `auth_codes.txt` will be generated. You can now import your data to ente Authenticator! - -## Importing to ente Authenticator -Now that we have the TXT file, let's import it. This should be the easiest part of the entire migration process. - -1. Copy the TXT file to one of your devices with ente Authenticator. -2. Log in to your account (if you haven't already). -3. Open the navigation menu (hamburger button on the top left), then press "Data", then press "Import codes". -4. Select the TXT file that was made earlier. - -And that's it! You have now successfully migrated from Authy to ente Authenticator. - -Just one more thing: Now that your secrets are safely stored, I recommend you delete the unencrypted JSON and TXT files that were made during the migration process for security. +Moved to +[help.ente.io/auth/migration-guides/authy](https://help.ente.io/auth/migration-guides/authy/) diff --git a/auth/migration-guides/encrypted_export.md b/auth/migration-guides/encrypted_export.md index b4e64134e..80a844c85 100644 --- a/auth/migration-guides/encrypted_export.md +++ b/auth/migration-guides/encrypted_export.md @@ -1,63 +1,2 @@ -# Auth Encrypted Export format - -## Overview - -When we export the auth codes, the data is encrypted using a key derived from the user's password. -This document describes the JSON structure used to organize exported data, including versioning and key derivation -parameters. - -## Export JSON Sample - -```json -{ - "version": 1, - "kdfParams": { - "memLimit": 4096, - "opsLimit": 3, - "salt": "example_salt" - }, - "encryptedData": "encrypted_data_here", - "encryptionNonce": "nonce_here" -} -``` - -The main object used to represent the export data. It contains the following key-value pairs: - -- `version`: The version of the export format. -- `kdfParams`: Key derivation function parameters. -- `encryptedData"`: The encrypted authentication data. -- `encryptionNonce`: The nonce used for encryption. - -### Version - -Export version is used to identify the format of the export data. - -#### Ver: 1 - -* KDF Algorithm: `ARGON2ID` -* Decrypted data format: `otpauth://totp/...`, separated by a new line. -* Encryption Algo: `XChaCha20-Poly1305` - -#### Key Derivation Function Params (KDF) - -This section contains the parameters that were using during KDF operation: - -- `memLimit`: Memory limit for the algorithm. -- `opsLimit`: Operations limit for the algorithm. -- `salt`: The salt used in the derivation process. - -#### Encrypted Data - -As mentioned above, the auth data is encrypted using a key that's derived by using user provided password & kdf params. -For encryption, we are using `XChaCha20-Poly1305` algorithm. - -## How to use the exported data - -* **Ente Authenticator app**: You can directly import the codes in the Ente Authenticator app. - > Settings -> Data -> Import Codes -> ente Encrypted export. - -* **Decrypt using Ente CLI** : Download the latest version of [Ente CLI](https://github.com/ente-io/ente/releases?q=CLI&expanded=false), and run the following command - -``` - ./ente auth decrypt -``` +Moved to +[help.ente.io/auth/migration-guides/export](https://help.ente.io/auth/migration-guides/export/) diff --git a/auth/pubspec.lock b/auth/pubspec.lock index 79a86072b..059e4eb38 100644 --- a/auth/pubspec.lock +++ b/auth/pubspec.lock @@ -213,7 +213,7 @@ packages: dependency: "direct main" description: name: collection - sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687 url: "https://pub.dev" source: hosted version: "1.18.0" @@ -901,26 +901,26 @@ packages: dependency: transitive description: name: matcher - sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" url: "https://pub.dev" source: hosted - version: "0.12.16+1" + version: "0.12.16" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" + sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" url: "https://pub.dev" source: hosted - version: "0.8.0" + version: "0.5.0" meta: dependency: transitive description: name: meta - sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 + sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.9.1" mime: dependency: transitive description: @@ -1021,10 +1021,10 @@ packages: dependency: "direct main" description: name: path - sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" url: "https://pub.dev" source: hosted - version: "1.9.0" + version: "1.8.3" path_drawing: dependency: transitive description: @@ -1419,10 +1419,10 @@ packages: dependency: transitive description: name: stack_trace - sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 url: "https://pub.dev" source: hosted - version: "1.11.1" + version: "1.11.0" step_progress_indicator: dependency: "direct main" description: @@ -1435,10 +1435,10 @@ packages: dependency: transitive description: name: stream_channel - sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.1" stream_transform: dependency: transitive description: @@ -1483,7 +1483,7 @@ packages: dependency: transitive description: name: test_api - sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" + sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8" url: "https://pub.dev" source: hosted version: "0.6.1" @@ -1643,10 +1643,10 @@ packages: dependency: transitive description: name: vm_service - sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 + sha256: c538be99af830f478718b51630ec1b6bee5e74e52c8a802d328d9e71d35d2583 url: "https://pub.dev" source: hosted - version: "13.0.0" + version: "11.10.0" watcher: dependency: transitive description: diff --git a/auth/pubspec.yaml b/auth/pubspec.yaml index 7b0245ecb..fc0f1c2f7 100644 --- a/auth/pubspec.yaml +++ b/auth/pubspec.yaml @@ -64,13 +64,11 @@ dependencies: 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 logging: ^1.0.1 modal_bottom_sheet: ^3.0.0-pre move_to_background: ^1.0.2 - open_filex: ^4.3.2 otp: ^3.1.1 package_info_plus: ^4.1.0 password_strength: ^0.2.0 diff --git a/cli/README.md b/cli/README.md index a24f4c131..8fc9aa694 100644 --- a/cli/README.md +++ b/cli/README.md @@ -1,49 +1,77 @@ -# Command Line Utility for exporting data from [Ente](https://ente.io) +# Ente CLI + +The Ente CLI is a Command Line Utility for exporting data from +[Ente](https://ente.io). It also does a few more things, for example, you can +use it to decrypting the export from Ente Auth. ## Install -You can either download the binary from the [GitHub releases -page](https://github.com/ente-io/ente/releases?q=tag%3Acli-v0&expanded=true) or -build it yourself. +The easiest way is to download a pre-built binary from the [GitHub +releases](https://github.com/ente-io/ente/releases?q=tag%3Acli-v0). -### Build from source +You can also build these binaries yourself + +```shell +./release.sh +``` + +Or you can build from source ```shell go build -o "bin/ente" main.go ``` -### Getting Started +The generated binaries are standalone, static binaries with no dependencies. You +can run them directly, or put them somewhere in your PATH. + +There is also an option to use [Docker](#docker). + +## Usage Run the help command to see all available commands. + ```shell ente --help ``` -#### Accounts +### Accounts + If you wish, you can add multiple accounts (your own and that of your family members) and export all data using this tool. -##### Add an account +#### Add an account + ```shell ente account add ``` -##### List accounts +#### List accounts + ```shell ente account list ``` -##### Change export directory +#### Change export directory + ```shell ente account update --email email@domain.com --dir ~/photos ``` ### Export -##### Start export + +#### Start export + ```shell ente export ``` ---- +### CLI Docs +You can view more cli documents at [docs](docs/generated/ente.md). +To update the docs, run the following command: + +```shell +go run main.go docs +``` + ## Docker @@ -51,16 +79,22 @@ If you fancy Docker, you can also run the CLI within a container. ### Configure -Modify the `docker-compose.yml` and add volume. -``cli-data`` volume is mandatory, you can add more volumes for your export directory. +Modify the `docker-compose.yml` and add volume. ``cli-data`` volume is +mandatory, you can add more volumes for your export directory. Build the docker image + ```shell docker build -t ente:latest . ``` +Note that [BuildKit](https://docs.docker.com/go/buildkit/) is needed to build +this image. If you face this issue, a quick fix is to add `DOCKER_BUILDKIT=1` in +front of the build command. + Start the container in detached mode -```bash + +```shell docker-compose up -d ``` @@ -69,20 +103,8 @@ docker-compose up -d docker-compose exec ente /bin/sh ``` - #### Directly executing commands ```shell docker run -it --rm ente:latest ls ``` - ---- - -## Releases - -Run the release script to build the binary and run it. - -```shell -./release.sh -``` - diff --git a/cli/cmd/account.go b/cli/cmd/account.go index 72e719e62..a4c78fb10 100644 --- a/cli/cmd/account.go +++ b/cli/cmd/account.go @@ -62,7 +62,7 @@ var updateAccCmd = &cobra.Command{ fmt.Printf("invalid app. Accepted values are 'photos', 'locker', 'auth'") } - err := ctrl.UpdateAccount(context.Background(), model.UpdateAccountParams{ + err := ctrl.UpdateAccount(context.Background(), model.AccountCommandParams{ Email: email, App: api.StringToApp(app), ExportDir: &exportDir, @@ -73,12 +73,49 @@ var updateAccCmd = &cobra.Command{ }, } +// Subcommand for 'account update' +var getTokenCmd = &cobra.Command{ + Use: "get-token", + Short: "Get token for an account for a specific app", + Run: func(cmd *cobra.Command, args []string) { + recoverWithLog() + app, _ := cmd.Flags().GetString("app") + email, _ := cmd.Flags().GetString("email") + if email == "" { + + fmt.Println("email must be specified, use --help for more information") + // print help + return + } + + validApps := map[string]bool{ + "photos": true, + "locker": true, + "auth": true, + } + + if !validApps[app] { + fmt.Printf("invalid app. Accepted values are 'photos', 'locker', 'auth'") + + } + err := ctrl.GetToken(context.Background(), model.AccountCommandParams{ + Email: email, + App: api.StringToApp(app), + }) + if err != nil { + fmt.Printf("Error updating account: %v\n", err) + } + }, +} + func init() { // Add 'config' subcommands to the root command rootCmd.AddCommand(accountCmd) // Add 'config' subcommands to the 'config' command updateAccCmd.Flags().String("dir", "", "update export directory") - updateAccCmd.Flags().String("email", "", "email address of the account to update") + updateAccCmd.Flags().String("email", "", "email address of the account") updateAccCmd.Flags().String("app", "photos", "Specify the app, default is 'photos'") - accountCmd.AddCommand(listAccCmd, addAccCmd, updateAccCmd) + getTokenCmd.Flags().String("email", "", "email address of the account") + getTokenCmd.Flags().String("app", "photos", "Specify the app, default is 'photos'") + accountCmd.AddCommand(listAccCmd, addAccCmd, updateAccCmd, getTokenCmd) } diff --git a/cli/cmd/admin.go b/cli/cmd/admin.go new file mode 100644 index 000000000..0e41bbfe2 --- /dev/null +++ b/cli/cmd/admin.go @@ -0,0 +1,90 @@ +package cmd + +import ( + "context" + "fmt" + "github.com/ente-io/cli/pkg/model" + "github.com/spf13/cobra" + "github.com/spf13/pflag" + "strings" +) + +var _adminCmd = &cobra.Command{ + Use: "admin", + Short: "Commands for admin actions", + Long: "Commands for admin actions like disable or enabling 2fa, bumping up the storage limit, etc.", +} + +var _userDetailsCmd = &cobra.Command{ + Use: "get-user-id", + Short: "Get user id", + RunE: func(cmd *cobra.Command, args []string) error { + recoverWithLog() + var flags = &model.AdminActionForUser{} + cmd.Flags().VisitAll(func(f *pflag.Flag) { + if f.Name == "admin-user" { + flags.AdminEmail = f.Value.String() + } + if f.Name == "user" { + flags.UserEmail = f.Value.String() + } + }) + return ctrl.GetUserId(context.Background(), *flags) + }, +} + +var _disable2faCmd = &cobra.Command{ + Use: "disable-2fa", + Short: "Disable 2fa for a user", + RunE: func(cmd *cobra.Command, args []string) error { + recoverWithLog() + var flags = &model.AdminActionForUser{} + cmd.Flags().VisitAll(func(f *pflag.Flag) { + if f.Name == "admin-user" { + flags.AdminEmail = f.Value.String() + } + if f.Name == "user" { + flags.UserEmail = f.Value.String() + } + }) + fmt.Println("Not supported yet") + return nil + }, +} + +var _updateFreeUserStorage = &cobra.Command{ + Use: "update-subscription", + Short: "Update subscription for the free user", + RunE: func(cmd *cobra.Command, args []string) error { + recoverWithLog() + var flags = &model.AdminActionForUser{} + noLimit := false + cmd.Flags().VisitAll(func(f *pflag.Flag) { + if f.Name == "admin-user" { + flags.AdminEmail = f.Value.String() + } + if f.Name == "user" { + flags.UserEmail = f.Value.String() + } + if f.Name == "no-limit" { + noLimit = strings.ToLower(f.Value.String()) == "true" + } + }) + return ctrl.UpdateFreeStorage(context.Background(), *flags, noLimit) + }, +} + +func init() { + rootCmd.AddCommand(_adminCmd) + _ = _userDetailsCmd.MarkFlagRequired("admin-user") + _ = _userDetailsCmd.MarkFlagRequired("user") + _userDetailsCmd.Flags().StringP("admin-user", "a", "", "The email of the admin user. (required)") + _userDetailsCmd.Flags().StringP("user", "u", "", "The email of the user to fetch details for. (required)") + _disable2faCmd.Flags().StringP("admin-user", "a", "", "The email of the admin user. (required)") + _disable2faCmd.Flags().StringP("user", "u", "", "The email of the user to disable 2FA for. (required)") + _updateFreeUserStorage.Flags().StringP("admin-user", "a", "", "The email of the admin user. (required)") + _updateFreeUserStorage.Flags().StringP("user", "u", "", "The email of the user to update subscription for. (required)") + // add a flag with no value --no-limit + _updateFreeUserStorage.Flags().String("no-limit", "True", "When true, sets 100TB as storage limit, and expiry to current date + 100 years") + _adminCmd.AddCommand(_userDetailsCmd, _disable2faCmd, _updateFreeUserStorage) +} diff --git a/cli/cmd/root.go b/cli/cmd/root.go index 5b7c1c6f3..e7f3378a4 100644 --- a/cli/cmd/root.go +++ b/cli/cmd/root.go @@ -3,6 +3,7 @@ package cmd import ( "fmt" "github.com/ente-io/cli/pkg" + "github.com/spf13/cobra/doc" "os" "runtime" @@ -11,7 +12,7 @@ import ( "github.com/spf13/cobra" ) -const AppVersion = "0.1.11" +var version string var ctrl *pkg.ClICtrl @@ -27,10 +28,15 @@ var rootCmd = &cobra.Command{ }, } +func GenerateDocs() error { + return doc.GenMarkdownTree(rootCmd, "./docs/generated") +} + // Execute adds all child commands to the root command and sets flags appropriately. // This is called by main.main(). It only needs to happen once to the rootCmd. -func Execute(controller *pkg.ClICtrl) { +func Execute(controller *pkg.ClICtrl, ver string) { ctrl = controller + version = ver err := rootCmd.Execute() if err != nil { os.Exit(1) diff --git a/cli/cmd/version.go b/cli/cmd/version.go index 9d5194bc7..3431bcfc3 100644 --- a/cli/cmd/version.go +++ b/cli/cmd/version.go @@ -12,7 +12,7 @@ var versionCmd = &cobra.Command{ Short: "Prints the current version", Long: ``, Run: func(cmd *cobra.Command, args []string) { - fmt.Printf("Version %s\n", AppVersion) + fmt.Printf("Version %s\n", version) }, } diff --git a/cli/config.yaml.example b/cli/config.yaml.example new file mode 100644 index 000000000..a00403656 --- /dev/null +++ b/cli/config.yaml.example @@ -0,0 +1,10 @@ +# You can put this configuration file in the following locations: +# - $HOME/.ente/config.yaml +# - config.yaml in the current working directory +# - $ENTE_CLI_CONFIG_PATH/config.yaml + +endpoint: + api: "http://localhost:8080" + +log: + http: false # log status code & time taken by requests diff --git a/cli/docs/generated/ente.md b/cli/docs/generated/ente.md new file mode 100644 index 000000000..6d0263ce4 --- /dev/null +++ b/cli/docs/generated/ente.md @@ -0,0 +1,28 @@ +## ente + +CLI tool for exporting your photos from ente.io + +### Synopsis + +Start by creating a config file in your home directory: + +``` +ente [flags] +``` + +### Options + +``` + -h, --help help for ente + -t, --toggle Help message for toggle +``` + +### SEE ALSO + +* [ente account](ente_account.md) - Manage account settings +* [ente admin](ente_admin.md) - Commands for admin actions +* [ente auth](ente_auth.md) - Authenticator commands +* [ente export](ente_export.md) - Starts the export process +* [ente version](ente_version.md) - Prints the current version + +###### Auto generated by spf13/cobra on 13-Mar-2024 diff --git a/cli/docs/generated/ente_account.md b/cli/docs/generated/ente_account.md new file mode 100644 index 000000000..ec26f9557 --- /dev/null +++ b/cli/docs/generated/ente_account.md @@ -0,0 +1,19 @@ +## ente account + +Manage account settings + +### Options + +``` + -h, --help help for account +``` + +### SEE ALSO + +* [ente](ente.md) - CLI tool for exporting your photos from ente.io +* [ente account add](ente_account_add.md) - Add a new account +* [ente account get-token](ente_account_get-token.md) - Get token for an account for a specific app +* [ente account list](ente_account_list.md) - list configured accounts +* [ente account update](ente_account_update.md) - Update an existing account's export directory + +###### Auto generated by spf13/cobra on 13-Mar-2024 diff --git a/cli/docs/generated/ente_account_add.md b/cli/docs/generated/ente_account_add.md new file mode 100644 index 000000000..74b2c23f9 --- /dev/null +++ b/cli/docs/generated/ente_account_add.md @@ -0,0 +1,19 @@ +## ente account add + +Add a new account + +``` +ente account add [flags] +``` + +### Options + +``` + -h, --help help for add +``` + +### SEE ALSO + +* [ente account](ente_account.md) - Manage account settings + +###### Auto generated by spf13/cobra on 13-Mar-2024 diff --git a/cli/docs/generated/ente_account_get-token.md b/cli/docs/generated/ente_account_get-token.md new file mode 100644 index 000000000..58ef3e7cd --- /dev/null +++ b/cli/docs/generated/ente_account_get-token.md @@ -0,0 +1,21 @@ +## ente account get-token + +Get token for an account for a specific app + +``` +ente account get-token [flags] +``` + +### Options + +``` + --app string Specify the app, default is 'photos' (default "photos") + --email string email address of the account + -h, --help help for get-token +``` + +### SEE ALSO + +* [ente account](ente_account.md) - Manage account settings + +###### Auto generated by spf13/cobra on 13-Mar-2024 diff --git a/cli/docs/generated/ente_account_list.md b/cli/docs/generated/ente_account_list.md new file mode 100644 index 000000000..3fc6fbc2e --- /dev/null +++ b/cli/docs/generated/ente_account_list.md @@ -0,0 +1,19 @@ +## ente account list + +list configured accounts + +``` +ente account list [flags] +``` + +### Options + +``` + -h, --help help for list +``` + +### SEE ALSO + +* [ente account](ente_account.md) - Manage account settings + +###### Auto generated by spf13/cobra on 13-Mar-2024 diff --git a/cli/docs/generated/ente_account_update.md b/cli/docs/generated/ente_account_update.md new file mode 100644 index 000000000..04c4418e7 --- /dev/null +++ b/cli/docs/generated/ente_account_update.md @@ -0,0 +1,22 @@ +## ente account update + +Update an existing account's export directory + +``` +ente account update [flags] +``` + +### Options + +``` + --app string Specify the app, default is 'photos' (default "photos") + --dir string update export directory + --email string email address of the account + -h, --help help for update +``` + +### SEE ALSO + +* [ente account](ente_account.md) - Manage account settings + +###### Auto generated by spf13/cobra on 13-Mar-2024 diff --git a/cli/docs/generated/ente_admin.md b/cli/docs/generated/ente_admin.md new file mode 100644 index 000000000..91e70324d --- /dev/null +++ b/cli/docs/generated/ente_admin.md @@ -0,0 +1,22 @@ +## ente admin + +Commands for admin actions + +### Synopsis + +Commands for admin actions like disable or enabling 2fa, bumping up the storage limit, etc. + +### Options + +``` + -h, --help help for admin +``` + +### SEE ALSO + +* [ente](ente.md) - CLI tool for exporting your photos from ente.io +* [ente admin disable-2fa](ente_admin_disable-2fa.md) - Disable 2fa for a user +* [ente admin get-user-id](ente_admin_get-user-id.md) - Get user id +* [ente admin update-subscription](ente_admin_update-subscription.md) - Update subscription for the free user + +###### Auto generated by spf13/cobra on 13-Mar-2024 diff --git a/cli/docs/generated/ente_admin_disable-2fa.md b/cli/docs/generated/ente_admin_disable-2fa.md new file mode 100644 index 000000000..19183fdfe --- /dev/null +++ b/cli/docs/generated/ente_admin_disable-2fa.md @@ -0,0 +1,21 @@ +## ente admin disable-2fa + +Disable 2fa for a user + +``` +ente admin disable-2fa [flags] +``` + +### Options + +``` + -a, --admin-user string The email of the admin user. (required) + -h, --help help for disable-2fa + -u, --user string The email of the user to disable 2FA for. (required) +``` + +### SEE ALSO + +* [ente admin](ente_admin.md) - Commands for admin actions + +###### Auto generated by spf13/cobra on 13-Mar-2024 diff --git a/cli/docs/generated/ente_admin_get-user-id.md b/cli/docs/generated/ente_admin_get-user-id.md new file mode 100644 index 000000000..0151a3ec8 --- /dev/null +++ b/cli/docs/generated/ente_admin_get-user-id.md @@ -0,0 +1,21 @@ +## ente admin get-user-id + +Get user id + +``` +ente admin get-user-id [flags] +``` + +### Options + +``` + -a, --admin-user string The email of the admin user. (required) + -h, --help help for get-user-id + -u, --user string The email of the user to fetch details for. (required) +``` + +### SEE ALSO + +* [ente admin](ente_admin.md) - Commands for admin actions + +###### Auto generated by spf13/cobra on 13-Mar-2024 diff --git a/cli/docs/generated/ente_admin_update-subscription.md b/cli/docs/generated/ente_admin_update-subscription.md new file mode 100644 index 000000000..30339acf2 --- /dev/null +++ b/cli/docs/generated/ente_admin_update-subscription.md @@ -0,0 +1,22 @@ +## ente admin update-subscription + +Update subscription for the free user + +``` +ente admin update-subscription [flags] +``` + +### Options + +``` + -a, --admin-user string The email of the admin user. (required) + -h, --help help for update-subscription + --no-limit string When true, sets 100TB as storage limit, and expiry to current date + 100 years (default "True") + -u, --user string The email of the user to update subscription for. (required) +``` + +### SEE ALSO + +* [ente admin](ente_admin.md) - Commands for admin actions + +###### Auto generated by spf13/cobra on 13-Mar-2024 diff --git a/cli/docs/generated/ente_auth.md b/cli/docs/generated/ente_auth.md new file mode 100644 index 000000000..4a64a944d --- /dev/null +++ b/cli/docs/generated/ente_auth.md @@ -0,0 +1,16 @@ +## ente auth + +Authenticator commands + +### Options + +``` + -h, --help help for auth +``` + +### SEE ALSO + +* [ente](ente.md) - CLI tool for exporting your photos from ente.io +* [ente auth decrypt](ente_auth_decrypt.md) - Decrypt authenticator export + +###### Auto generated by spf13/cobra on 13-Mar-2024 diff --git a/cli/docs/generated/ente_auth_decrypt.md b/cli/docs/generated/ente_auth_decrypt.md new file mode 100644 index 000000000..1203319e9 --- /dev/null +++ b/cli/docs/generated/ente_auth_decrypt.md @@ -0,0 +1,19 @@ +## ente auth decrypt + +Decrypt authenticator export + +``` +ente auth decrypt [input] [output] [flags] +``` + +### Options + +``` + -h, --help help for decrypt +``` + +### SEE ALSO + +* [ente auth](ente_auth.md) - Authenticator commands + +###### Auto generated by spf13/cobra on 13-Mar-2024 diff --git a/cli/docs/generated/ente_export.md b/cli/docs/generated/ente_export.md new file mode 100644 index 000000000..fb4cc6541 --- /dev/null +++ b/cli/docs/generated/ente_export.md @@ -0,0 +1,19 @@ +## ente export + +Starts the export process + +``` +ente export [flags] +``` + +### Options + +``` + -h, --help help for export +``` + +### SEE ALSO + +* [ente](ente.md) - CLI tool for exporting your photos from ente.io + +###### Auto generated by spf13/cobra on 13-Mar-2024 diff --git a/cli/docs/generated/ente_version.md b/cli/docs/generated/ente_version.md new file mode 100644 index 000000000..0254e2ebd --- /dev/null +++ b/cli/docs/generated/ente_version.md @@ -0,0 +1,19 @@ +## ente version + +Prints the current version + +``` +ente version [flags] +``` + +### Options + +``` + -h, --help help for version +``` + +### SEE ALSO + +* [ente](ente.md) - CLI tool for exporting your photos from ente.io + +###### Auto generated by spf13/cobra on 13-Mar-2024 diff --git a/cli/docs/release.md b/cli/docs/release.md new file mode 100644 index 000000000..dce097eaf --- /dev/null +++ b/cli/docs/release.md @@ -0,0 +1,26 @@ +# Releasing + +Tag main, and push the tag. + +> [!NOTE] +> +> See [auth/docs/release](../../auth/docs/release.md) for more details about the +> tag format. The prefix for cli releases should be `cli-`. + +```sh +git tag cli-v1.2.3 +git push origin cli-v1.2.3 +``` + +This'll trigger a [GitHub workflow](../../.github/workflows/cli-release.yml) +that creates a new draft GitHub release and attaches all the build artifacts to +it (zipped up binaries for various OS and architecture combinations). + +## Local release builds + +Run the release script to build the binaries for the various OS and architecture +cominations + +```shell +./release.sh +``` diff --git a/cli/docs/selfhost.md b/cli/docs/selfhost.md new file mode 100644 index 000000000..d38f0325a --- /dev/null +++ b/cli/docs/selfhost.md @@ -0,0 +1,27 @@ +## Self Hosting +If you are self-hosting the server, you can still configure CLI to export data & perform basic admin actions. + +To do this, first configure the CLI to point to your server. +Define a config.yaml and put it either in the same directory as CLI binary or path defined in env variable `ENTE_CLI_CONFIG_PATH` + +```yaml +endpoint: + api: "http://localhost:8080" +``` + +You should be able to [add an account](https://github.com/ente-io/ente/blob/main/cli/docs/generated/ente_account_add.md), and subsequently increase the [storage and account validity](https://github.com/ente-io/ente/blob/main/cli/docs/generated/ente_admin_update-subscription.md) using the CLI. + + +For the admin actions, you first need to whitelist admin users. You can create `server/museum.yaml`, and whitelist add the admin userID `internal.admins`. See [local.yaml](https://github.com/ente-io/ente/blob/main/server/configurations/local.yaml#L211C1-L232C1) in the server source code for details about how to define this. + +You can use [account list](https://github.com/ente-io/ente/blob/main/cli/docs/generated/ente_account_list.md) command to find the user id of any account. + +```yaml +# .... + +internal: + admins: + # - 1580559962386440 + +# .... +``` \ No newline at end of file diff --git a/cli/go.mod b/cli/go.mod index 090185f64..d643e8b26 100644 --- a/cli/go.mod +++ b/cli/go.mod @@ -12,10 +12,12 @@ require ( require ( github.com/alessio/shellescape v1.4.1 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/danieljoos/wincred v1.2.0 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.17 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect ) require ( diff --git a/cli/go.sum b/cli/go.sum index 914c2284a..bcf5fa319 100644 --- a/cli/go.sum +++ b/cli/go.sum @@ -48,6 +48,7 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/danieljoos/wincred v1.2.0 h1:ozqKHaLK0W/ii4KVbbvluM91W2H3Sh0BncbUNPS7jLE= github.com/danieljoos/wincred v1.2.0/go.mod h1:FzQLLMKBFdvu+osBrnFODiv32YGwCfx0SkRa/eYHgec= @@ -168,6 +169,7 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM= github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= diff --git a/cli/internal/api/admin.go b/cli/internal/api/admin.go new file mode 100644 index 000000000..1c0b2c50a --- /dev/null +++ b/cli/internal/api/admin.go @@ -0,0 +1,57 @@ +package api + +import ( + "context" + "fmt" + "github.com/ente-io/cli/internal/api/models" + "time" +) + +func (c *Client) GetUserIdFromEmail(ctx context.Context, email string) (*models.UserDetails, error) { + var res models.UserDetails + r, err := c.restClient.R(). + SetContext(ctx). + SetResult(&res). + SetQueryParam("email", email). + Get("/admin/user/") + if err != nil { + return nil, err + } + if r.IsError() { + return nil, &ApiError{ + StatusCode: r.StatusCode(), + Message: r.String(), + } + } + return &res, nil +} +func (c *Client) UpdateFreePlanSub(ctx context.Context, userDetails *models.UserDetails, storageInBytes int64, expiryTimeInMicro int64) error { + var res interface{} + if userDetails.Subscription.ProductID != "free" { + return fmt.Errorf("user is not on free plan") + } + payload := map[string]interface{}{ + "userID": userDetails.User.ID, + "expiryTime": expiryTimeInMicro, + "transactionID": fmt.Sprintf("cli-on-%d", time.Now().Unix()), + "productID": "free", + "paymentProvider": "", + "storage": storageInBytes, + } + r, err := c.restClient.R(). + SetContext(ctx). + SetResult(&res). + SetBody(payload). + Put("/admin/user/subscription") + if err != nil { + return err + } + if r.IsError() { + return &ApiError{ + StatusCode: r.StatusCode(), + Message: r.String(), + } + } + return nil + +} diff --git a/cli/internal/api/files.go b/cli/internal/api/files.go index 2e4af7701..b0f82f692 100644 --- a/cli/internal/api/files.go +++ b/cli/internal/api/files.go @@ -2,19 +2,30 @@ package api import ( "context" + "github.com/ente-io/cli/utils/constants" + "github.com/spf13/viper" "strconv" + "strings" ) var ( downloadHost = "https://files.ente.io/?fileID=" ) +func downloadUrl(fileID int64) string { + apiEndpoint := viper.GetString("endpoint.api") + if apiEndpoint == "" || strings.Compare(apiEndpoint, constants.EnteApiUrl) == 0 { + return downloadHost + strconv.FormatInt(fileID, 10) + } + return apiEndpoint + "/files/download/" + strconv.FormatInt(fileID, 10) +} + func (c *Client) DownloadFile(ctx context.Context, fileID int64, absolutePath string) error { req := c.downloadClient.R(). SetContext(ctx). SetOutput(absolutePath) attachToken(req) - r, err := req.Get(downloadHost + strconv.FormatInt(fileID, 10)) + r, err := req.Get(downloadUrl(fileID)) if r.IsError() { return &ApiError{ StatusCode: r.StatusCode(), diff --git a/cli/internal/api/log.go b/cli/internal/api/log.go index ba97c0d86..31ce404df 100644 --- a/cli/internal/api/log.go +++ b/cli/internal/api/log.go @@ -30,6 +30,16 @@ func logRequest(req *resty.Request) { } } } + // log query params if present + if len(req.QueryParam) > 0 { + fmt.Println(color.GreenString("Query Params:")) + for k, v := range req.QueryParam { + if k == TokenQuery { + v = []string{"REDACTED"} + } + fmt.Printf("%s: %s\n", color.CyanString(k), color.YellowString(strings.Join(v, ","))) + } + } } func logResponse(resp *resty.Response) { diff --git a/cli/internal/api/models/user_details.go b/cli/internal/api/models/user_details.go new file mode 100644 index 000000000..259ff972b --- /dev/null +++ b/cli/internal/api/models/user_details.go @@ -0,0 +1,16 @@ +package models + +type UserDetails struct { + User struct { + ID int64 `json:"id"` + } `json:"user"` + Usage int64 `json:"usage"` + Email string `json:"email"` + + Subscription struct { + ExpiryTime int64 `json:"expiryTime"` + Storage int64 `json:"storage"` + ProductID string `json:"productID"` + PaymentProvider string `json:"paymentProvider"` + } `json:"subscription"` +} diff --git a/cli/internal/promt.go b/cli/internal/promt.go index 2e988ac24..fe2f61bde 100644 --- a/cli/internal/promt.go +++ b/cli/internal/promt.go @@ -5,11 +5,12 @@ import ( "errors" "fmt" "github.com/ente-io/cli/internal/api" + "golang.org/x/term" "log" "os" + "regexp" + "strconv" "strings" - - "golang.org/x/term" ) func GetSensitiveField(label string) (string, error) { @@ -81,6 +82,79 @@ func GetCode(promptText string, length int) (string, error) { } } +// parseStorageSize parses a string representing a storage size (e.g., "500MB", "2GB") into bytes. +func parseStorageSize(input string) (int64, error) { + units := map[string]int64{ + "MB": 1 << 20, + "GB": 1 << 30, + "TB": 1 << 40, + } + re := regexp.MustCompile(`(?i)^(\d+(?:\.\d+)?)(MB|GB|TB)$`) + matches := re.FindStringSubmatch(input) + + if matches == nil { + return 0, errors.New("invalid format") + } + + number, err := strconv.ParseFloat(matches[1], 64) + if err != nil { + return 0, fmt.Errorf("invalid number: %s", matches[1]) + } + + unit := strings.ToUpper(matches[2]) + bytes := int64(number * float64(units[unit])) + + return bytes, nil +} + +func ConfirmAction(promptText string) (bool, error) { + for { + input, err := GetUserInput(promptText) + if err != nil { + return false, err + } + if input == "" { + log.Fatal("No input entered") + return false, errors.New("invalid input. Please enter 'y' or 'n'") + } + if input == "c" { + return false, errors.New("cancelled") + } + if input == "y" { + return true, nil + } + if input == "n" { + return false, nil + } + fmt.Println("Invalid input. Please enter 'y' or 'n'.") + } +} + +// GetStorageSize prompts the user for a storage size and returns the size in bytes. +func GetStorageSize(promptText string) (int64, error) { + for { + input, err := GetUserInput(promptText) + if err != nil { + return 0, err + } + if input == "" { + log.Fatal("No storage size entered") + return 0, errors.New("no storage size entered") + } + if input == "c" { + return 0, errors.New("storage size entry cancelled") + } + + bytes, err := parseStorageSize(input) + if err != nil { + fmt.Println("Invalid storage size format. Please use a valid format like '500MB', '2GB'.") + continue + } + + return bytes, nil + } +} + func GetExportDir() string { for { exportDir, err := GetUserInput("Enter export directory") diff --git a/cli/main.go b/cli/main.go index 2147afde9..157c11fd8 100644 --- a/cli/main.go +++ b/cli/main.go @@ -8,12 +8,15 @@ import ( "github.com/ente-io/cli/pkg" "github.com/ente-io/cli/pkg/secrets" "github.com/ente-io/cli/utils/constants" + "github.com/spf13/viper" "log" "os" "path/filepath" "strings" ) +var AppVersion = "0.1.12" + func main() { cliDBPath, err := GetCLIConfigPath() if secrets.IsRunningInContainer() { @@ -23,10 +26,10 @@ func main() { log.Fatalf("Please mount a volume to %s to persist cli data\n%v\n", cliDBPath, err) } } - if err != nil { log.Fatalf("Could not create cli config path\n%v\n", err) } + initConfig(cliDBPath) newCliPath := fmt.Sprintf("%s/ente-cli.db", cliDBPath) if !strings.HasPrefix(cliDBPath, "/") { oldCliPath := fmt.Sprintf("%sente-cli.db", cliDBPath) @@ -48,8 +51,8 @@ func main() { } ctrl := pkg.ClICtrl{ Client: api.NewClient(api.Params{ - Debug: false, - //Host: "http://localhost:8080", + Debug: viper.GetBool("log.http"), + Host: viper.GetString("endpoint.api"), }), DB: db, KeyHolder: secrets.NewKeyHolder(secrets.GetOrCreateClISecret()), @@ -63,7 +66,32 @@ func main() { panic(err) } }() - cmd.Execute(&ctrl) + + if len(os.Args) == 2 && os.Args[1] == "docs" { + log.Println("Generating docs") + err = cmd.GenerateDocs() + if err != nil { + log.Fatal(err) + } + return + } + cmd.Execute(&ctrl, AppVersion) +} + +func initConfig(cliConfigPath string) { + viper.SetConfigName("config") // name of config file (without extension) + viper.SetConfigType("yaml") // REQUIRED if the config file does not have the extension in the name + viper.AddConfigPath(cliConfigPath + "/") // path to look for the config file in + viper.AddConfigPath(".") // optionally look for config in the working directory + + viper.SetDefault("endpoint.api", constants.EnteApiUrl) + viper.SetDefault("log.http", false) + if err := viper.ReadInConfig(); err != nil { + if _, ok := err.(viper.ConfigFileNotFoundError); ok { + } else { + // Config file was found but another error was produced + } + } } // GetCLIConfigPath returns the path to the .ente-cli folder and creates it if it doesn't exist. diff --git a/cli/pkg/account.go b/cli/pkg/account.go index df45f3239..9363e2f80 100644 --- a/cli/pkg/account.go +++ b/cli/pkg/account.go @@ -142,7 +142,7 @@ func (c *ClICtrl) ListAccounts(cxt context.Context) error { return nil } -func (c *ClICtrl) UpdateAccount(ctx context.Context, params model.UpdateAccountParams) error { +func (c *ClICtrl) UpdateAccount(ctx context.Context, params model.AccountCommandParams) error { accounts, err := c.GetAccounts(ctx) if err != nil { return err @@ -177,5 +177,27 @@ func (c *ClICtrl) UpdateAccount(ctx context.Context, params model.UpdateAccountP return b.Put([]byte(accountKey), accInfoBytes) }) return err - +} + +func (c *ClICtrl) GetToken(ctx context.Context, params model.AccountCommandParams) error { + accounts, err := c.GetAccounts(ctx) + if err != nil { + return err + } + var acc *model.Account + for _, a := range accounts { + if a.Email == params.Email && a.App == params.App { + acc = &a + break + } + } + if acc == nil { + return fmt.Errorf("account not found, use `account list` to list accounts") + } + secretInfo, err := c.KeyHolder.LoadSecrets(*acc) + if err != nil { + return err + } + fmt.Println(secretInfo.TokenStr()) + return nil } diff --git a/cli/pkg/admin_actions.go b/cli/pkg/admin_actions.go new file mode 100644 index 000000000..c9ec00667 --- /dev/null +++ b/cli/pkg/admin_actions.go @@ -0,0 +1,114 @@ +package pkg + +import ( + "context" + "fmt" + "github.com/ente-io/cli/internal" + "github.com/ente-io/cli/pkg/model" + "github.com/ente-io/cli/utils" + "log" + "strings" + "time" +) + +func (c *ClICtrl) GetUserId(ctx context.Context, params model.AdminActionForUser) error { + accountCtx, err := c.buildAdminContext(ctx, params.AdminEmail) + if err != nil { + return err + } + id, err := c.Client.GetUserIdFromEmail(accountCtx, params.UserEmail) + if err != nil { + return err + } + fmt.Println(id.User.ID) + return nil +} + +func (c *ClICtrl) UpdateFreeStorage(ctx context.Context, params model.AdminActionForUser, noLimit bool) error { + accountCtx, err := c.buildAdminContext(ctx, params.AdminEmail) + if err != nil { + return err + } + userDetails, err := c.Client.GetUserIdFromEmail(accountCtx, params.UserEmail) + if err != nil { + return err + } + if noLimit { + // set storage to 100TB and expiry to + 100 years + err := c.Client.UpdateFreePlanSub(accountCtx, userDetails, 100*1024*1024*1024*1024, time.Now().AddDate(100, 0, 0).UnixMicro()) + if err != nil { + return err + } else { + fmt.Println("Successfully updated storage and expiry date for user") + } + return nil + } + storageSize, err := internal.GetStorageSize("Enter a storage size (e.g.'5MB', '10GB', '2Tb'): ") + if err != nil { + log.Fatalf("Error: %v", err) + } + dateStr, err := internal.GetUserInput("Enter sub expiry date in YYYY-MM-DD format (e.g.'2040-12-31')") + if err != nil { + log.Fatalf("Error: %v", err) + } + date, err := _parseDateOrDateTime(dateStr) + if err != nil { + return err + } + + fmt.Printf("Updating storage for user %s to %s (old %s) with new expirty %s (old %s) \n", + params.UserEmail, + utils.ByteCountDecimalGIB(storageSize), utils.ByteCountDecimalGIB(userDetails.Subscription.Storage), + date.Format("2006-01-02"), + time.UnixMicro(userDetails.Subscription.ExpiryTime).Format("2006-01-02")) + // press y to confirm + confirmed, _ := internal.ConfirmAction("Are you sure you want to update the storage ('y' or 'n')?") + if !confirmed { + return nil + } else { + err := c.Client.UpdateFreePlanSub(accountCtx, userDetails, storageSize, date.UnixMicro()) + if err != nil { + return err + } else { + fmt.Println("Successfully updated storage and expiry date for user") + } + } + + return nil +} + +func (c *ClICtrl) buildAdminContext(ctx context.Context, adminEmail string) (context.Context, error) { + accounts, err := c.GetAccounts(ctx) + if err != nil { + return nil, err + } + var acc *model.Account + for _, a := range accounts { + if a.Email == adminEmail { + acc = &a + break + } + } + if acc == nil { + return nil, fmt.Errorf("account not found for %s, use `account list` to list accounts", adminEmail) + } + secretInfo, err := c.KeyHolder.LoadSecrets(*acc) + if err != nil { + return nil, err + } + accountCtx := c.buildRequestContext(ctx, *acc) + c.Client.AddToken(acc.AccountKey(), secretInfo.TokenStr()) + return accountCtx, nil +} + +func _parseDateOrDateTime(input string) (time.Time, error) { + var layout string + if strings.Contains(input, " ") { + // If the input contains a space, assume it's a date-time format + layout = "2006-01-02 15:04:05" + } else { + // If there's no space, assume it's just a date + layout = "2006-01-02" + } + return time.Parse(layout, input) +} diff --git a/cli/pkg/model/account.go b/cli/pkg/model/account.go index 7e18a9f66..31f7866ab 100644 --- a/cli/pkg/model/account.go +++ b/cli/pkg/model/account.go @@ -1,6 +1,7 @@ package model import ( + "encoding/base64" "fmt" "github.com/ente-io/cli/internal/api" ) @@ -17,7 +18,7 @@ type Account struct { ExportDir string `json:"exportDir"` } -type UpdateAccountParams struct { +type AccountCommandParams struct { Email string App api.App ExportDir *string @@ -37,3 +38,7 @@ type AccSecretInfo struct { Token []byte PublicKey []byte } + +func (a *AccSecretInfo) TokenStr() string { + return base64.URLEncoding.EncodeToString(a.Token) +} diff --git a/cli/pkg/model/admin.go b/cli/pkg/model/admin.go new file mode 100644 index 000000000..e4d14d75c --- /dev/null +++ b/cli/pkg/model/admin.go @@ -0,0 +1,6 @@ +package model + +type AdminActionForUser struct { + UserEmail string + AdminEmail string +} diff --git a/cli/release.sh b/cli/release.sh index 8e2deba0f..b3946c381 100755 --- a/cli/release.sh +++ b/cli/release.sh @@ -1,5 +1,16 @@ #!/bin/bash +# Fetch the latest tag that starts with "cli-" +# shellcheck disable=SC2046 +# shellcheck disable=SC2006 +LATEST_TAG=$(git describe --tags `git rev-list --tags='cli-*' --max-count=1`) + +# Check if the LATEST_TAG variable is empty +if [ -z "$LATEST_TAG" ]; then + echo "No 'cli-' tag found. Exiting..." + exit 1 +fi +VERSION=${LATEST_TAG#cli-} # Create a "bin" directory if it doesn't exist mkdir -p bin @@ -9,6 +20,7 @@ OS_TARGETS=("windows" "linux" "darwin") # Corresponding architectures for each OS ARCH_TARGETS=("386 amd64" "386 amd64 arm arm64" "amd64 arm64") +export CGO_ENABLED=0 # Loop through each OS target for index in "${!OS_TARGETS[@]}" do @@ -28,16 +40,12 @@ do fi # Build the binary and place it in the "bin" directory - go build -o "bin/$BINARY_NAME" main.go + go build -ldflags="-X main.AppVersion=${VERSION} -s -w" -trimpath -o "bin/$BINARY_NAME" main.go # Print a message indicating the build is complete for the current OS and architecture echo "Built for $OS ($ARCH) as bin/$BINARY_NAME" done done -# Clean up any environment variables -unset GOOS -unset GOARCH - # Print a message indicating the build process is complete echo "Build process completed for all platforms and architectures. Binaries are in the 'bin' directory." diff --git a/cli/utils/constants/constants.go b/cli/utils/constants/constants.go index 7209d6466..e6147ae9a 100644 --- a/cli/utils/constants/constants.go +++ b/cli/utils/constants/constants.go @@ -1,3 +1,4 @@ package constants const CliDataPath = "/cli-data/" +const EnteApiUrl = "https://api.ente.io" diff --git a/cli/utils/convert.go b/cli/utils/convert.go new file mode 100644 index 000000000..5bd7c8026 --- /dev/null +++ b/cli/utils/convert.go @@ -0,0 +1,31 @@ +package utils + +import ( + "fmt" +) + +func ByteCountDecimal(b int64) string { + const unit = 1000 + if b < unit { + return fmt.Sprintf("%d B", b) + } + div, exp := int64(unit), 0 + for n := b / unit; n >= unit; n /= unit { + div *= unit + exp++ + } + return fmt.Sprintf("%.1f %cB", float64(b)/float64(div), "kMGTPE"[exp]) +} + +func ByteCountDecimalGIB(b int64) string { + const unit = 1024 + if b < unit { + return fmt.Sprintf("%d B", b) + } + div, exp := int64(unit), 0 + for n := b / unit; n >= unit; n /= unit { + div *= unit + exp++ + } + return fmt.Sprintf("%.1f %cB", float64(b)/float64(div), "kMGTPE"[exp]) +} diff --git a/cli/utils/time.go b/cli/utils/time.go index a86a9fc9f..fcb61f842 100644 --- a/cli/utils/time.go +++ b/cli/utils/time.go @@ -1,7 +1,6 @@ package utils import ( - "fmt" "log" "time" ) @@ -10,16 +9,3 @@ func TimeTrack(start time.Time, name string) { elapsed := time.Since(start) log.Printf("%s took %s", name, elapsed) } - -func ByteCountDecimal(b int64) string { - const unit = 1000 - if b < unit { - return fmt.Sprintf("%d B", b) - } - div, exp := int64(unit), 0 - for n := b / unit; n >= unit; n /= unit { - div *= unit - exp++ - } - return fmt.Sprintf("%.1f %cB", float64(b)/float64(div), "kMGTPE"[exp]) -} diff --git a/desktop/.github/workflows/build.yml b/desktop/.github/workflows/build.yml index c43dfcfdf..acd744c05 100644 --- a/desktop/.github/workflows/build.yml +++ b/desktop/.github/workflows/build.yml @@ -52,7 +52,4 @@ jobs: # macOS notarization API key API_KEY_ID: ${{ secrets.api_key_id }} API_KEY_ISSUER_ID: ${{ secrets.api_key_issuer_id}} - # setry crash reporting token - SENTRY_AUTH_TOKEN: ${{secrets.sentry_auth_token}} - NEXT_PUBLIC_DISABLE_SENTRY: ${{secrets.next_public_disable_sentry}} USE_HARD_LINKS: false diff --git a/desktop/.gitignore b/desktop/.gitignore index d8a436c36..9b7e0cc60 100644 --- a/desktop/.gitignore +++ b/desktop/.gitignore @@ -1,12 +1,21 @@ -node_modules -app -.next/ -dist -.vscode -buildingSteps.md +# Node +node_modules/ + +# macOS .DS_Store -.idea/ -build/.DS_Store + +# Editors +.vscode/ + +# Local env files .env -.electron-symbols/ -models/ +.env.*.local + +# tsc transpiles src/**/*.ts and emits the generated JS into app +app/ + +# out is a symlink to the photos web app's dir +out + +# electron-builder +dist/ diff --git a/desktop/.husky/pre-commit b/desktop/.husky/pre-commit deleted file mode 100755 index 869b4f5d5..000000000 --- a/desktop/.husky/pre-commit +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/sh -. "$(dirname "$0")/_/husky.sh" - -branch="$(git rev-parse --abbrev-ref HEAD)" - -if [ "$branch" = "main" ]; then - echo "You can't commit directly to main branch" - exit 1 -fi - -npx lint-staged diff --git a/desktop/.prettierrc.json b/desktop/.prettierrc.json index 4510827ca..8b0652597 100644 --- a/desktop/.prettierrc.json +++ b/desktop/.prettierrc.json @@ -1,6 +1,7 @@ { "tabWidth": 4, - "trailingComma": "es5", - "singleQuote": true, - "bracketSameLine": true -} \ No newline at end of file + "plugins": [ + "prettier-plugin-organize-imports", + "prettier-plugin-packagejson" + ] +} diff --git a/desktop/.yarnrc b/desktop/.yarnrc deleted file mode 100644 index 02b1010b3..000000000 --- a/desktop/.yarnrc +++ /dev/null @@ -1 +0,0 @@ -network-timeout 500000 diff --git a/desktop/CHANGELOG.md b/desktop/CHANGELOG.md index 0a1602b94..73aae2397 100644 --- a/desktop/CHANGELOG.md +++ b/desktop/CHANGELOG.md @@ -4,128 +4,128 @@ ### New -- Option to select file download location. -- Add support for searching popular cities -- Sorted duplicates in desecending order of size -- Add Counter to upload section -- Display full name and collection name on hover on dedupe screen photos +- Option to select file download location. +- Add support for searching popular cities +- Sorted duplicates in desecending order of size +- Add Counter to upload section +- Display full name and collection name on hover on dedupe screen photos ### Bug Fixes -- Fix add to album padding issue -- Fix double uncategorized album issue -- Hide Hidden collection files from all section +- Fix add to album padding issue +- Fix double uncategorized album issue +- Hide Hidden collection files from all section ## v1.6.62 ### New -- Integrated onnx clip runner +- Integrated onnx clip runner ### Bug Fixes -- Fixes login button requiring double click issue -- Fixes Collection sort state not preserved issue -- Fixes continuous export causing app crash -- Improves ML related copies for better distinction from clip -- Added Better favicon for light mode -- Fixed face indexing issues -- Fixed thumbnail load issue +- Fixes login button requiring double click issue +- Fixes Collection sort state not preserved issue +- Fixes continuous export causing app crash +- Improves ML related copies for better distinction from clip +- Added Better favicon for light mode +- Fixed face indexing issues +- Fixed thumbnail load issue ## v1.6.60 ### Bug Fixes -- Fix Thumbnail Orientation issue -- Fix ML logging issue +- Fix Thumbnail Orientation issue +- Fix ML logging issue ## v1.6.59 ### New -- Added arm64 builds for linux +- Added arm64 builds for linux ### Bug Fixes -- Fix Editor file not loading issue -- Fix ML results missing thumbnail issue +- Fix Editor file not loading issue +- Fix ML results missing thumbnail issue ## v1.6.58 ### Bug Fixes -- Fix File load issue +- Fix File load issue ## v1.6.57 ### New Features -- Added encrypted Disk caching for files -- Added option to customize cache folder location +- Added encrypted Disk caching for files +- Added option to customize cache folder location ### Bug Fixes -- Fixed caching issue,causing multiple download of file during ml sync +- Fixed caching issue,causing multiple download of file during ml sync ## v1.6.55 ### Bug Fixes -- Added manage family portal option if add-on is active -- Fixed filename date parsing issue -- Fixed storage limit ui glitch -- Fixed dedupe page layout issue -- Fixed ElectronAPI refactoring issue -- Fixed Search related issues +- Added manage family portal option if add-on is active +- Fixed filename date parsing issue +- Fixed storage limit ui glitch +- Fixed dedupe page layout issue +- Fixed ElectronAPI refactoring issue +- Fixed Search related issues ## v1.6.54 ### New Features -- Added support for HEIC and raw image in photo editor +- Added support for HEIC and raw image in photo editor ### Bug Fixes -- Fixed 16bit HDR HEIC images support -- Fixed blocked login due safe storage issue -- Fixed Search related issues -- Fixed issue of watch folder not cleared on logout -- other under the hood ui/ux improvements +- Fixed 16bit HDR HEIC images support +- Fixed blocked login due safe storage issue +- Fixed Search related issues +- Fixed issue of watch folder not cleared on logout +- other under the hood ui/ux improvements ## v1.6.53 ### Bug Fixes -- Fixed watch folder disabled issue -- Fixed BF Add on related issues -- Fixed clip sync issue and added better logging -- Fixed mov file upload -- Fixed clip extraction related issue +- Fixed watch folder disabled issue +- Fixed BF Add on related issues +- Fixed clip sync issue and added better logging +- Fixed mov file upload +- Fixed clip extraction related issue ## v1.6.52 ### New Features -- Added Clip Desktop on windows +- Added Clip Desktop on windows ### Bug Fixes -- fixed google json matching issue -- other under-the-hood changes to improve performance and bug fixes +- fixed google json matching issue +- other under-the-hood changes to improve performance and bug fixes ## v1.6.50 ### New Features -- Added Clip desktop +- Added Clip desktop ### Bug Fixes -- Fixed desktop downloaded file had extra dot in the name -- Cleanup error messages -- fix the motion photo clustering issue -- Add option to disable cf proxy locally -- other under-the-hood changes to improve UX +- Fixed desktop downloaded file had extra dot in the name +- Cleanup error messages +- fix the motion photo clustering issue +- Add option to disable cf proxy locally +- other under-the-hood changes to improve UX ## v1.6.49 @@ -137,54 +137,54 @@ Check out our [blog](https://ente.io/blog/introducing-web-desktop-photo-editor/) ### Bug Fixes -- Fixed misaligned icons in photo-viewer -- Fixed issue with Motion photo upload -- Fixed issue with Live-photo upload -- other minor ux improvement +- Fixed misaligned icons in photo-viewer +- Fixed issue with Motion photo upload +- Fixed issue with Live-photo upload +- other minor ux improvement ## v1.6.46 ### Bug Fixes -- Fixes OOM crashes during file upload [#1379](https://github.com/ente-io/photos-web/pull/1379) +- Fixes OOM crashes during file upload [#1379](https://github.com/ente-io/photos-web/pull/1379) ## v1.6.45 ### Bug Fixes -- Fixed app keeps reloading issue [#235](https://github.com/ente-io/photos-desktop/pull/235) -- Fixed dng and arw preview issue [#1378](https://github.com/ente-io/photos-web/pull/1378) -- Added view crash report option (help menu) for user to share electron crash report locally +- Fixed app keeps reloading issue [#235](https://github.com/ente-io/photos-desktop/pull/235) +- Fixed dng and arw preview issue [#1378](https://github.com/ente-io/photos-web/pull/1378) +- Added view crash report option (help menu) for user to share electron crash report locally ## v1.6.44 -- Upgraded electron to get latest security patches and other improvements. +- Upgraded electron to get latest security patches and other improvements. ## v1.6.43 ### Added -- #### Check for update and changelog option +- #### Check for update and changelog option Added options to check for update manually and a view changelog via the app menubar -- #### Opt out of crash reporting +- #### Opt out of crash reporting Added option to out of a crash reporting, it can accessed from the settings -> preferences -> disable crash reporting -- #### Type search +- #### Type search Added new search option to search files based on file type i.e, image, video, live-photo. -- #### Manual Convert Button +- #### Manual Convert Button In case the video is not playable, Now there is a convert button which can be used to trigger conversion of the video to supported format. -- #### File Download Progress +- #### File Download Progress The file loader now also shows the exact percentage download progress, instead of just a simple loader. -- #### Bug fixes & other enhancements +- #### Bug fixes & other enhancements We have squashed a few pesky bugs that were reported by our community @@ -192,21 +192,21 @@ Check out our [blog](https://ente.io/blog/introducing-web-desktop-photo-editor/) ### Added -- #### Hidden albums +- #### Hidden albums You can now hide albums, just like individual memories. -- #### Email verification +- #### Email verification We have now made email verification optional, so you can sign in with just your email address and password, without waiting for a verification code. You can opt in / out of email verification from Settings > Security. -- #### Download Album +- #### Download Album You can now chose the download location for downloading albums. Along with that we have also added progress bar for album download. -- #### Bug fixes & other enhancements +- #### Bug fixes & other enhancements We have squashed a few pesky bugs that were reported by our community diff --git a/desktop/README.md b/desktop/README.md index 0cc67408b..da74b133f 100644 --- a/desktop/README.md +++ b/desktop/README.md @@ -10,33 +10,28 @@ To know more about Ente, see [our main README](../README.md) or visit ## Building from source -> [!CAUTION] -> -> We moved a few things around when switching to a monorepo recently, so this -> folder might not build with the instructions below. Hang tight, we're on it, -> will fix things if. - -Fetch submodules - -```sh -git submodule update --init --recursive -``` - Install dependencies ```sh yarn install ``` -Run the app +Run in development mode (with hot reload) ```sh -yarn start +yarn dev ``` -To recompile automatically using electron-reload, run this in a separate -terminal: +> [!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`. -```bash -yarn watch +Or create a binary for your platform + +```sh +yarn build ``` + +That's the gist of it. For more development related documentation, see +[docs](docs/README.md). diff --git a/desktop/build/error.html b/desktop/build/error.html index 54b787357..cf1ea149d 100644 --- a/desktop/build/error.html +++ b/desktop/build/error.html @@ -1,20 +1,30 @@ - + + + + + + ente Photos + - - - - - ente Photos - - - -
-
-
Site unreachable, please try again later
- + +
+
+
+ Site unreachable, please try again later +
+ +
-
- - + diff --git a/desktop/build/splash.html b/desktop/build/splash.html index 776f375f8..199c31601 100644 --- a/desktop/build/splash.html +++ b/desktop/build/splash.html @@ -1,30 +1,50 @@ - + + + + + + ente Photos + - - - - - ente Photos - - - -
-
- - - - + +
+
+ + + + + +
-
- - + diff --git a/desktop/build/version.html b/desktop/build/version.html index d104eaba9..b2038edd7 100644 --- a/desktop/build/version.html +++ b/desktop/build/version.html @@ -1,24 +1,24 @@ - + - - Electron Updater Example - - - Current version: vX.Y.Z -
- - - \ No newline at end of file + // Listen for messages + const { ipcRenderer } = require("electron"); + ipcRenderer.on("message", function (event, text) { + var container = document.getElementById("messages"); + var message = document.createElement("div"); + message.innerHTML = text; + container.appendChild(message); + }); + + + diff --git a/desktop/deployment.md b/desktop/deployment.md deleted file mode 100644 index b3cf1ac3b..000000000 --- a/desktop/deployment.md +++ /dev/null @@ -1,25 +0,0 @@ -Notes on how to upload electron symbols directly to sentry instance (bypassing the CF limits) cc @abhi just for future reference - -To upload electron symbols - -1. Create a tunnel -``` -ssh -p 7426 -N -L 8080:localhost:9000 sentry -``` - -2. Add the following env file -``` -NEXT_PUBLIC_IS_SENTRY_ENABLED = yes -SENTRY_ORG = ente -SENTRY_PROJECT = bhari-frame -SENTRY_URL2 = https://sentry.ente.io/ -SENTRY_URL = http://localhost:8080/ -SENTRY_AUTH_TOKEN = xxx -SENTRY_LOG_LEVEL = debug -``` - -3. Run - -``` -node sentry-symbols.js -``` \ No newline at end of file diff --git a/desktop/docs/README.md b/desktop/docs/README.md new file mode 100644 index 000000000..412e7f61c --- /dev/null +++ b/desktop/docs/README.md @@ -0,0 +1,11 @@ +# Developer docs + +If you just want to run the Ente Photos desktop app locally or develop it, you +can do: + + yarn install + yarn dev + +The docs in this directory provide more details that some developers might find +useful. You might also find the developer docs for +[web](../../web/docs/README.md) useful. diff --git a/desktop/docs/dependencies.md b/desktop/docs/dependencies.md new file mode 100644 index 000000000..6b0196723 --- /dev/null +++ b/desktop/docs/dependencies.md @@ -0,0 +1,14 @@ +# Dependencies + +See [web/docs/dependencies.md](../../web/docs/dependencies.md) for general web +specific dependencies. See [electron.md](electron.md) for our main dependency, +Electron. The rest of this document describes the remaining, desktop specific +dependencies that are used by the Photos desktop app. + +## Electron related + +### 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. diff --git a/desktop/docs/dev.md b/desktop/docs/dev.md new file mode 100644 index 000000000..bfa80df69 --- /dev/null +++ b/desktop/docs/dev.md @@ -0,0 +1,4 @@ +# Development tips + +- `yarn build:quick` is a variant of `yarn build` that uses the + `--config.compression=store` flag to (slightly) speed up electron-builder. diff --git a/desktop/docs/electron.md b/desktop/docs/electron.md new file mode 100644 index 000000000..84c47e329 --- /dev/null +++ b/desktop/docs/electron.md @@ -0,0 +1,21 @@ +# Electron + +[Electron](https://www.electronjs.org) is a cross-platform (Linux, Windows, +macOS) way for creating desktop apps using TypeScript. + +Electron embeds Chromium and Node.js in the generated app's binary. The +generated app thus consists of two separate processes - the _main_ process, and +a _renderer_ process. + +- The _main_ process is runs the embedded node. This process can deal with the + host OS - it is conceptually like a `node` repl running on your machine. In our + case, the TypeScript code (in the `src/` directory) gets transpiled by `tsc` + into JavaScript in the `build/app/` directory, which gets bundled in the + generated app's binary and is loaded by the node (main) process when the app + starts. + +- The _renderer_ process is a regular web app that gets loaded into the embedded + Chromium. When the main process starts, it creates a new "window" that shows + this embedded Chromium. In our case, we build and bundle a static export of + the [Photos web app](../web/README.md) in the generated app. This gets loaded + by the embedded Chromium at runtime, acting as the app's UI. diff --git a/desktop/package.json b/desktop/package.json index 6180fd8cd..8d90fca6e 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -1,10 +1,65 @@ { "name": "ente", - "productName": "ente", "version": "1.6.63", "private": true, - "description": "Desktop client for ente.io", + "description": "Desktop client for Ente Photos", + "author": "Ente ", "main": "app/main.js", + "scripts": { + "build": "yarn build-renderer && yarn build-main", + "build-main": "tsc && electron-builder", + "build-main:quick": "tsc && electron-builder --config.compression=store", + "build-renderer": "cd ../web && yarn install && yarn build:photos && cd ../desktop && rm -f out && ln -sf ../web/apps/photos/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-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 \"src/**/*.ts\"", + "lint-fix": "yarn prettier --write . && eslint --fix 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", + "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" + }, + "devDependencies": { + "@types/auto-launch": "^5.0.2", + "@types/ffmpeg-static": "^3.0.1", + "@types/get-folder-size": "^2.0.0", + "@types/node": "18.15.0", + "@types/node-fetch": "^2.6.2", + "@types/promise-fs": "^2.1.1", + "@typescript-eslint/eslint-plugin": "^5.28.0", + "@typescript-eslint/parser": "^5.28.0", + "concurrently": "^7.0.0", + "electron": "^25.8.4", + "electron-builder": "^24.6.4", + "electron-builder-notarize": "^1.2.0", + "electron-download": "^4.1.1", + "eslint": "^7.23.0", + "eslint-config-google": "^0.14.0", + "eslint-config-prettier": "^8.5.0", + "prettier": "^3", + "prettier-plugin-organize-imports": "^3.2", + "prettier-plugin-packagejson": "^2.4", + "typescript": "^4.2.3" + }, "build": { "appId": "io.ente.bhari-frame", "artifactName": "${productName}-${version}-${arch}.${ext}", @@ -42,7 +97,7 @@ ] } ], - "icon": "./build/icon.icns", + "icon": "./resources/icon.icns", "category": "Photography" }, "mac": { @@ -57,98 +112,24 @@ "x64ArchFiles": "Contents/Resources/ggmlclip-mac" }, "afterSign": "electron-builder-notarize", - "extraFiles": [ - { - "from": "build", - "to": "resources", - "filter": [ - "**/*" - ] - } - ], "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" + } + ], "files": [ "app/**/*", - { - "from": "ui/apps/photos", - "to": "ui", - "filter": [ - "!**/*", - "out/**/*" - ] - } + "out" ] }, - "scripts": { - "postinstall": "electron-builder install-app-deps", - "prebuild": "eslint \"src/**/*.{js,jsx,ts,tsx}\"", - "prepare": "husky install", - "lint": "eslint -c .eslintrc --ext .ts src", - "watch": "tsc -w", - "build-main": "yarn install && tsc", - "start-main": "yarn build-main && electron app/main.js", - "start-renderer": "cd ui && yarn install && yarn dev:photos", - "start": "concurrently \"yarn start-main\" \"yarn start-renderer\"", - "build-renderer": "cd ui && yarn install && yarn export:photos", - "build": "yarn build-renderer && yarn build-main", - "test-release": "cross-env IS_TEST_RELEASE=true yarn build && electron-builder --config.compression=store" - }, - "author": "ente ", - "devDependencies": { - "@sentry/cli": "^1.68.0", - "@types/auto-launch": "^5.0.2", - "@types/ffmpeg-static": "^3.0.1", - "@types/get-folder-size": "^2.0.0", - "@types/node": "18.15.0", - "@types/node-fetch": "^2.6.2", - "@types/promise-fs": "^2.1.1", - "@typescript-eslint/eslint-plugin": "^5.28.0", - "@typescript-eslint/parser": "^5.28.0", - "concurrently": "^7.0.0", - "cross-env": "^7.0.3", - "electron": "^25.8.4", - "electron-builder": "^24.6.4", - "electron-builder-notarize": "^1.2.0", - "electron-download": "^4.1.1", - "eslint": "^7.23.0", - "eslint-config-google": "^0.14.0", - "eslint-config-prettier": "^8.5.0", - "husky": "^8.0.1", - "lint-staged": "^13.0.1", - "prettier": "2.5.1", - "typescript": "^4.2.3" - }, - "dependencies": { - "@sentry/electron": "^2.5.1", - "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", - "next-electron-server": "file:./thirdparty/next-electron-server", - "node-fetch": "^2.6.7", - "node-stream-zip": "^1.15.0", - "onnxruntime-node": "^1.16.3", - "promise-fs": "^2.1.1" - }, + "productName": "ente", "standard": { "parser": "babel-eslint" - }, - "lint-staged": { - "src/**/*.{js,jsx,ts,tsx}": [ - "eslint --fix", - "prettier --write --ignore-unknown" - ] } } diff --git a/desktop/sentry-symbols.js b/desktop/sentry-symbols.js deleted file mode 100644 index 955cda5f5..000000000 --- a/desktop/sentry-symbols.js +++ /dev/null @@ -1,94 +0,0 @@ -#!/usr/bin/env node - -let SentryCli; -let download; - -try { - SentryCli = require('@sentry/cli'); - download = require('electron-download'); -} catch (e) { - console.error('ERROR: Missing required packages, please run:'); - console.error('npm install --save-dev @sentry/cli electron-download'); - process.exit(1); -} - -const SYMBOL_CACHE_FOLDER = '.electron-symbols'; -const sentryCli = new SentryCli('./sentry.properties'); - -async function main() { - const version = getElectronVersion(); - if (!version) { - console.error('Cannot detect electron version, check that electron is installed'); - return; - } - - console.log('We are starting to download all possible electron symbols'); - console.log('We need it in order to symbolicate native crashes'); - console.log( - 'This step is only needed once whenever you update your electron version', - ); - console.log('Just call this script again it should do everything for you.'); - - let zipPath = await downloadSymbols({ - version, - platform: 'darwin', - arch: 'x64', - dsym: true, - }); - await sentryCli.execute(['upload-dif', '-t', 'dsym', zipPath], true); - - zipPath = await downloadSymbols({ - version, - platform: 'win32', - arch: 'ia32', - symbols: true, - }); - await sentryCli.execute(['upload-dif', '-t', 'breakpad', zipPath], true); - - zipPath = await downloadSymbols({ - version, - platform: 'win32', - arch: 'x64', - symbols: true, - }); - await sentryCli.execute(['upload-dif', '-t', 'breakpad', zipPath], true); - - zipPath = await downloadSymbols({ - version, - platform: 'linux', - arch: 'x64', - symbols: true, - }); - await sentryCli.execute(['upload-dif', '-t', 'breakpad', zipPath], true); - - console.log('Finished downloading and uploading to Sentry'); - console.log(`Feel free to delete the ${SYMBOL_CACHE_FOLDER}`); -} - -function getElectronVersion() { - try { - return require('electron/package.json').version; - } catch (error) { - return undefined; - } -} - -async function downloadSymbols(options) { - return new Promise((resolve, reject) => { - download( - { - ...options, - cache: SYMBOL_CACHE_FOLDER, - }, - (err, zipPath) => { - if (err) { - reject(err); - } else { - resolve(zipPath); - } - }, - ); - }); -} - -main().catch(e => console.error(e)); diff --git a/desktop/sentry.properties b/desktop/sentry.properties deleted file mode 100644 index d9303317c..000000000 --- a/desktop/sentry.properties +++ /dev/null @@ -1,3 +0,0 @@ -defaults.url=https://sentry.ente.io/ -defaults.org=ente -defaults.project=desktop-photos diff --git a/desktop/src/api/cache.ts b/desktop/src/api/cache.ts index ea1151ae1..86ba4378c 100644 --- a/desktop/src/api/cache.ts +++ b/desktop/src/api/cache.ts @@ -1,16 +1,16 @@ -import { ipcRenderer } from 'electron/renderer'; -import path from 'path'; -import { existsSync, mkdir, rmSync } from 'promise-fs'; -import { DiskCache } from '../services/diskCache'; +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 ENTE_CACHE_DIR_NAME = "ente"; export const getCacheDirectory = async () => { const customCacheDir = await getCustomCacheDirectory(); if (customCacheDir && existsSync(customCacheDir)) { return customCacheDir; } - const defaultSystemCacheDir = await ipcRenderer.invoke('get-path', 'cache'); + const defaultSystemCacheDir = await ipcRenderer.invoke("get-path", "cache"); return path.join(defaultSystemCacheDir, ENTE_CACHE_DIR_NAME); }; @@ -22,7 +22,7 @@ const getCacheBucketDir = async (cacheName: string) => { export async function openDiskCache( cacheName: string, - cacheLimitInBytes?: number + cacheLimitInBytes?: number, ) { const cacheBucketDir = await getCacheBucketDir(cacheName); if (!existsSync(cacheBucketDir)) { @@ -42,11 +42,11 @@ export async function deleteDiskCache(cacheName: string) { } export async function setCustomCacheDirectory( - directory: string + directory: string, ): Promise { - await ipcRenderer.invoke('set-custom-cache-directory', directory); + await ipcRenderer.invoke("set-custom-cache-directory", directory); } async function getCustomCacheDirectory(): Promise { - return await ipcRenderer.invoke('get-custom-cache-directory'); + return await ipcRenderer.invoke("get-custom-cache-directory"); } diff --git a/desktop/src/api/clip.ts b/desktop/src/api/clip.ts index 2e37a97e6..d2469e7b9 100644 --- a/desktop/src/api/clip.ts +++ b/desktop/src/api/clip.ts @@ -1,22 +1,21 @@ -import { ipcRenderer } from 'electron'; -import { writeStream } from '../services/fs'; -import { isExecError } from '../utils/error'; -import { parseExecError } from '../utils/error'; -import { Model } from '../types'; +import { ipcRenderer } from "electron"; +import { writeStream } from "../services/fs"; +import { Model } from "../types"; +import { isExecError, parseExecError } from "../utils/error"; export async function computeImageEmbedding( model: Model, - imageData: Uint8Array + imageData: Uint8Array, ): Promise { let tempInputFilePath = null; try { - tempInputFilePath = await ipcRenderer.invoke('get-temp-file-path', ''); + 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', + "compute-image-embedding", model, - tempInputFilePath + tempInputFilePath, ); return embedding; } catch (err) { @@ -28,20 +27,20 @@ export async function computeImageEmbedding( } } finally { if (tempInputFilePath) { - await ipcRenderer.invoke('remove-temp-file', tempInputFilePath); + await ipcRenderer.invoke("remove-temp-file", tempInputFilePath); } } } export async function computeTextEmbedding( model: Model, - text: string + text: string, ): Promise { try { const embedding = await ipcRenderer.invoke( - 'compute-text-embedding', + "compute-text-embedding", model, - text + text, ); return embedding; } catch (err) { diff --git a/desktop/src/api/common.ts b/desktop/src/api/common.ts index fb8cf5224..f18506981 100644 --- a/desktop/src/api/common.ts +++ b/desktop/src/api/common.ts @@ -1,44 +1,39 @@ -import { ipcRenderer } from 'electron/renderer'; -import { logError } from '../services/logging'; +import { ipcRenderer } from "electron/renderer"; +import { logError } from "../services/logging"; export const selectDirectory = async (): Promise => { try { - return await ipcRenderer.invoke('select-dir'); + return await ipcRenderer.invoke("select-dir"); } catch (e) { - logError(e, 'error while selecting root directory'); + logError(e, "error while selecting root directory"); } }; export const getAppVersion = async (): Promise => { try { - return await ipcRenderer.invoke('get-app-version'); + return await ipcRenderer.invoke("get-app-version"); } catch (e) { - logError(e, 'failed to get release version'); + logError(e, "failed to get release version"); throw e; } }; export const openDirectory = async (dirPath: string): Promise => { try { - await ipcRenderer.invoke('open-dir', dirPath); + await ipcRenderer.invoke("open-dir", dirPath); } catch (e) { - logError(e, 'error while opening directory'); + logError(e, "error while opening directory"); throw e; } }; -export const getPlatform = async (): Promise<'mac' | 'windows' | 'linux'> => { +export const getPlatform = async (): Promise<"mac" | "windows" | "linux"> => { try { - return await ipcRenderer.invoke('get-platform'); + return await ipcRenderer.invoke("get-platform"); } catch (e) { - logError(e, 'failed to get platform'); + logError(e, "failed to get platform"); throw e; } }; -export { - logToDisk, - openLogDirectory, - getSentryUserID, - updateOptOutOfCrashReports, -} from '../services/logging'; +export { logToDisk, openLogDirectory } from "../services/logging"; diff --git a/desktop/src/api/electronStore.ts b/desktop/src/api/electronStore.ts index b5ba5ea3e..5f84776e1 100644 --- a/desktop/src/api/electronStore.ts +++ b/desktop/src/api/electronStore.ts @@ -1,9 +1,8 @@ -import { keysStore } from '../stores/keys.store'; -import { safeStorageStore } from '../stores/safeStorage.store'; -import { uploadStatusStore } from '../stores/upload.store'; -import { logError } from '../services/logging'; -import { userPreferencesStore } from '../stores/userPreferences.store'; -import { watchStore } from '../stores/watch.store'; +import { logError } from "../services/logging"; +import { keysStore } from "../stores/keys.store"; +import { safeStorageStore } from "../stores/safeStorage.store"; +import { uploadStatusStore } from "../stores/upload.store"; +import { watchStore } from "../stores/watch.store"; export const clearElectronStore = () => { try { @@ -11,9 +10,8 @@ export const clearElectronStore = () => { keysStore.clear(); safeStorageStore.clear(); watchStore.clear(); - userPreferencesStore.delete('optOutOfCrashReports'); } catch (e) { - logError(e, 'error while clearing electron store'); + logError(e, "error while clearing electron store"); throw e; } }; diff --git a/desktop/src/api/export.ts b/desktop/src/api/export.ts index 50a9a88d7..8adaa236f 100644 --- a/desktop/src/api/export.ts +++ b/desktop/src/api/export.ts @@ -1,5 +1,5 @@ -import { writeStream } from './../services/fs'; -import * as fs from 'promise-fs'; +import * as fs from "promise-fs"; +import { writeStream } from "./../services/fs"; export const exists = (path: string) => { return fs.existsSync(path); @@ -13,7 +13,7 @@ export const checkExistsAndCreateDir = async (dirPath: string) => { export const saveStreamToDisk = async ( filePath: string, - fileStream: ReadableStream + fileStream: ReadableStream, ) => { await writeStream(filePath, fileStream); }; diff --git a/desktop/src/api/ffmpeg.ts b/desktop/src/api/ffmpeg.ts index fca3ca83e..9d11183a8 100644 --- a/desktop/src/api/ffmpeg.ts +++ b/desktop/src/api/ffmpeg.ts @@ -1,22 +1,22 @@ -import { ipcRenderer } from 'electron'; -import { existsSync } from 'fs'; -import { writeStream } from '../services/fs'; -import { logError } from '../services/logging'; -import { ElectronFile } from '../types'; +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 + dontTimeout?: boolean, ) { let inputFilePath = null; let createdTempInputFile = null; try { if (!existsSync(inputFile.path)) { const tempFilePath = await ipcRenderer.invoke( - 'get-temp-file-path', - inputFile.name + "get-temp-file-path", + inputFile.name, ); await writeStream(tempFilePath, await inputFile.stream()); inputFilePath = tempFilePath; @@ -25,19 +25,19 @@ export async function runFFmpegCmd( inputFilePath = inputFile.path; } const outputFileData = await ipcRenderer.invoke( - 'run-ffmpeg-cmd', + "run-ffmpeg-cmd", cmd, inputFilePath, outputFileName, - dontTimeout + dontTimeout, ); return new File([outputFileData], outputFileName); } finally { if (createdTempInputFile) { try { - await ipcRenderer.invoke('remove-temp-file', inputFilePath); + await ipcRenderer.invoke("remove-temp-file", inputFilePath); } catch (e) { - logError(e, 'failed to deleteTempFile'); + logError(e, "failed to deleteTempFile"); } } } diff --git a/desktop/src/api/fs.ts b/desktop/src/api/fs.ts index 37890ae91..d9ec2eeec 100644 --- a/desktop/src/api/fs.ts +++ b/desktop/src/api/fs.ts @@ -1,4 +1,4 @@ -import { getElectronFile, getDirFilePaths } from '../services/fs'; +import { getDirFilePaths, getElectronFile } from "../services/fs"; export async function getDirFiles(dirPath: string) { const files = await getDirFilePaths(dirPath); @@ -6,10 +6,10 @@ export async function getDirFiles(dirPath: string) { return electronFiles; } export { + deleteFile, + deleteFolder, isFolder, moveFile, - deleteFolder, - deleteFile, - rename, readTextFile, -} from '../services/fs'; + rename, +} from "../services/fs"; diff --git a/desktop/src/api/imageProcessor.ts b/desktop/src/api/imageProcessor.ts index 6b6803eb7..9d93aecd1 100644 --- a/desktop/src/api/imageProcessor.ts +++ b/desktop/src/api/imageProcessor.ts @@ -1,22 +1,22 @@ -import { CustomErrors } from '../constants/errors'; -import { ipcRenderer } from 'electron/renderer'; -import { existsSync } from 'fs'; -import { writeStream } from '../services/fs'; -import { logError } from '../services/logging'; -import { ElectronFile } from '../types'; -import { isPlatform } from '../utils/common/platform'; +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 + filename: string, ): Promise { - if (isPlatform('windows')) { + if (isPlatform("windows")) { throw Error(CustomErrors.WINDOWS_NATIVE_IMAGE_PROCESSING_NOT_SUPPORTED); } const convertedFileData = await ipcRenderer.invoke( - 'convert-to-jpeg', + "convert-to-jpeg", fileData, - filename + filename, ); return convertedFileData; } @@ -24,20 +24,20 @@ export async function convertToJPEG( export async function generateImageThumbnail( inputFile: File | ElectronFile, maxDimension: number, - maxSize: number + maxSize: number, ): Promise { let inputFilePath = null; let createdTempInputFile = null; try { - if (isPlatform('windows')) { + if (isPlatform("windows")) { throw Error( - CustomErrors.WINDOWS_NATIVE_IMAGE_PROCESSING_NOT_SUPPORTED + CustomErrors.WINDOWS_NATIVE_IMAGE_PROCESSING_NOT_SUPPORTED, ); } if (!existsSync(inputFile.path)) { const tempFilePath = await ipcRenderer.invoke( - 'get-temp-file-path', - inputFile.name + "get-temp-file-path", + inputFile.name, ); await writeStream(tempFilePath, await inputFile.stream()); inputFilePath = tempFilePath; @@ -46,18 +46,18 @@ export async function generateImageThumbnail( inputFilePath = inputFile.path; } const thumbnail = await ipcRenderer.invoke( - 'generate-image-thumbnail', + "generate-image-thumbnail", inputFilePath, maxDimension, - maxSize + maxSize, ); return thumbnail; } finally { if (createdTempInputFile) { try { - await ipcRenderer.invoke('remove-temp-file', inputFilePath); + await ipcRenderer.invoke("remove-temp-file", inputFilePath); } catch (e) { - logError(e, 'failed to deleteTempFile'); + logError(e, "failed to deleteTempFile"); } } } diff --git a/desktop/src/api/safeStorage.ts b/desktop/src/api/safeStorage.ts index 804f23f5b..64c489195 100644 --- a/desktop/src/api/safeStorage.ts +++ b/desktop/src/api/safeStorage.ts @@ -1,32 +1,32 @@ -import { ipcRenderer } from 'electron'; -import { safeStorageStore } from '../stores/safeStorage.store'; -import { logError } from '../services/logging'; +import { ipcRenderer } from "electron"; +import { logError } from "../services/logging"; +import { safeStorageStore } from "../stores/safeStorage.store"; export async function setEncryptionKey(encryptionKey: string) { try { const encryptedKey: Buffer = await ipcRenderer.invoke( - 'safeStorage-encrypt', - encryptionKey + "safeStorage-encrypt", + encryptionKey, ); - const b64EncryptedKey = Buffer.from(encryptedKey).toString('base64'); - safeStorageStore.set('encryptionKey', b64EncryptedKey); + const b64EncryptedKey = Buffer.from(encryptedKey).toString("base64"); + safeStorageStore.set("encryptionKey", b64EncryptedKey); } catch (e) { - logError(e, 'setEncryptionKey failed'); + logError(e, "setEncryptionKey failed"); throw e; } } export async function getEncryptionKey(): Promise { try { - const b64EncryptedKey = safeStorageStore.get('encryptionKey'); + const b64EncryptedKey = safeStorageStore.get("encryptionKey"); if (b64EncryptedKey) { const keyBuffer = new Uint8Array( - Buffer.from(b64EncryptedKey, 'base64') + Buffer.from(b64EncryptedKey, "base64"), ); - return await ipcRenderer.invoke('safeStorage-decrypt', keyBuffer); + return await ipcRenderer.invoke("safeStorage-decrypt", keyBuffer); } } catch (e) { - logError(e, 'getEncryptionKey failed'); + logError(e, "getEncryptionKey failed"); throw e; } } diff --git a/desktop/src/api/system.ts b/desktop/src/api/system.ts index 8879f19b6..a4dc91e05 100644 --- a/desktop/src/api/system.ts +++ b/desktop/src/api/system.ts @@ -1,37 +1,37 @@ -import { ipcRenderer } from 'electron'; -import { AppUpdateInfo } from '../types'; +import { ipcRenderer } from "electron"; +import { AppUpdateInfo } from "../types"; export const sendNotification = (content: string) => { - ipcRenderer.send('send-notification', content); + ipcRenderer.send("send-notification", content); }; export const reloadWindow = () => { - ipcRenderer.send('reload-window'); + ipcRenderer.send("reload-window"); }; export const registerUpdateEventListener = ( - showUpdateDialog: (updateInfo: AppUpdateInfo) => void + showUpdateDialog: (updateInfo: AppUpdateInfo) => void, ) => { - ipcRenderer.removeAllListeners('show-update-dialog'); - ipcRenderer.on('show-update-dialog', (_, updateInfo: AppUpdateInfo) => { + 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', () => { + ipcRenderer.removeAllListeners("app-in-foreground"); + ipcRenderer.on("app-in-foreground", () => { onForeground(); }); }; export const updateAndRestart = () => { - ipcRenderer.send('update-and-restart'); + ipcRenderer.send("update-and-restart"); }; export const skipAppUpdate = (version: string) => { - ipcRenderer.send('skip-app-update', version); + ipcRenderer.send("skip-app-update", version); }; export const muteUpdateNotification = (version: string) => { - ipcRenderer.send('mute-update-notification', version); + ipcRenderer.send("mute-update-notification", version); }; diff --git a/desktop/src/api/upload.ts b/desktop/src/api/upload.ts index d6611763f..280ff084f 100644 --- a/desktop/src/api/upload.ts +++ b/desktop/src/api/upload.ts @@ -1,17 +1,17 @@ -import { getElectronFile } from './../services/fs'; -import { uploadStatusStore } from '../stores/upload.store'; -import { ElectronFile, FILE_PATH_TYPE } from '../types'; -import { logError } from '../services/logging'; -import { ipcRenderer } from 'electron'; +import { ipcRenderer } from "electron"; +import { logError } from "../services/logging"; import { getElectronFilesFromGoogleZip, getSavedFilePaths, -} from '../services/upload'; +} from "../services/upload"; +import { uploadStatusStore } from "../stores/upload.store"; +import { ElectronFile, FILE_PATH_TYPE } from "../types"; +import { getElectronFile } from "./../services/fs"; export const getPendingUploads = async () => { const filePaths = getSavedFilePaths(FILE_PATH_TYPE.FILES); const zipPaths = getSavedFilePaths(FILE_PATH_TYPE.ZIPS); - const collectionName = uploadStatusStore.get('collectionName'); + const collectionName = uploadStatusStore.get("collectionName"); let files: ElectronFile[] = []; let type: FILE_PATH_TYPE; @@ -39,31 +39,31 @@ export const getPendingUploads = async () => { export const showUploadDirsDialog = async () => { try { const filePaths: string[] = await ipcRenderer.invoke( - 'show-upload-dirs-dialog' + "show-upload-dirs-dialog", ); const files = await Promise.all(filePaths.map(getElectronFile)); return files; } catch (e) { - logError(e, 'error while selecting folders'); + logError(e, "error while selecting folders"); } }; export const showUploadFilesDialog = async () => { try { const filePaths: string[] = await ipcRenderer.invoke( - 'show-upload-files-dialog' + "show-upload-files-dialog", ); const files = await Promise.all(filePaths.map(getElectronFile)); return files; } catch (e) { - logError(e, 'error while selecting files'); + logError(e, "error while selecting files"); } }; export const showUploadZipDialog = async () => { try { const filePaths: string[] = await ipcRenderer.invoke( - 'show-upload-zip-dialog' + "show-upload-zip-dialog", ); let files: ElectronFile[] = []; @@ -79,12 +79,12 @@ export const showUploadZipDialog = async () => { files, }; } catch (e) { - logError(e, 'error while selecting zips'); + logError(e, "error while selecting zips"); } }; export { - setToUploadFiles, getElectronFilesFromGoogleZip, setToUploadCollection, -} from '../services/upload'; + setToUploadFiles, +} from "../services/upload"; diff --git a/desktop/src/api/watch.ts b/desktop/src/api/watch.ts index 20008375e..1b7a4ac3c 100644 --- a/desktop/src/api/watch.ts +++ b/desktop/src/api/watch.ts @@ -1,15 +1,15 @@ -import { isMappingPresent } from '../utils/watch'; -import path from 'path'; -import { ipcRenderer } from 'electron'; -import { ElectronFile, WatchMapping } from '../types'; -import { getElectronFile } from '../services/fs'; -import { getWatchMappings, setWatchMappings } from '../services/watch'; -import ElectronLog from 'electron-log'; +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 + uploadStrategy: number, ) { ElectronLog.log(`Adding watch mapping: ${folderPath}`); const watchMappings = getWatchMappings(); @@ -17,7 +17,7 @@ export async function addWatchMapping( throw new Error(`Watch mapping already exists`); } - await ipcRenderer.invoke('add-watcher', { + await ipcRenderer.invoke("add-watcher", { dir: folderPath, }); @@ -35,19 +35,19 @@ export async function addWatchMapping( export async function removeWatchMapping(folderPath: string) { let watchMappings = getWatchMappings(); const watchMapping = watchMappings.find( - (mapping) => mapping.folderPath === folderPath + (mapping) => mapping.folderPath === folderPath, ); if (!watchMapping) { throw new Error(`Watch mapping does not exist`); } - await ipcRenderer.invoke('remove-watcher', { + await ipcRenderer.invoke("remove-watcher", { dir: watchMapping.folderPath, }); watchMappings = watchMappings.filter( - (mapping) => mapping.folderPath !== watchMapping.folderPath + (mapping) => mapping.folderPath !== watchMapping.folderPath, ); setWatchMappings(watchMappings); @@ -55,11 +55,11 @@ export async function removeWatchMapping(folderPath: string) { export function updateWatchMappingSyncedFiles( folderPath: string, - files: WatchMapping['syncedFiles'] + files: WatchMapping["syncedFiles"], ): void { const watchMappings = getWatchMappings(); const watchMapping = watchMappings.find( - (mapping) => mapping.folderPath === folderPath + (mapping) => mapping.folderPath === folderPath, ); if (!watchMapping) { @@ -72,11 +72,11 @@ export function updateWatchMappingSyncedFiles( export function updateWatchMappingIgnoredFiles( folderPath: string, - files: WatchMapping['ignoredFiles'] + files: WatchMapping["ignoredFiles"], ): void { const watchMappings = getWatchMappings(); const watchMapping = watchMappings.find( - (mapping) => mapping.folderPath === folderPath + (mapping) => mapping.folderPath === folderPath, ); if (!watchMapping) { @@ -90,25 +90,25 @@ export function updateWatchMappingIgnoredFiles( export function registerWatcherFunctions( addFile: (file: ElectronFile) => Promise, removeFile: (path: string) => Promise, - removeFolder: (folderPath: 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) => { + 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) => { + 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) => { + 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'; +export { getWatchMappings } from "../services/watch"; diff --git a/desktop/src/config/index.ts b/desktop/src/config/index.ts deleted file mode 100644 index b5cd9ef4b..000000000 --- a/desktop/src/config/index.ts +++ /dev/null @@ -1,20 +0,0 @@ -const PROD_HOST_URL: string = 'ente://app'; -const RENDERER_OUTPUT_DIR: string = './ui/out'; -const LOG_FILENAME = 'ente.log'; -const MAX_LOG_SIZE = 50 * 1024 * 1024; // 50MB - -const FILE_STREAM_CHUNK_SIZE: number = 4 * 1024 * 1024; - -const SENTRY_DSN = 'https://759d8498487a81ac33a0c2efa2a42c4f@sentry.ente.io/9'; - -const RELEASE_VERSION = require('../../package.json').version; - -export { - PROD_HOST_URL, - RENDERER_OUTPUT_DIR, - FILE_STREAM_CHUNK_SIZE, - LOG_FILENAME, - MAX_LOG_SIZE, - SENTRY_DSN, - RELEASE_VERSION, -}; diff --git a/desktop/src/constants/errors.ts b/desktop/src/constants/errors.ts index e54e856ce..97aef616c 100644 --- a/desktop/src/constants/errors.ts +++ b/desktop/src/constants/errors.ts @@ -1,12 +1,12 @@ export const CustomErrors = { WINDOWS_NATIVE_IMAGE_PROCESSING_NOT_SUPPORTED: - 'Windows native image processing is not supported', + "Windows native image processing is not supported", INVALID_OS: (os: string) => `Invalid OS - ${os}`, - WAIT_TIME_EXCEEDED: 'Wait time exceeded', + 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', + "Model download pending, skipping clip search request", + INVALID_FILE_PATH: "Invalid file path", INVALID_CLIP_MODEL: (model: string) => `Invalid Clip model - ${model}`, }; diff --git a/desktop/src/main.ts b/desktop/src/main.ts index a275e6899..a280a9b59 100644 --- a/desktop/src/main.ts +++ b/desktop/src/main.ts @@ -1,27 +1,25 @@ -import { app, BrowserWindow } from 'electron'; -import { createWindow } from './utils/createWindow'; -import setupIpcComs from './utils/ipcComms'; -import { initWatcher } from './services/chokidar'; -import { addAllowOriginHeader } from './utils/cors'; +import { app, BrowserWindow } from "electron"; +import electronReload from "electron-reload"; +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 { - setupTrayItem, - handleDownloads, - setupMacWindowOnDockIconClick, - setupMainMenu, - setupMainHotReload, - setupNextElectronServe, enableSharedArrayBufferSupport, handleDockIconHideOnAutoLaunch, + handleDownloads, + handleExternalLinks, handleUpdates, logSystemInfo, - handleExternalLinks, -} from './utils/main'; -import { initSentry } from './services/sentry'; -import { setupLogging } from './utils/logging'; -import { isDev } from './utils/common'; -import { setupMainProcessStatsLogger } from './utils/processStats'; -import { setupAppEventEmitter } from './utils/events'; -import { getOptOutOfCrashReports } from './services/userPreference'; + setupMacWindowOnDockIconClick, + setupMainMenu, + setupTrayItem, +} from "./utils/main"; +import { setupMainProcessStatsLogger } from "./utils/processStats"; let mainWindow: BrowserWindow; @@ -29,8 +27,6 @@ let appIsQuitting = false; let updateIsAvailable = false; -let optedOutOfCrashReports = false; - export const isAppQuitting = (): boolean => { return appIsQuitting; }; @@ -42,22 +38,48 @@ export const setIsAppQuitting = (value: boolean): void => { export const isUpdateAvailable = (): boolean => { return updateIsAvailable; }; + export const setIsUpdateAvailable = (value: boolean): void => { updateIsAvailable = value; }; -export const hasOptedOutOfCrashReports = (): boolean => { - return optedOutOfCrashReports; +/** + * 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, {}); + } }; -export const updateOptOutOfCrashReports = (value: boolean): void => { - optedOutOfCrashReports = value; +/** + * The URL where the renderer HTML is being served from. + */ +export const rendererURL = "next://app"; + +/** + * next-electron-server allows up to directly use the output of `next build` in + * production mode and `next dev` in development mode, whilst keeping the rest + * of our code the same. + * + * It uses protocol handlers to serve files from the "next://app" protocol + * + * - In development this is proxied to http://localhost:3000 + * - In production it serves files from the `/out` directory + * + * For more details, see this comparison: + * https://github.com/HaNdTriX/next-electron-server/issues/5 + */ +const setupRendererServer = () => { + serveNextAt(rendererURL); }; setupMainHotReload(); - -setupNextElectronServe(); - +setupRendererServer(); setupLogging(isDev); const gotTheLock = app.requestSingleInstanceLock(); @@ -66,7 +88,7 @@ if (!gotTheLock) { } else { handleDockIconHideOnAutoLaunch(); enableSharedArrayBufferSupport(); - app.on('second-instance', () => { + app.on("second-instance", () => { // Someone tried to run a second instance, we should focus our window. if (mainWindow) { mainWindow.show(); @@ -80,14 +102,9 @@ 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. - app.on('ready', async () => { + app.on("ready", async () => { logSystemInfo(); setupMainProcessStatsLogger(); - const hasOptedOutOfCrashReports = getOptOutOfCrashReports(); - updateOptOutOfCrashReports(hasOptedOutOfCrashReports); - if (!hasOptedOutOfCrashReports) { - initSentry(); - } mainWindow = await createWindow(); const tray = setupTrayItem(mainWindow); const watcher = initWatcher(mainWindow); @@ -101,5 +118,5 @@ if (!gotTheLock) { setupAppEventEmitter(mainWindow); }); - app.on('before-quit', () => setIsAppQuitting(true)); + app.on("before-quit", () => setIsAppQuitting(true)); } diff --git a/desktop/src/preload.ts b/desktop/src/preload.ts index e19f61447..a602e76bb 100644 --- a/desktop/src/preload.ts +++ b/desktop/src/preload.ts @@ -1,79 +1,75 @@ import { + deleteDiskCache, + getCacheDirectory, + openDiskCache, + setCustomCacheDirectory, +} from "./api/cache"; +import { computeImageEmbedding, computeTextEmbedding } from "./api/clip"; +import { + getAppVersion, + getPlatform, + logToDisk, + openDirectory, + openLogDirectory, + selectDirectory, +} from "./api/common"; +import { clearElectronStore } from "./api/electronStore"; +import { + checkExistsAndCreateDir, + exists, + saveFileToDisk, + saveStreamToDisk, +} from "./api/export"; +import { runFFmpegCmd } from "./api/ffmpeg"; +import { + deleteFile, + deleteFolder, + getDirFiles, + isFolder, + moveFile, + readTextFile, + rename, +} from "./api/fs"; +import { convertToJPEG, generateImageThumbnail } from "./api/imageProcessor"; +import { getEncryptionKey, setEncryptionKey } from "./api/safeStorage"; +import { + muteUpdateNotification, + registerForegroundEventListener, registerUpdateEventListener, reloadWindow, sendNotification, - updateAndRestart, skipAppUpdate, - muteUpdateNotification, - registerForegroundEventListener, -} from './api/system'; + updateAndRestart, +} from "./api/system"; import { + getElectronFilesFromGoogleZip, + getPendingUploads, + setToUploadCollection, + setToUploadFiles, showUploadDirsDialog, showUploadFilesDialog, showUploadZipDialog, - getPendingUploads, - setToUploadFiles, - getElectronFilesFromGoogleZip, - setToUploadCollection, -} from './api/upload'; +} from "./api/upload"; import { - registerWatcherFunctions, addWatchMapping, - removeWatchMapping, - updateWatchMappingSyncedFiles, - updateWatchMappingIgnoredFiles, getWatchMappings, -} from './api/watch'; -import { getEncryptionKey, setEncryptionKey } from './api/safeStorage'; -import { clearElectronStore } from './api/electronStore'; + registerWatcherFunctions, + removeWatchMapping, + updateWatchMappingIgnoredFiles, + updateWatchMappingSyncedFiles, +} from "./api/watch"; +import { setupLogging } from "./utils/logging"; import { - openDiskCache, - deleteDiskCache, - getCacheDirectory, - setCustomCacheDirectory, -} from './api/cache'; -import { - checkExistsAndCreateDir, - saveStreamToDisk, - saveFileToDisk, - exists, -} from './api/export'; -import { - selectDirectory, - logToDisk, - openLogDirectory, - getSentryUserID, - getAppVersion, - openDirectory, - updateOptOutOfCrashReports, - getPlatform, -} from './api/common'; -import { fixHotReloadNext12 } from './utils/preload'; -import { - isFolder, - getDirFiles, - moveFile, - deleteFolder, - rename, - readTextFile, - deleteFile, -} from './api/fs'; -import { convertToJPEG, generateImageThumbnail } from './api/imageProcessor'; -import { setupLogging } from './utils/logging'; -import { - setupRendererProcessStatsLogger, logRendererProcessMemoryUsage, -} from './utils/processStats'; -import { runFFmpegCmd } from './api/ffmpeg'; -import { computeImageEmbedding, computeTextEmbedding } from './api/clip'; + setupRendererProcessStatsLogger, +} from "./utils/processStats"; -fixHotReloadNext12(); setupLogging(); setupRendererProcessStatsLogger(); const windowObject: any = window; -windowObject['ElectronAPIs'] = { +windowObject["ElectronAPIs"] = { exists, checkExistsAndCreateDir, saveStreamToDisk, @@ -108,7 +104,6 @@ windowObject['ElectronAPIs'] = { registerUpdateEventListener, updateAndRestart, skipAppUpdate, - getSentryUserID, getAppVersion, runFFmpegCmd, muteUpdateNotification, @@ -120,7 +115,6 @@ windowObject['ElectronAPIs'] = { deleteFolder, rename, deleteFile, - updateOptOutOfCrashReports, computeImageEmbedding, computeTextEmbedding, getPlatform, diff --git a/desktop/src/services/appUpdater.ts b/desktop/src/services/appUpdater.ts index 088263efe..2ddcef704 100644 --- a/desktop/src/services/appUpdater.ts +++ b/desktop/src/services/appUpdater.ts @@ -1,9 +1,12 @@ -import { app, BrowserWindow } from 'electron'; -import { autoUpdater } from 'electron-updater'; -import log from 'electron-log'; -import { setIsAppQuitting, setIsUpdateAvailable } from '../main'; -import { compareVersions } from 'compare-versions'; -import { AppUpdateInfo, GetFeatureFlagResponse } from '../types'; +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 { clearMuteUpdateNotificationVersion, clearSkipAppVersion, @@ -11,11 +14,7 @@ import { getSkipAppVersion, setMuteUpdateNotificationVersion, setSkipAppVersion, -} from './userPreference'; -import fetch from 'node-fetch'; -import { logErrorSentry } from './sentry'; -import ElectronLog from 'electron-log'; -import { isPlatform } from '../utils/common/platform'; +} from "./userPreference"; const FIVE_MIN_IN_MICROSECOND = 5 * 60 * 1000; const ONE_DAY_IN_MICROSECOND = 1 * 24 * 60 * 60 * 1000; @@ -26,7 +25,7 @@ export function setupAutoUpdater(mainWindow: BrowserWindow) { checkForUpdateAndNotify(mainWindow); setInterval( () => checkForUpdateAndNotify(mainWindow), - ONE_DAY_IN_MICROSECOND + ONE_DAY_IN_MICROSECOND, ); } @@ -36,22 +35,22 @@ export function forceCheckForUpdateAndNotify(mainWindow: BrowserWindow) { clearMuteUpdateNotificationVersion(); checkForUpdateAndNotify(mainWindow); } catch (e) { - logErrorSentry(e, 'forceCheckForUpdateAndNotify failed'); + logErrorSentry(e, "forceCheckForUpdateAndNotify failed"); } } async function checkForUpdateAndNotify(mainWindow: BrowserWindow) { try { - log.debug('checkForUpdateAndNotify called'); + log.debug("checkForUpdateAndNotify called"); const updateCheckResult = await autoUpdater.checkForUpdates(); - log.debug('update version', updateCheckResult.updateInfo.version); + log.debug("update version", updateCheckResult.updateInfo.version); if ( compareVersions( updateCheckResult.updateInfo.version, - app.getVersion() + app.getVersion(), ) <= 0 ) { - log.debug('already at latest version'); + log.debug("already at latest version"); return; } const skipAppVersion = getSkipAppVersion(); @@ -60,28 +59,28 @@ async function checkForUpdateAndNotify(mainWindow: BrowserWindow) { updateCheckResult.updateInfo.version === skipAppVersion ) { log.info( - 'user chose to skip version ', - updateCheckResult.updateInfo.version + "user chose to skip version ", + updateCheckResult.updateInfo.version, ); return; } const desktopCutoffVersion = await getDesktopCutoffVersion(); if ( desktopCutoffVersion && - isPlatform('mac') && + isPlatform("mac") && compareVersions( updateCheckResult.updateInfo.version, - desktopCutoffVersion + desktopCutoffVersion, ) > 0 ) { - log.debug('auto update not possible due to key change'); + log.debug("auto update not possible due to key change"); showUpdateDialog(mainWindow, { autoUpdatable: false, version: updateCheckResult.updateInfo.version, }); } else { let timeout: NodeJS.Timeout; - log.debug('attempting auto update'); + log.debug("attempting auto update"); autoUpdater.downloadUpdate(); const muteUpdateNotificationVersion = getMuteUpdateNotificationVersion(); @@ -91,24 +90,24 @@ async function checkForUpdateAndNotify(mainWindow: BrowserWindow) { muteUpdateNotificationVersion ) { log.info( - 'user chose to mute update notification for version ', - updateCheckResult.updateInfo.version + "user chose to mute update notification for version ", + updateCheckResult.updateInfo.version, ); return; } - autoUpdater.on('update-downloaded', () => { + autoUpdater.on("update-downloaded", () => { timeout = setTimeout( () => showUpdateDialog(mainWindow, { autoUpdatable: true, version: updateCheckResult.updateInfo.version, }), - FIVE_MIN_IN_MICROSECOND + FIVE_MIN_IN_MICROSECOND, ); }); - autoUpdater.on('error', (error) => { + autoUpdater.on("error", (error) => { clearTimeout(timeout); - logErrorSentry(error, 'auto update failed'); + logErrorSentry(error, "auto update failed"); showUpdateDialog(mainWindow, { autoUpdatable: false, version: updateCheckResult.updateInfo.version, @@ -117,12 +116,12 @@ async function checkForUpdateAndNotify(mainWindow: BrowserWindow) { } setIsUpdateAvailable(true); } catch (e) { - logErrorSentry(e, 'checkForUpdateAndNotify failed'); + logErrorSentry(e, "checkForUpdateAndNotify failed"); } } export function updateAndRestart() { - ElectronLog.log('user quit the app'); + ElectronLog.log("user quit the app"); setIsAppQuitting(true); autoUpdater.quitAndInstall(); } @@ -142,18 +141,18 @@ export function muteUpdateNotification(version: string) { async function getDesktopCutoffVersion() { try { const featureFlags = ( - await fetch('https://static.ente.io/feature_flags.json') + await fetch("https://static.ente.io/feature_flags.json") ).json() as GetFeatureFlagResponse; return featureFlags.desktopCutoffVersion; } catch (e) { - logErrorSentry(e, 'failed to get feature flags'); + logErrorSentry(e, "failed to get feature flags"); return undefined; } } function showUpdateDialog( mainWindow: BrowserWindow, - updateInfo: AppUpdateInfo + updateInfo: AppUpdateInfo, ) { - mainWindow.webContents.send('show-update-dialog', updateInfo); + mainWindow.webContents.send("show-update-dialog", updateInfo); } diff --git a/desktop/src/services/autoLauncher.ts b/desktop/src/services/autoLauncher.ts index ab724f312..5cac556a9 100644 --- a/desktop/src/services/autoLauncher.ts +++ b/desktop/src/services/autoLauncher.ts @@ -1,18 +1,18 @@ -import { isPlatform } from '../utils/common/platform'; -import { AutoLauncherClient } from '../types/autoLauncher'; -import linuxAndWinAutoLauncher from './autoLauncherClients/linuxAndWinAutoLauncher'; -import macAutoLauncher from './autoLauncherClients/macAutoLauncher'; +import { AutoLauncherClient } from "../types/autoLauncher"; +import { isPlatform } from "../utils/common/platform"; +import linuxAndWinAutoLauncher from "./autoLauncherClients/linuxAndWinAutoLauncher"; +import macAutoLauncher from "./autoLauncherClients/macAutoLauncher"; class AutoLauncher { private client: AutoLauncherClient; async init() { - if (isPlatform('linux') || isPlatform('windows')) { + if (isPlatform("linux") || isPlatform("windows")) { this.client = linuxAndWinAutoLauncher; } else { this.client = macAutoLauncher; } // migrate old auto launch settings for windows from mac auto launcher to linux and windows auto launcher - if (isPlatform('windows') && (await macAutoLauncher.isEnabled())) { + if (isPlatform("windows") && (await macAutoLauncher.isEnabled())) { await macAutoLauncher.toggleAutoLaunch(); await linuxAndWinAutoLauncher.toggleAutoLaunch(); } diff --git a/desktop/src/services/autoLauncherClients/linuxAndWinAutoLauncher.ts b/desktop/src/services/autoLauncherClients/linuxAndWinAutoLauncher.ts index 240a3bf70..132f8d1f5 100644 --- a/desktop/src/services/autoLauncherClients/linuxAndWinAutoLauncher.ts +++ b/desktop/src/services/autoLauncherClients/linuxAndWinAutoLauncher.ts @@ -1,14 +1,14 @@ -import AutoLaunch from 'auto-launch'; -import { AutoLauncherClient } from '../../types/autoLauncher'; -import { app } from 'electron'; +import AutoLaunch from "auto-launch"; +import { app } from "electron"; +import { AutoLauncherClient } from "../../types/autoLauncher"; -const LAUNCHED_AS_HIDDEN_FLAG = 'hidden'; +const LAUNCHED_AS_HIDDEN_FLAG = "hidden"; class LinuxAndWinAutoLauncher implements AutoLauncherClient { private instance: AutoLaunch; constructor() { const autoLauncher = new AutoLaunch({ - name: 'ente', + name: "ente", isHidden: true, }); this.instance = autoLauncher; diff --git a/desktop/src/services/autoLauncherClients/macAutoLauncher.ts b/desktop/src/services/autoLauncherClients/macAutoLauncher.ts index 387861435..fcdc7bd81 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 { app } from "electron"; +import { AutoLauncherClient } from "../../types/autoLauncher"; class MacAutoLauncher implements AutoLauncherClient { async isEnabled() { diff --git a/desktop/src/services/chokidar.ts b/desktop/src/services/chokidar.ts index 4fea900d9..f0d217d09 100644 --- a/desktop/src/services/chokidar.ts +++ b/desktop/src/services/chokidar.ts @@ -1,7 +1,7 @@ -import chokidar from 'chokidar'; -import { BrowserWindow } from 'electron'; -import { logError } from '../services/logging'; -import { getWatchMappings } from '../api/watch'; +import chokidar from "chokidar"; +import { BrowserWindow } from "electron"; +import { getWatchMappings } from "../api/watch"; +import { logError } from "../services/logging"; export function initWatcher(mainWindow: BrowserWindow) { const mappings = getWatchMappings(); @@ -13,20 +13,20 @@ export function initWatcher(mainWindow: BrowserWindow) { awaitWriteFinish: true, }); watcher - .on('add', (path) => { - mainWindow.webContents.send('watch-add', path); + .on("add", (path) => { + mainWindow.webContents.send("watch-add", path); }) - .on('change', (path) => { - mainWindow.webContents.send('watch-change', path); + .on("change", (path) => { + mainWindow.webContents.send("watch-change", path); }) - .on('unlink', (path) => { - mainWindow.webContents.send('watch-unlink', path); + .on("unlink", (path) => { + mainWindow.webContents.send("watch-unlink", path); }) - .on('unlinkDir', (path) => { - mainWindow.webContents.send('watch-unlink-dir', path); + .on("unlinkDir", (path) => { + mainWindow.webContents.send("watch-unlink-dir", path); }) - .on('error', (error) => { - logError(error, 'error while watching files'); + .on("error", (error) => { + logError(error, "error while watching files"); }); return watcher; diff --git a/desktop/src/services/clipService.ts b/desktop/src/services/clipService.ts index 2924beabf..4a808d7a4 100644 --- a/desktop/src/services/clipService.ts +++ b/desktop/src/services/clipService.ts @@ -1,59 +1,59 @@ -import * as log from 'electron-log'; -import util from 'util'; -import { logErrorSentry } from './sentry'; -import { isDev } from '../utils/common'; -import { app } from 'electron'; -import path from 'path'; -import { existsSync } from 'fs'; -import fs from 'fs/promises'; -const shellescape = require('any-shell-escape'); -const execAsync = util.promisify(require('child_process').exec); -import fetch from 'node-fetch'; -import { writeNodeStream } from './fs'; -import { getPlatform } from '../utils/common/platform'; -import { CustomErrors } from '../constants/errors'; -const jpeg = require('jpeg-js'); +import { app } from "electron"; +import * as log from "electron-log"; +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 { CustomErrors } from "../constants/errors"; +import { Model } from "../types"; +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); +const jpeg = require("jpeg-js"); -const CLIP_MODEL_PATH_PLACEHOLDER = 'CLIP_MODEL'; -const GGMLCLIP_PATH_PLACEHOLDER = 'GGML_PATH'; -const INPUT_PATH_PLACEHOLDER = 'INPUT'; +const CLIP_MODEL_PATH_PLACEHOLDER = "CLIP_MODEL"; +const GGMLCLIP_PATH_PLACEHOLDER = "GGML_PATH"; +const INPUT_PATH_PLACEHOLDER = "INPUT"; const IMAGE_EMBEDDING_EXTRACT_CMD: string[] = [ GGMLCLIP_PATH_PLACEHOLDER, - '-mv', + "-mv", CLIP_MODEL_PATH_PLACEHOLDER, - '--image', + "--image", INPUT_PATH_PLACEHOLDER, ]; const TEXT_EMBEDDING_EXTRACT_CMD: string[] = [ GGMLCLIP_PATH_PLACEHOLDER, - '-mt', + "-mt", CLIP_MODEL_PATH_PLACEHOLDER, - '--text', + "--text", INPUT_PATH_PLACEHOLDER, ]; -const ort = require('onnxruntime-node'); -import Tokenizer from '../utils/clip-bpe-ts/mod'; -import { readFile } from 'promise-fs'; -import { Model } from '../types'; +const ort = require("onnxruntime-node"); const TEXT_MODEL_DOWNLOAD_URL = { - ggml: 'https://models.ente.io/clip-vit-base-patch32_ggml-text-model-f16.gguf', - onnx: 'https://models.ente.io/clip-text-vit-32-uint8.onnx', + ggml: "https://models.ente.io/clip-vit-base-patch32_ggml-text-model-f16.gguf", + onnx: "https://models.ente.io/clip-text-vit-32-uint8.onnx", }; const IMAGE_MODEL_DOWNLOAD_URL = { - ggml: 'https://models.ente.io/clip-vit-base-patch32_ggml-vision-model-f16.gguf', - onnx: 'https://models.ente.io/clip-image-vit-32-float32.onnx', + ggml: "https://models.ente.io/clip-vit-base-patch32_ggml-vision-model-f16.gguf", + onnx: "https://models.ente.io/clip-image-vit-32-float32.onnx", }; const TEXT_MODEL_NAME = { - ggml: 'clip-vit-base-patch32_ggml-text-model-f16.gguf', - onnx: 'clip-text-vit-32-uint8.onnx', + ggml: "clip-vit-base-patch32_ggml-text-model-f16.gguf", + onnx: "clip-text-vit-32-uint8.onnx", }; const IMAGE_MODEL_NAME = { - ggml: 'clip-vit-base-patch32_ggml-vision-model-f16.gguf', - onnx: 'clip-image-vit-32-float32.onnx', + ggml: "clip-vit-base-patch32_ggml-vision-model-f16.gguf", + onnx: "clip-image-vit-32-float32.onnx", }; const IMAGE_MODEL_SIZE_IN_BYTES = { @@ -65,14 +65,14 @@ const TEXT_MODEL_SIZE_IN_BYTES = { onnx: 64173509, // 61.2 MB }; -const MODEL_SAVE_FOLDER = 'models'; +const MODEL_SAVE_FOLDER = "models"; function getModelSavePath(modelName: string) { let userDataDir: string; if (isDev) { - userDataDir = '.'; + userDataDir = "."; } else { - userDataDir = app.getPath('userData'); + userDataDir = app.getPath("userData"); } return path.join(userDataDir, MODEL_SAVE_FOLDER, modelName); } @@ -81,41 +81,41 @@ 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'); + log.info("creating model save dir"); await fs.mkdir(saveDir, { recursive: true }); } - log.info('downloading clip model'); + log.info("downloading clip model"); const resp = await fetch(url); await writeNodeStream(saveLocation, resp.body); - log.info('clip model downloaded'); + log.info("clip model downloaded"); } let imageModelDownloadInProgress: Promise = null; -export async function getClipImageModelPath(type: 'ggml' | 'onnx') { +export async function getClipImageModelPath(type: "ggml" | "onnx") { try { const modelSavePath = getModelSavePath(IMAGE_MODEL_NAME[type]); if (imageModelDownloadInProgress) { - log.info('waiting for image model download to finish'); + log.info("waiting for image model download to finish"); await imageModelDownloadInProgress; } else { if (!existsSync(modelSavePath)) { - log.info('clip image model not found, downloading'); + log.info("clip image model not found, downloading"); imageModelDownloadInProgress = downloadModel( modelSavePath, - IMAGE_MODEL_DOWNLOAD_URL[type] + IMAGE_MODEL_DOWNLOAD_URL[type], ); await imageModelDownloadInProgress; } else { 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, - IMAGE_MODEL_DOWNLOAD_URL[type] + IMAGE_MODEL_DOWNLOAD_URL[type], ); await imageModelDownloadInProgress; } @@ -129,13 +129,13 @@ export async function getClipImageModelPath(type: 'ggml' | 'onnx') { let textModelDownloadInProgress: boolean = false; -export async function getClipTextModelPath(type: 'ggml' | 'onnx') { +export async function getClipTextModelPath(type: "ggml" | "onnx") { const modelSavePath = getModelSavePath(TEXT_MODEL_NAME[type]); if (textModelDownloadInProgress) { throw Error(CustomErrors.MODEL_DOWNLOAD_PENDING); } else { if (!existsSync(modelSavePath)) { - log.info('clip text model not found, downloading'); + log.info("clip text model not found, downloading"); textModelDownloadInProgress = true; downloadModel(modelSavePath, TEXT_MODEL_DOWNLOAD_URL[type]) .catch(() => { @@ -149,8 +149,8 @@ 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]) @@ -169,7 +169,7 @@ export async function getClipTextModelPath(type: 'ggml' | 'onnx') { function getGGMLClipPath() { return isDev - ? path.join('./build', `ggmlclip-${getPlatform()}`) + ? path.join("./build", `ggmlclip-${getPlatform()}`) : path.join(process.resourcesPath, `ggmlclip-${getPlatform()}`); } @@ -185,7 +185,7 @@ let onnxImageSessionPromise: Promise = null; async function getOnnxImageSession() { if (!onnxImageSessionPromise) { onnxImageSessionPromise = (async () => { - const clipModelPath = await getClipImageModelPath('onnx'); + const clipModelPath = await getClipImageModelPath("onnx"); return createOnnxSession(clipModelPath); })(); } @@ -196,7 +196,7 @@ let onnxTextSession: any = null; async function getOnnxTextSession() { if (!onnxTextSession) { - const clipModelPath = await getClipTextModelPath('onnx'); + const clipModelPath = await getClipTextModelPath("onnx"); onnxTextSession = await createOnnxSession(clipModelPath); } return onnxTextSession; @@ -212,7 +212,7 @@ function getTokenizer() { export async function computeImageEmbedding( model: Model, - inputFilePath: string + inputFilePath: string, ): Promise { if (!existsSync(inputFilePath)) { throw Error(CustomErrors.INVALID_FILE_PATH); @@ -227,10 +227,10 @@ export async function computeImageEmbedding( } export async function computeGGMLImageEmbedding( - inputFilePath: string + inputFilePath: string, ): Promise { try { - const clipModelPath = await getClipImageModelPath('ggml'); + const clipModelPath = await getClipImageModelPath("ggml"); const ggmlclipPath = getGGMLClipPath(); const cmd = IMAGE_EMBEDDING_EXTRACT_CMD.map((cmdPart) => { if (cmdPart === GGMLCLIP_PATH_PLACEHOLDER) { @@ -245,51 +245,51 @@ export async function computeGGMLImageEmbedding( }); const escapedCmd = shellescape(cmd); - log.info('running clip command', escapedCmd); + log.info("running clip command", escapedCmd); const startTime = Date.now(); const { stdout } = await execAsync(escapedCmd); - log.info('clip command execution time ', Date.now() - startTime); + log.info("clip command execution time ", Date.now() - startTime); // parse stdout and return embedding // get the last line of stdout - const lines = stdout.split('\n'); + const lines = stdout.split("\n"); const lastLine = lines[lines.length - 1]; const embedding = JSON.parse(lastLine); const embeddingArray = new Float32Array(embedding); return embeddingArray; } catch (err) { - logErrorSentry(err, 'Error in computeGGMLImageEmbedding'); + logErrorSentry(err, "Error in computeGGMLImageEmbedding"); throw err; } } export async function computeONNXImageEmbedding( - inputFilePath: string + inputFilePath: string, ): Promise { try { const imageSession = await getOnnxImageSession(); const t1 = Date.now(); const rgbData = await getRGBData(inputFilePath); const feeds = { - input: new ort.Tensor('float32', rgbData, [1, 3, 224, 224]), + input: new ort.Tensor("float32", rgbData, [1, 3, 224, 224]), }; const t2 = Date.now(); const results = await imageSession.run(feeds); log.info( `onnx image embedding time: ${Date.now() - t1} ms (prep:${ t2 - t1 - } ms, extraction: ${Date.now() - t2} ms)` + } ms, extraction: ${Date.now() - t2} ms)`, ); - const imageEmbedding = results['output'].data; // Float32Array + const imageEmbedding = results["output"].data; // Float32Array return normalizeEmbedding(imageEmbedding); } catch (err) { - logErrorSentry(err, 'Error in computeONNXImageEmbedding'); + logErrorSentry(err, "Error in computeONNXImageEmbedding"); throw err; } } export async function computeTextEmbedding( model: Model, - text: string + text: string, ): Promise { if (model === Model.GGML_CLIP) { return await computeGGMLTextEmbedding(text); @@ -299,10 +299,10 @@ export async function computeTextEmbedding( } export async function computeGGMLTextEmbedding( - text: string + text: string, ): Promise { try { - const clipModelPath = await getClipTextModelPath('ggml'); + const clipModelPath = await getClipTextModelPath("ggml"); const ggmlclipPath = getGGMLClipPath(); const cmd = TEXT_EMBEDDING_EXTRACT_CMD.map((cmdPart) => { if (cmdPart === GGMLCLIP_PATH_PLACEHOLDER) { @@ -317,13 +317,13 @@ export async function computeGGMLTextEmbedding( }); const escapedCmd = shellescape(cmd); - log.info('running clip command', escapedCmd); + log.info("running clip command", escapedCmd); const startTime = Date.now(); const { stdout } = await execAsync(escapedCmd); - log.info('clip command execution time ', Date.now() - startTime); + log.info("clip command execution time ", Date.now() - startTime); // parse stdout and return embedding // get the last line of stdout - const lines = stdout.split('\n'); + const lines = stdout.split("\n"); const lastLine = lines[lines.length - 1]; const embedding = JSON.parse(lastLine); const embeddingArray = new Float32Array(embedding); @@ -332,14 +332,14 @@ export async function computeGGMLTextEmbedding( if (err.message === CustomErrors.MODEL_DOWNLOAD_PENDING) { log.info(CustomErrors.MODEL_DOWNLOAD_PENDING); } else { - logErrorSentry(err, 'Error in computeGGMLTextEmbedding'); + logErrorSentry(err, "Error in computeGGMLTextEmbedding"); } throw err; } } export async function computeONNXTextEmbedding( - text: string + text: string, ): Promise { try { const imageSession = await getOnnxTextSession(); @@ -347,22 +347,22 @@ export async function computeONNXTextEmbedding( const tokenizer = getTokenizer(); const tokenizedText = Int32Array.from(tokenizer.encodeForCLIP(text)); const feeds = { - input: new ort.Tensor('int32', tokenizedText, [1, 77]), + input: new ort.Tensor("int32", tokenizedText, [1, 77]), }; const t2 = Date.now(); const results = await imageSession.run(feeds); log.info( `onnx text embedding time: ${Date.now() - t1} ms (prep:${ t2 - t1 - } ms, extraction: ${Date.now() - t2} ms)` + } ms, extraction: ${Date.now() - t2} ms)`, ); - const textEmbedding = results['output'].data; // Float32Array + const textEmbedding = results["output"].data; // Float32Array return normalizeEmbedding(textEmbedding); } catch (err) { if (err.message === CustomErrors.MODEL_DOWNLOAD_PENDING) { log.info(CustomErrors.MODEL_DOWNLOAD_PENDING); } else { - logErrorSentry(err, 'Error in computeONNXTextEmbedding'); + logErrorSentry(err, "Error in computeONNXTextEmbedding"); } throw err; } @@ -377,7 +377,7 @@ async function getRGBData(inputFilePath: string) { formatAsRGBA: false, }); } catch (err) { - logErrorSentry(err, 'JPEG decode error'); + logErrorSentry(err, "JPEG decode error"); throw err; } @@ -444,10 +444,10 @@ async function getRGBData(inputFilePath: string) { export const computeClipMatchScore = async ( imageEmbedding: Float32Array, - textEmbedding: Float32Array + textEmbedding: Float32Array, ) => { if (imageEmbedding.length !== textEmbedding.length) { - throw Error('imageEmbedding and textEmbedding length mismatch'); + throw Error("imageEmbedding and textEmbedding length mismatch"); } let score = 0; for (let index = 0; index < imageEmbedding.length; index++) { diff --git a/desktop/src/services/diskCache.ts b/desktop/src/services/diskCache.ts index 09be221f9..6861bba9f 100644 --- a/desktop/src/services/diskCache.ts +++ b/desktop/src/services/diskCache.ts @@ -1,17 +1,17 @@ -import DiskLRUService from '../services/diskLRU'; -import crypto from 'crypto'; -import { existsSync, unlink, rename, stat } from 'promise-fs'; -import path from 'path'; -import { LimitedCache } from '../types/cache'; -import { logError } from './logging'; -import { getFileStream, writeStream } from './fs'; +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 + private cacheLimit = DEFAULT_CACHE_LIMIT, ) {} async put(cacheKey: string, response: Response): Promise { @@ -19,13 +19,13 @@ export class DiskCache implements LimitedCache { await writeStream(cachePath, response.body); DiskLRUService.enforceCacheSizeLimit( this.cacheBucketDir, - this.cacheLimit + this.cacheLimit, ); } async match( cacheKey: string, - { sizeInBytes }: { sizeInBytes?: number } = {} + { sizeInBytes }: { sizeInBytes?: number } = {}, ): Promise { const cachePath = path.join(this.cacheBucketDir, cacheKey); if (existsSync(cachePath)) { @@ -33,11 +33,11 @@ export class DiskCache implements LimitedCache { if (sizeInBytes && fileStats.size !== sizeInBytes) { logError( Error(), - 'Cache key exists but size does not match. Deleting cache key.' + "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'); + if (e.code === "ENOENT") return; + logError(e, "Failed to delete cache key"); }); return undefined; } @@ -47,18 +47,18 @@ export class DiskCache implements LimitedCache { // add fallback for old cache keys const oldCachePath = getOldAssetCachePath( this.cacheBucketDir, - cacheKey + 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.' + "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'); + if (e.code === "ENOENT") return; + logError(e, "Failed to delete cache key"); }); return undefined; } @@ -83,9 +83,9 @@ export class DiskCache implements LimitedCache { function getOldAssetCachePath(cacheDir: string, cacheKey: string) { // hashing the key to prevent illegal filenames const cacheKeyHash = crypto - .createHash('sha256') + .createHash("sha256") .update(cacheKey) - .digest('hex'); + .digest("hex"); return path.join(cacheDir, cacheKeyHash); } @@ -93,6 +93,6 @@ 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'); + 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 index 56f0e9593..44b05c099 100644 --- a/desktop/src/services/diskLRU.ts +++ b/desktop/src/services/diskLRU.ts @@ -1,8 +1,7 @@ -import path from 'path'; -import { readdir, stat, unlink } from 'promise-fs'; -import getFolderSize from 'get-folder-size'; -import { utimes, close, open } from 'promise-fs'; -import { logError } from '../services/logging'; +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; @@ -18,11 +17,11 @@ class DiskLRUService { const time = new Date(); await utimes(path, time, time); } catch (err) { - logError(err, 'utimes method touch failed'); + logError(err, "utimes method touch failed"); try { - await close(await open(path, 'w')); + await close(await open(path, "w")); } catch (e) { - logError(e, 'open-close method touch failed'); + logError(e, "open-close method touch failed"); } // log and ignore } @@ -58,10 +57,10 @@ class DiskLRUService { } catch (e) { // ENOENT: File not found // which can be ignored as we are trying to delete the file anyway - if (e.code !== 'ENOENT') { + if (e.code !== "ENOENT") { logError( e, - 'Failed to evict least recently used' + "Failed to evict least recently used", ); } // ignoring the error, as it would get retried on the next run @@ -72,15 +71,15 @@ class DiskLRUService { }); }); } catch (e) { - logError(e, 'evictLeastRecentlyUsed failed'); + logError(e, "evictLeastRecentlyUsed failed"); } } private async findLeastRecentlyUsed( dir: string, - result?: LeastRecentlyUsedResult + result?: LeastRecentlyUsedResult, ): Promise { - result = result || { atime: new Date(), path: '' }; + result = result || { atime: new Date(), path: "" }; const files = await readdir(dir); for (const file of files) { diff --git a/desktop/src/services/ffmpeg.ts b/desktop/src/services/ffmpeg.ts index 6276aa309..e0a915790 100644 --- a/desktop/src/services/ffmpeg.ts +++ b/desktop/src/services/ffmpeg.ts @@ -1,30 +1,30 @@ -import pathToFfmpeg from 'ffmpeg-static'; -const shellescape = require('any-shell-escape'); -import util from 'util'; -import log from 'electron-log'; -import { readFile, rmSync, writeFile } from 'promise-fs'; -import { logErrorSentry } from './sentry'; -import { generateTempFilePath, getTempDirPath } from '../utils/temp'; -import { existsSync } from 'fs'; -import { promiseWithTimeout } from '../utils/common'; +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 { generateTempFilePath, getTempDirPath } from "../utils/temp"; +import { logErrorSentry } from "./sentry"; +const shellescape = require("any-shell-escape"); -const execAsync = util.promisify(require('child_process').exec); +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'; +const INPUT_PATH_PLACEHOLDER = "INPUT"; +const FFMPEG_PLACEHOLDER = "FFMPEG"; +const OUTPUT_PATH_PLACEHOLDER = "OUTPUT"; function getFFmpegStaticPath() { - return pathToFfmpeg.replace('app.asar', 'app.asar.unpacked'); + return pathToFfmpeg.replace("app.asar", "app.asar.unpacked"); } export async function runFFmpegCmd( cmd: string[], inputFilePath: string, outputFileName: string, - dontTimeout = false + dontTimeout = false, ) { let tempOutputFilePath: string; try { @@ -42,36 +42,36 @@ export async function runFFmpegCmd( } }); const escapedCmd = shellescape(cmd); - log.info('running ffmpeg command', escapedCmd); + log.info("running ffmpeg command", escapedCmd); const startTime = Date.now(); if (dontTimeout) { await execAsync(escapedCmd); } else { await promiseWithTimeout( execAsync(escapedCmd), - FFMPEG_EXECUTION_WAIT_TIME + FFMPEG_EXECUTION_WAIT_TIME, ); } if (!existsSync(tempOutputFilePath)) { - throw new Error('ffmpeg output file not found'); + throw new Error("ffmpeg output file not found"); } log.info( - 'ffmpeg command execution time ', + "ffmpeg command execution time ", escapedCmd, Date.now() - startTime, - 'ms' + "ms", ); const outputFile = await readFile(tempOutputFilePath); return new Uint8Array(outputFile); } catch (e) { - logErrorSentry(e, 'ffmpeg run command error'); + logErrorSentry(e, "ffmpeg run command error"); throw e; } finally { try { rmSync(tempOutputFilePath, { force: true }); } catch (e) { - logErrorSentry(e, 'failed to remove tempOutputFile'); + logErrorSentry(e, "failed to remove tempOutputFile"); } } } @@ -86,8 +86,8 @@ 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' + Error("not a temp file"), + "tried to delete a non temp file", ); } rmSync(tempFilePath, { force: true }); diff --git a/desktop/src/services/fs.ts b/desktop/src/services/fs.ts index 4829ce7bf..06d413c1f 100644 --- a/desktop/src/services/fs.ts +++ b/desktop/src/services/fs.ts @@ -1,11 +1,12 @@ -import { FILE_STREAM_CHUNK_SIZE } from '../config'; -import path from 'path'; -import * as fs from 'promise-fs'; -import { ElectronFile } from '../types'; -import StreamZip from 'node-stream-zip'; -import { Readable } from 'stream'; -import { logError } from './logging'; -import { existsSync } from 'fs'; +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"; + +const FILE_STREAM_CHUNK_SIZE: number = 4 * 1024 * 1024; // https://stackoverflow.com/a/63111390 export const getDirFilePaths = async (dirPath: string) => { @@ -25,7 +26,7 @@ export const getDirFilePaths = async (dirPath: string) => { }; export const getFileStream = async (filePath: string) => { - const file = await fs.open(filePath, 'r'); + const file = await fs.open(filePath, "r"); let offset = 0; const readableStream = new ReadableStream({ async pull(controller) { @@ -37,7 +38,7 @@ export const getFileStream = async (filePath: string) => { buff, 0, FILE_STREAM_CHUNK_SIZE, - offset + offset, )) as unknown as number; offset += bytesRead; if (bytesRead === 0) { @@ -66,20 +67,20 @@ export async function getElectronFile(filePath: string): Promise { lastModified: fileStats.mtime.valueOf(), stream: async () => { if (!existsSync(filePath)) { - throw new Error('electronFile does not exist'); + throw new Error("electronFile does not exist"); } return await getFileStream(filePath); }, blob: async () => { if (!existsSync(filePath)) { - throw new Error('electronFile does not exist'); + throw new Error("electronFile does not exist"); } const blob = await fs.readFile(filePath); return new Blob([new Uint8Array(blob)]); }, arrayBuffer: async () => { if (!existsSync(filePath)) { - throw new Error('electronFile does not exist'); + throw new Error("electronFile does not exist"); } const blob = await fs.readFile(filePath); return new Uint8Array(blob); @@ -102,7 +103,7 @@ export const getValidPaths = (paths: string[]) => { export const getZipFileStream = async ( zip: StreamZip.StreamZipAsync, - filePath: string + filePath: string, ) => { const stream = await zip.stream(filePath); const done = { @@ -113,7 +114,7 @@ export const getZipFileStream = async ( }; let resolveObj: (value?: any) => void = null; let rejectObj: (reason?: any) => void = null; - stream.on('readable', () => { + stream.on("readable", () => { try { if (resolveObj) { inProgress.current = true; @@ -128,7 +129,7 @@ export const getZipFileStream = async ( rejectObj(e); } }); - stream.on('end', () => { + stream.on("end", () => { try { done.current = true; if (resolveObj && !inProgress.current) { @@ -139,7 +140,7 @@ export const getZipFileStream = async ( rejectObj(e); } }); - stream.on('error', (e) => { + stream.on("error", (e) => { try { done.current = true; if (rejectObj) { @@ -175,7 +176,7 @@ export const getZipFileStream = async ( controller.close(); } } catch (e) { - logError(e, 'readableStream pull failed'); + logError(e, "readableStream pull failed"); controller.close(); } }, @@ -190,20 +191,20 @@ export async function isFolder(dirPath: string) { } catch (e) { let err = e; // if code is defined, it's an error from fs.stat - if (typeof e.code !== 'undefined') { + if (typeof e.code !== "undefined") { // ENOENT means the file does not exist - if (e.code === 'ENOENT') { + if (e.code === "ENOENT") { return false; } err = Error(`fs error code: ${e.code}`); } - logError(err, 'isFolder failed'); + logError(err, "isFolder failed"); return false; } } export const convertBrowserStreamToNode = ( - fileStream: ReadableStream + fileStream: ReadableStream, ) => { const reader = fileStream.getReader(); const rs = new Readable(); @@ -219,7 +220,7 @@ export const convertBrowserStreamToNode = ( return; } } catch (e) { - rs.emit('error', e); + rs.emit("error", e); } }; @@ -228,19 +229,19 @@ export const convertBrowserStreamToNode = ( export async function writeNodeStream( filePath: string, - fileStream: NodeJS.ReadableStream + fileStream: NodeJS.ReadableStream, ) { const writeable = fs.createWriteStream(filePath); - fileStream.on('error', (error) => { + 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) => { + writeable.on("finish", resolve); + writeable.on("error", async (e) => { if (existsSync(filePath)) { await fs.unlink(filePath); } @@ -251,7 +252,7 @@ export async function writeNodeStream( export async function writeStream( filePath: string, - fileStream: ReadableStream + fileStream: ReadableStream, ) { const readable = convertBrowserStreamToNode(fileStream); await writeNodeStream(filePath, readable); @@ -259,20 +260,20 @@ export async function writeStream( export async function readTextFile(filePath: string) { if (!existsSync(filePath)) { - throw new Error('File does not exist'); + throw new Error("File does not exist"); } - return await fs.readFile(filePath, 'utf-8'); + return await fs.readFile(filePath, "utf-8"); } export async function moveFile( sourcePath: string, - destinationPath: string + destinationPath: string, ): Promise { if (!existsSync(sourcePath)) { - throw new Error('File does not exist'); + throw new Error("File does not exist"); } if (existsSync(destinationPath)) { - throw new Error('Destination file already exists'); + throw new Error("Destination file already exists"); } // check if destination folder exists const destinationFolder = path.dirname(destinationPath); @@ -287,19 +288,19 @@ export async function deleteFolder(folderPath: string): Promise { return; } if (!fs.statSync(folderPath).isDirectory()) { - throw new Error('Path is not a folder'); + 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'); + throw new Error("Folder is not empty"); } await fs.rmdir(folderPath); } export async function rename(oldPath: string, newPath: string) { if (!existsSync(oldPath)) { - throw new Error('Path does not exist'); + throw new Error("Path does not exist"); } await fs.rename(oldPath, newPath); } @@ -309,7 +310,7 @@ export function deleteFile(filePath: string): void { return; } if (!fs.statSync(filePath).isFile()) { - throw new Error('Path is not a file'); + throw new Error("Path is not a file"); } fs.rmSync(filePath); } diff --git a/desktop/src/services/imageProcessor.ts b/desktop/src/services/imageProcessor.ts index e74768680..cb6c7416d 100644 --- a/desktop/src/services/imageProcessor.ts +++ b/desktop/src/services/imageProcessor.ts @@ -1,140 +1,140 @@ -import util from 'util'; -import { exec } from 'child_process'; +import { exec } from "child_process"; +import util from "util"; -import { existsSync, rmSync } from 'fs'; -import { readFile, writeFile } from 'promise-fs'; -import { generateTempFilePath } from '../utils/temp'; -import { logErrorSentry } from './sentry'; -import { isPlatform } from '../utils/common/platform'; -import { isDev } from '../utils/common'; -import path from 'path'; -import log from 'electron-log'; -import { CustomErrors } from '../constants/errors'; -const shellescape = require('any-shell-escape'); +import log from "electron-log"; +import { existsSync, rmSync } from "fs"; +import path from "path"; +import { readFile, writeFile } from "promise-fs"; +import { CustomErrors } from "../constants/errors"; +import { isDev } from "../utils/common"; +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); -const IMAGE_MAGICK_PLACEHOLDER = 'IMAGE_MAGICK'; -const MAX_DIMENSION_PLACEHOLDER = 'MAX_DIMENSION'; -const SAMPLE_SIZE_PLACEHOLDER = 'SAMPLE_SIZE'; -const INPUT_PATH_PLACEHOLDER = 'INPUT'; -const OUTPUT_PATH_PLACEHOLDER = 'OUTPUT'; -const QUALITY_PLACEHOLDER = 'QUALITY'; +const IMAGE_MAGICK_PLACEHOLDER = "IMAGE_MAGICK"; +const MAX_DIMENSION_PLACEHOLDER = "MAX_DIMENSION"; +const SAMPLE_SIZE_PLACEHOLDER = "SAMPLE_SIZE"; +const INPUT_PATH_PLACEHOLDER = "INPUT"; +const OUTPUT_PATH_PLACEHOLDER = "OUTPUT"; +const QUALITY_PLACEHOLDER = "QUALITY"; const MAX_QUALITY = 70; const MIN_QUALITY = 50; const SIPS_HEIC_CONVERT_COMMAND_TEMPLATE = [ - 'sips', - '-s', - 'format', - 'jpeg', + "sips", + "-s", + "format", + "jpeg", INPUT_PATH_PLACEHOLDER, - '--out', + "--out", OUTPUT_PATH_PLACEHOLDER, ]; const SIPS_THUMBNAIL_GENERATE_COMMAND_TEMPLATE = [ - 'sips', - '-s', - 'format', - 'jpeg', - '-s', - 'formatOptions', + "sips", + "-s", + "format", + "jpeg", + "-s", + "formatOptions", QUALITY_PLACEHOLDER, - '-Z', + "-Z", MAX_DIMENSION_PLACEHOLDER, INPUT_PATH_PLACEHOLDER, - '--out', + "--out", OUTPUT_PATH_PLACEHOLDER, ]; const IMAGEMAGICK_HEIC_CONVERT_COMMAND_TEMPLATE = [ IMAGE_MAGICK_PLACEHOLDER, INPUT_PATH_PLACEHOLDER, - '-quality', - '100%', + "-quality", + "100%", OUTPUT_PATH_PLACEHOLDER, ]; const IMAGE_MAGICK_THUMBNAIL_GENERATE_COMMAND_TEMPLATE = [ IMAGE_MAGICK_PLACEHOLDER, - '-auto-orient', - '-define', + "-auto-orient", + "-define", `jpeg:size=${SAMPLE_SIZE_PLACEHOLDER}x${SAMPLE_SIZE_PLACEHOLDER}`, INPUT_PATH_PLACEHOLDER, - '-thumbnail', + "-thumbnail", `${MAX_DIMENSION_PLACEHOLDER}x${MAX_DIMENSION_PLACEHOLDER}>`, - '-unsharp', - '0x.5', - '-quality', + "-unsharp", + "0x.5", + "-quality", QUALITY_PLACEHOLDER, OUTPUT_PATH_PLACEHOLDER, ]; function getImageMagickStaticPath() { return isDev - ? 'build/image-magick' - : path.join(process.resourcesPath, 'image-magick'); + ? "resources/image-magick" + : path.join(process.resourcesPath, "image-magick"); } export async function convertToJPEG( fileData: Uint8Array, - filename: string + filename: string, ): Promise { let tempInputFilePath: string; let tempOutputFilePath: string; try { tempInputFilePath = await generateTempFilePath(filename); - tempOutputFilePath = await generateTempFilePath('output.jpeg'); + tempOutputFilePath = await generateTempFilePath("output.jpeg"); await writeFile(tempInputFilePath, fileData); await runConvertCommand(tempInputFilePath, tempOutputFilePath); if (!existsSync(tempOutputFilePath)) { - throw new Error('heic convert output file not found'); + throw new Error("heic convert output file not found"); } const convertedFileData = new Uint8Array( - await readFile(tempOutputFilePath) + await readFile(tempOutputFilePath), ); return convertedFileData; } catch (e) { - logErrorSentry(e, 'failed to convert heic'); + logErrorSentry(e, "failed to convert heic"); throw e; } finally { try { rmSync(tempInputFilePath, { force: true }); } catch (e) { - logErrorSentry(e, 'failed to remove tempInputFile'); + logErrorSentry(e, "failed to remove tempInputFile"); } try { rmSync(tempOutputFilePath, { force: true }); } catch (e) { - logErrorSentry(e, 'failed to remove tempOutputFile'); + logErrorSentry(e, "failed to remove tempOutputFile"); } } } async function runConvertCommand( tempInputFilePath: string, - tempOutputFilePath: string + tempOutputFilePath: string, ) { const convertCmd = constructConvertCommand( tempInputFilePath, - tempOutputFilePath + tempOutputFilePath, ); const escapedCmd = shellescape(convertCmd); - log.info('running convert command: ' + escapedCmd); + log.info("running convert command: " + escapedCmd); await asyncExec(escapedCmd); } function constructConvertCommand( tempInputFilePath: string, - tempOutputFilePath: string + tempOutputFilePath: string, ) { let convertCmd: string[]; - if (isPlatform('mac')) { + if (isPlatform("mac")) { convertCmd = SIPS_HEIC_CONVERT_COMMAND_TEMPLATE.map((cmdPart) => { if (cmdPart === INPUT_PATH_PLACEHOLDER) { return tempInputFilePath; @@ -144,7 +144,7 @@ function constructConvertCommand( } return cmdPart; }); - } else if (isPlatform('linux')) { + } else if (isPlatform("linux")) { convertCmd = IMAGEMAGICK_HEIC_CONVERT_COMMAND_TEMPLATE.map( (cmdPart) => { if (cmdPart === IMAGE_MAGICK_PLACEHOLDER) { @@ -157,7 +157,7 @@ function constructConvertCommand( return tempOutputFilePath; } return cmdPart; - } + }, ); } else { throw Error(CustomErrors.INVALID_OS(process.platform)); @@ -168,36 +168,36 @@ function constructConvertCommand( export async function generateImageThumbnail( inputFilePath: string, width: number, - maxSize: number + maxSize: number, ): Promise { let tempOutputFilePath: string; let quality = MAX_QUALITY; try { - tempOutputFilePath = await generateTempFilePath('thumb.jpeg'); + tempOutputFilePath = await generateTempFilePath("thumb.jpeg"); let thumbnail: Uint8Array; do { await runThumbnailGenerationCommand( inputFilePath, tempOutputFilePath, width, - quality + quality, ); if (!existsSync(tempOutputFilePath)) { - throw new Error('output thumbnail file not found'); + throw new Error("output thumbnail file not found"); } thumbnail = new Uint8Array(await readFile(tempOutputFilePath)); quality -= 10; } while (thumbnail.length > maxSize && quality > MIN_QUALITY); return thumbnail; } catch (e) { - logErrorSentry(e, 'generate image thumbnail failed'); + logErrorSentry(e, "generate image thumbnail failed"); throw e; } finally { try { rmSync(tempOutputFilePath, { force: true }); } catch (e) { - logErrorSentry(e, 'failed to remove tempOutputFile'); + logErrorSentry(e, "failed to remove tempOutputFile"); } } } @@ -206,27 +206,27 @@ async function runThumbnailGenerationCommand( inputFilePath: string, tempOutputFilePath: string, maxDimension: number, - quality: number + quality: number, ) { const thumbnailGenerationCmd: string[] = constructThumbnailGenerationCommand( inputFilePath, tempOutputFilePath, maxDimension, - quality + quality, ); const escapedCmd = shellescape(thumbnailGenerationCmd); - log.info('running thumbnail generation command: ' + escapedCmd); + log.info("running thumbnail generation command: " + escapedCmd); await asyncExec(escapedCmd); } function constructThumbnailGenerationCommand( inputFilePath: string, tempOutputFilePath: string, maxDimension: number, - quality: number + quality: number, ) { let thumbnailGenerationCmd: string[]; - if (isPlatform('mac')) { + if (isPlatform("mac")) { thumbnailGenerationCmd = SIPS_THUMBNAIL_GENERATE_COMMAND_TEMPLATE.map( (cmdPart) => { if (cmdPart === INPUT_PATH_PLACEHOLDER) { @@ -242,9 +242,9 @@ function constructThumbnailGenerationCommand( return quality.toString(); } return cmdPart; - } + }, ); - } else if (isPlatform('linux')) { + } else if (isPlatform("linux")) { thumbnailGenerationCmd = IMAGE_MAGICK_THUMBNAIL_GENERATE_COMMAND_TEMPLATE.map((cmdPart) => { if (cmdPart === IMAGE_MAGICK_PLACEHOLDER) { @@ -259,13 +259,13 @@ function constructThumbnailGenerationCommand( if (cmdPart.includes(SAMPLE_SIZE_PLACEHOLDER)) { return cmdPart.replaceAll( SAMPLE_SIZE_PLACEHOLDER, - (2 * maxDimension).toString() + (2 * maxDimension).toString(), ); } if (cmdPart.includes(MAX_DIMENSION_PLACEHOLDER)) { return cmdPart.replaceAll( MAX_DIMENSION_PLACEHOLDER, - maxDimension.toString() + maxDimension.toString(), ); } if (cmdPart === QUALITY_PLACEHOLDER) { diff --git a/desktop/src/services/logging.ts b/desktop/src/services/logging.ts index 61406b888..bcbacd9f5 100644 --- a/desktop/src/services/logging.ts +++ b/desktop/src/services/logging.ts @@ -1,22 +1,14 @@ -import log from 'electron-log'; -import { ipcRenderer } from 'electron'; +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'); + ipcRenderer.invoke("open-log-dir"); } export function logError(error: Error, message: string, info?: string): void { - ipcRenderer.invoke('log-error', error, message, info); -} - -export function getSentryUserID(): Promise { - return ipcRenderer.invoke('get-sentry-id'); -} - -export function updateOptOutOfCrashReports(optOut: boolean) { - return ipcRenderer.invoke('update-opt-out-crash-reports', optOut); + ipcRenderer.invoke("log-error", error, message, info); } diff --git a/desktop/src/services/sentry.ts b/desktop/src/services/sentry.ts index 66c0294da..4c5573152 100644 --- a/desktop/src/services/sentry.ts +++ b/desktop/src/services/sentry.ts @@ -1,68 +1,18 @@ -import * as Sentry from '@sentry/electron/dist/main'; -import { makeID } from '../utils/logging'; -import { keysStore } from '../stores/keys.store'; -import { SENTRY_DSN, RELEASE_VERSION } from '../config'; -import { isDev } from '../utils/common'; -import { logToDisk } from './logging'; -import { hasOptedOutOfCrashReports } from '../main'; - -const ENV_DEVELOPMENT = 'development'; - -const isDEVSentryENV = () => - process.env.NEXT_PUBLIC_SENTRY_ENV === ENV_DEVELOPMENT; - -export function initSentry(): void { - Sentry.init({ - dsn: SENTRY_DSN, - release: RELEASE_VERSION, - environment: isDev ? 'development' : 'production', - }); - Sentry.setUser({ id: getSentryUserID() }); -} +import { isDev } from "../utils/common"; +import { logToDisk } from "./logging"; +/** Deprecated, but no alternative yet */ export function logErrorSentry( error: any, msg: string, - info?: Record + info?: Record, ) { - const err = errorWithContext(error, msg); logToDisk( `error: ${error?.name} ${error?.message} ${ error?.stack - } msg: ${msg} info: ${JSON.stringify(info)}` + } msg: ${msg} info: ${JSON.stringify(info)}`, ); - if (isDEVSentryENV()) { + if (isDev) { console.log(error, { msg, info }); } - if (hasOptedOutOfCrashReports()) { - return; - } - Sentry.captureException(err, { - level: Sentry.Severity.Info, - user: { id: getSentryUserID() }, - contexts: { - ...(info && { - info: info, - }), - rootCause: { message: error?.message }, - }, - }); -} - -function errorWithContext(originalError: Error, context: string) { - const errorWithContext = new Error(context); - errorWithContext.stack = - errorWithContext.stack.split('\n').slice(2, 4).join('\n') + - '\n' + - originalError.stack; - return errorWithContext; -} - -export function getSentryUserID() { - let anonymizeUserID = keysStore.get('AnonymizeUserID')?.id; - if (!anonymizeUserID) { - anonymizeUserID = makeID(6); - keysStore.set('AnonymizeUserID', { id: anonymizeUserID }); - } - return anonymizeUserID; } diff --git a/desktop/src/services/upload.ts b/desktop/src/services/upload.ts index 7d5144121..38a628c25 100644 --- a/desktop/src/services/upload.ts +++ b/desktop/src/services/upload.ts @@ -1,13 +1,13 @@ -import StreamZip from 'node-stream-zip'; -import path from 'path'; -import { uploadStatusStore } from '../stores/upload.store'; -import { FILE_PATH_TYPE, FILE_PATH_KEYS, ElectronFile } from '../types'; -import { getValidPaths, getZipFileStream } from './fs'; +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 { getValidPaths, getZipFileStream } from "./fs"; export const getSavedFilePaths = (type: FILE_PATH_TYPE) => { const paths = getValidPaths( - uploadStatusStore.get(FILE_PATH_KEYS[type]) as string[] + uploadStatusStore.get(FILE_PATH_KEYS[type]) as string[], ) ?? []; setToUploadFiles(type, paths); @@ -17,7 +17,7 @@ export const getSavedFilePaths = (type: FILE_PATH_TYPE) => { export async function getZipEntryAsElectronFile( zipName: string, zip: StreamZip.StreamZipAsync, - entry: StreamZip.ZipEntry + entry: StreamZip.ZipEntry, ): Promise { return { path: path @@ -52,9 +52,9 @@ export const setToUploadFiles = (type: FILE_PATH_TYPE, filePaths: string[]) => { export const setToUploadCollection = (collectionName: string) => { if (collectionName) { - uploadStatusStore.set('collectionName', collectionName); + uploadStatusStore.set("collectionName", collectionName); } else { - uploadStatusStore.delete('collectionName'); + uploadStatusStore.delete("collectionName"); } }; @@ -62,14 +62,14 @@ export const getElectronFilesFromGoogleZip = async (filePath: string) => { const zip = new StreamZip.async({ file: filePath, }); - const zipName = path.basename(filePath, '.zip'); + const zipName = path.basename(filePath, ".zip"); const entries = await zip.entries(); const files: ElectronFile[] = []; for (const entry of Object.values(entries)) { const basename = path.basename(entry.name); - if (entry.isFile && basename.length > 0 && basename[0] !== '.') { + if (entry.isFile && basename.length > 0 && basename[0] !== ".") { files.push(await getZipEntryAsElectronFile(zipName, zip, entry)); } } diff --git a/desktop/src/services/userPreference.ts b/desktop/src/services/userPreference.ts index e2d3626dd..e3c6db290 100644 --- a/desktop/src/services/userPreference.ts +++ b/desktop/src/services/userPreference.ts @@ -1,49 +1,41 @@ -import { userPreferencesStore } from '../stores/userPreferences.store'; +import { userPreferencesStore } from "../stores/userPreferences.store"; export function getHideDockIconPreference() { - return userPreferencesStore.get('hideDockIcon'); + return userPreferencesStore.get("hideDockIcon"); } export function setHideDockIconPreference(shouldHideDockIcon: boolean) { - userPreferencesStore.set('hideDockIcon', shouldHideDockIcon); + userPreferencesStore.set("hideDockIcon", shouldHideDockIcon); } export function getSkipAppVersion() { - return userPreferencesStore.get('skipAppVersion'); + return userPreferencesStore.get("skipAppVersion"); } export function setSkipAppVersion(version: string) { - userPreferencesStore.set('skipAppVersion', version); + userPreferencesStore.set("skipAppVersion", version); } export function getMuteUpdateNotificationVersion() { - return userPreferencesStore.get('muteUpdateNotificationVersion'); + return userPreferencesStore.get("muteUpdateNotificationVersion"); } export function setMuteUpdateNotificationVersion(version: string) { - userPreferencesStore.set('muteUpdateNotificationVersion', version); -} - -export function getOptOutOfCrashReports() { - return userPreferencesStore.get('optOutOfCrashReports') ?? false; -} - -export function setOptOutOfCrashReports(optOut: boolean) { - userPreferencesStore.set('optOutOfCrashReports', optOut); + userPreferencesStore.set("muteUpdateNotificationVersion", version); } export function clearSkipAppVersion() { - userPreferencesStore.delete('skipAppVersion'); + userPreferencesStore.delete("skipAppVersion"); } export function clearMuteUpdateNotificationVersion() { - userPreferencesStore.delete('muteUpdateNotificationVersion'); + userPreferencesStore.delete("muteUpdateNotificationVersion"); } export function setCustomCacheDirectory(directory: string) { - userPreferencesStore.set('customCacheDirectory', directory); + userPreferencesStore.set("customCacheDirectory", directory); } export function getCustomCacheDirectory(): string { - return userPreferencesStore.get('customCacheDirectory'); + return userPreferencesStore.get("customCacheDirectory"); } diff --git a/desktop/src/services/watch.ts b/desktop/src/services/watch.ts index 47157470f..8b7746964 100644 --- a/desktop/src/services/watch.ts +++ b/desktop/src/services/watch.ts @@ -1,11 +1,11 @@ -import { WatchStoreType } from '../types'; -import { watchStore } from '../stores/watch.store'; +import { watchStore } from "../stores/watch.store"; +import { WatchStoreType } from "../types"; export function getWatchMappings() { - const mappings = watchStore.get('mappings') ?? []; + const mappings = watchStore.get("mappings") ?? []; return mappings; } -export function setWatchMappings(watchMappings: WatchStoreType['mappings']) { - watchStore.set('mappings', watchMappings); +export 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 f3b324f2d..943bdb1ca 100644 --- a/desktop/src/stores/keys.store.ts +++ b/desktop/src/stores/keys.store.ts @@ -1,18 +1,18 @@ -import Store, { Schema } from 'electron-store'; -import { KeysStoreType } from '../types'; +import Store, { Schema } from "electron-store"; +import { KeysStoreType } from "../types"; const keysStoreSchema: Schema = { AnonymizeUserID: { - type: 'object', + type: "object", properties: { id: { - type: 'string', + type: "string", }, }, }, }; export const keysStore = new Store({ - name: 'keys', + name: "keys", schema: keysStoreSchema, }); diff --git a/desktop/src/stores/safeStorage.store.ts b/desktop/src/stores/safeStorage.store.ts index 6aad19153..7822e32e3 100644 --- a/desktop/src/stores/safeStorage.store.ts +++ b/desktop/src/stores/safeStorage.store.ts @@ -1,13 +1,13 @@ -import Store, { Schema } from 'electron-store'; -import { SafeStorageStoreType } from '../types'; +import Store, { Schema } from "electron-store"; +import { SafeStorageStoreType } from "../types"; const safeStorageSchema: Schema = { encryptionKey: { - type: 'string', + type: "string", }, }; export const safeStorageStore = new Store({ - name: 'safeStorage', + name: "safeStorage", schema: safeStorageSchema, }); diff --git a/desktop/src/stores/upload.store.ts b/desktop/src/stores/upload.store.ts index 33f1ec33f..b918fd283 100644 --- a/desktop/src/stores/upload.store.ts +++ b/desktop/src/stores/upload.store.ts @@ -1,25 +1,25 @@ -import Store, { Schema } from 'electron-store'; -import { UploadStoreType } from '../types'; +import Store, { Schema } from "electron-store"; +import { UploadStoreType } from "../types"; const uploadStoreSchema: Schema = { filePaths: { - type: 'array', + type: "array", items: { - type: 'string', + type: "string", }, }, zipPaths: { - type: 'array', + type: "array", items: { - type: 'string', + type: "string", }, }, collectionName: { - type: 'string', + type: "string", }, }; export const uploadStatusStore = new Store({ - name: 'upload-status', + name: "upload-status", schema: uploadStoreSchema, }); diff --git a/desktop/src/stores/userPreferences.store.ts b/desktop/src/stores/userPreferences.store.ts index aa8ba53d9..e6fec425a 100644 --- a/desktop/src/stores/userPreferences.store.ts +++ b/desktop/src/stores/userPreferences.store.ts @@ -1,25 +1,22 @@ -import Store, { Schema } from 'electron-store'; -import { UserPreferencesType } from '../types'; +import Store, { Schema } from "electron-store"; +import { UserPreferencesType } from "../types"; const userPreferencesSchema: Schema = { hideDockIcon: { - type: 'boolean', + type: "boolean", }, skipAppVersion: { - type: 'string', + type: "string", }, muteUpdateNotificationVersion: { - type: 'string', - }, - optOutOfCrashReports: { - type: 'boolean', + type: "string", }, customCacheDirectory: { - type: 'string', + type: "string", }, }; export const userPreferencesStore = new Store({ - name: 'userPreferences', + name: "userPreferences", schema: userPreferencesSchema, }); diff --git a/desktop/src/stores/watch.store.ts b/desktop/src/stores/watch.store.ts index a60e9060e..6489ba3e8 100644 --- a/desktop/src/stores/watch.store.ts +++ b/desktop/src/stores/watch.store.ts @@ -1,39 +1,39 @@ -import Store, { Schema } from 'electron-store'; -import { WatchStoreType } from '../types'; +import Store, { Schema } from "electron-store"; +import { WatchStoreType } from "../types"; const watchStoreSchema: Schema = { mappings: { - type: 'array', + type: "array", items: { - type: 'object', + type: "object", properties: { rootFolderName: { - type: 'string', + type: "string", }, uploadStrategy: { - type: 'number', + type: "number", }, folderPath: { - type: 'string', + type: "string", }, syncedFiles: { - type: 'array', + type: "array", items: { - type: 'object', + type: "object", properties: { path: { - type: 'string', + type: "string", }, id: { - type: 'number', + type: "number", }, }, }, }, ignoredFiles: { - type: 'array', + type: "array", items: { - type: 'string', + type: "string", }, }, }, @@ -42,6 +42,6 @@ const watchStoreSchema: Schema = { }; export const watchStore = new Store({ - name: 'watch-status', + name: "watch-status", schema: watchStoreSchema, }); diff --git a/desktop/src/types/cache.ts b/desktop/src/types/cache.ts index fde7dc038..112716eea 100644 --- a/desktop/src/types/cache.ts +++ b/desktop/src/types/cache.ts @@ -1,7 +1,7 @@ export interface LimitedCache { match: ( key: string, - options?: { sizeInBytes?: number } + 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/index.ts index a40ad5477..208983826 100644 --- a/desktop/src/types/index.ts +++ b/desktop/src/types/index.ts @@ -39,15 +39,15 @@ export interface WatchStoreType { } export enum FILE_PATH_TYPE { - FILES = 'files', - ZIPS = 'zips', + FILES = "files", + ZIPS = "zips", } export const FILE_PATH_KEYS: { [k in FILE_PATH_TYPE]: keyof UploadStoreType; } = { - [FILE_PATH_TYPE.ZIPS]: 'zipPaths', - [FILE_PATH_TYPE.FILES]: 'filePaths', + [FILE_PATH_TYPE.ZIPS]: "zipPaths", + [FILE_PATH_TYPE.FILES]: "filePaths", }; export interface SafeStorageStoreType { @@ -58,7 +58,6 @@ export interface UserPreferencesType { hideDockIcon: boolean; skipAppVersion: string; muteUpdateNotificationVersion: string; - optOutOfCrashReports: boolean; customCacheDirectory: string; } @@ -72,6 +71,6 @@ export interface GetFeatureFlagResponse { } export enum Model { - GGML_CLIP = 'ggml-clip', - ONNX_CLIP = 'onnx-clip', + GGML_CLIP = "ggml-clip", + ONNX_CLIP = "onnx-clip", } diff --git a/desktop/src/utils/clip-bpe-ts/README.md b/desktop/src/utils/clip-bpe-ts/README.md index dd171eb42..ee052eb41 100644 --- a/desktop/src/utils/clip-bpe-ts/README.md +++ b/desktop/src/utils/clip-bpe-ts/README.md @@ -1,20 +1,21 @@ # CLIP Byte Pair Encoding JavaScript Port + A JavaScript port of [OpenAI's CLIP byte-pair-encoding tokenizer](https://github.com/openai/CLIP/blob/3bee28119e6b28e75b82b811b87b56935314e6a5/clip/simple_tokenizer.py). ```js import Tokenizer from "https://deno.land/x/clip_bpe@v0.0.6/mod.js"; let t = new Tokenizer(); -t.encode("hello") // [3306] -t.encode("magnificent") // [10724] -t.encode("magnificently") // [9725, 2922] -t.decode(t.encode("HELLO")) // "hello " -t.decode(t.encode("abc123")) // "abc 1 2 3 " -t.decode(st.encode("let's see here")) // "let 's see here " -t.encode("hello world!") // [3306, 1002, 256] +t.encode("hello"); // [3306] +t.encode("magnificent"); // [10724] +t.encode("magnificently"); // [9725, 2922] +t.decode(t.encode("HELLO")); // "hello " +t.decode(t.encode("abc123")); // "abc 1 2 3 " +t.decode(st.encode("let's see here")); // "let 's see here " +t.encode("hello world!"); // [3306, 1002, 256] // to encode for CLIP (trims to maximum of 77 tokens and adds start and end token, and pads with zeros if less than 77 tokens): -t.encodeForCLIP("hello world!") // [49406,3306,1002,256,49407,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] +t.encodeForCLIP("hello world!"); // [49406,3306,1002,256,49407,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] ``` This encoder/decoder behaves differently to the the GPT-2/3 tokenizer (JavaScript version of that [here](https://github.com/latitudegames/GPT-3-Encoder)). For example, it doesn't preserve capital letters, as shown above. diff --git a/desktop/src/utils/clip-bpe-ts/mod.ts b/desktop/src/utils/clip-bpe-ts/mod.ts index 698b86c8d..6cdf246f7 100644 --- a/desktop/src/utils/clip-bpe-ts/mod.ts +++ b/desktop/src/utils/clip-bpe-ts/mod.ts @@ -1,5 +1,5 @@ -import * as htmlEntities from 'html-entities'; -import bpeVocabData from './bpe_simple_vocab_16e6'; +import * as htmlEntities from "html-entities"; +import bpeVocabData from "./bpe_simple_vocab_16e6"; // import ftfy from "https://deno.land/x/ftfy_pyodide@v0.1.1/mod.js"; function ord(c: string) { @@ -25,9 +25,9 @@ function range(start: number, stop?: number, step: number = 1) { function bytesToUnicode() { const bs = [ - ...range(ord('!'), ord('~') + 1), - ...range(ord('¡'), ord('¬') + 1), - ...range(ord('®'), ord('ÿ') + 1), + ...range(ord("!"), ord("~") + 1), + ...range(ord("¡"), ord("¬") + 1), + ...range(ord("®"), ord("ÿ") + 1), ]; const cs = bs.slice(0); let n = 0; @@ -59,7 +59,7 @@ function basicClean(text: string) { } function whitespaceClean(text: string) { - return text.replace(/\s+/g, ' ').trim(); + return text.replace(/\s+/g, " ").trim(); } export default class { @@ -75,285 +75,285 @@ export default class { constructor() { this.byteEncoder = bytesToUnicode(); this.byteDecoder = Object.fromEntries( - Object.entries(this.byteEncoder).map(([k, v]) => [v, Number(k)]) + Object.entries(this.byteEncoder).map(([k, v]) => [v, Number(k)]), ); - let merges = bpeVocabData.text.split('\n'); + let merges = bpeVocabData.text.split("\n"); merges = merges.slice(1, 49152 - 256 - 2 + 1); - const mergedMerges = merges.map((merge) => merge.split(' ')); + const mergedMerges = merges.map((merge) => merge.split(" ")); // There was a bug related to the ordering of Python's .values() output. I'm lazy do I've just copy-pasted the Python output: let vocab = [ - '!', + "!", '"', - '#', - '$', - '%', - '&', + "#", + "$", + "%", + "&", "'", - '(', - ')', - '*', - '+', - ',', - '-', - '.', - '/', - '0', - '1', - '2', - '3', - '4', - '5', - '6', - '7', - '8', - '9', - ':', - ';', - '<', - '=', - '>', - '?', - '@', - 'A', - 'B', - 'C', - 'D', - 'E', - 'F', - 'G', - 'H', - 'I', - 'J', - 'K', - 'L', - 'M', - 'N', - 'O', - 'P', - 'Q', - 'R', - 'S', - 'T', - 'U', - 'V', - 'W', - 'X', - 'Y', - 'Z', - '[', - '\\', - ']', - '^', - '_', - '`', - 'a', - 'b', - 'c', - 'd', - 'e', - 'f', - 'g', - 'h', - 'i', - 'j', - 'k', - 'l', - 'm', - 'n', - 'o', - 'p', - 'q', - 'r', - 's', - 't', - 'u', - 'v', - 'w', - 'x', - 'y', - 'z', - '{', - '|', - '}', - '~', - '¡', - '¢', - '£', - '¤', - '¥', - '¦', - '§', - '¨', - '©', - 'ª', - '«', - '¬', - '®', - '¯', - '°', - '±', - '²', - '³', - '´', - 'µ', - '¶', - '·', - '¸', - '¹', - 'º', - '»', - '¼', - '½', - '¾', - '¿', - 'À', - 'Á', - 'Â', - 'Ã', - 'Ä', - 'Å', - 'Æ', - 'Ç', - 'È', - 'É', - 'Ê', - 'Ë', - 'Ì', - 'Í', - 'Î', - 'Ï', - 'Ð', - 'Ñ', - 'Ò', - 'Ó', - 'Ô', - 'Õ', - 'Ö', - '×', - 'Ø', - 'Ù', - 'Ú', - 'Û', - 'Ü', - 'Ý', - 'Þ', - 'ß', - 'à', - 'á', - 'â', - 'ã', - 'ä', - 'å', - 'æ', - 'ç', - 'è', - 'é', - 'ê', - 'ë', - 'ì', - 'í', - 'î', - 'ï', - 'ð', - 'ñ', - 'ò', - 'ó', - 'ô', - 'õ', - 'ö', - '÷', - 'ø', - 'ù', - 'ú', - 'û', - 'ü', - 'ý', - 'þ', - 'ÿ', - 'Ā', - 'ā', - 'Ă', - 'ă', - 'Ą', - 'ą', - 'Ć', - 'ć', - 'Ĉ', - 'ĉ', - 'Ċ', - 'ċ', - 'Č', - 'č', - 'Ď', - 'ď', - 'Đ', - 'đ', - 'Ē', - 'ē', - 'Ĕ', - 'ĕ', - 'Ė', - 'ė', - 'Ę', - 'ę', - 'Ě', - 'ě', - 'Ĝ', - 'ĝ', - 'Ğ', - 'ğ', - 'Ġ', - 'ġ', - 'Ģ', - 'ģ', - 'Ĥ', - 'ĥ', - 'Ħ', - 'ħ', - 'Ĩ', - 'ĩ', - 'Ī', - 'ī', - 'Ĭ', - 'ĭ', - 'Į', - 'į', - 'İ', - 'ı', - 'IJ', - 'ij', - 'Ĵ', - 'ĵ', - 'Ķ', - 'ķ', - 'ĸ', - 'Ĺ', - 'ĺ', - 'Ļ', - 'ļ', - 'Ľ', - 'ľ', - 'Ŀ', - 'ŀ', - 'Ł', - 'ł', - 'Ń', + "(", + ")", + "*", + "+", + ",", + "-", + ".", + "/", + "0", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + ":", + ";", + "<", + "=", + ">", + "?", + "@", + "A", + "B", + "C", + "D", + "E", + "F", + "G", + "H", + "I", + "J", + "K", + "L", + "M", + "N", + "O", + "P", + "Q", + "R", + "S", + "T", + "U", + "V", + "W", + "X", + "Y", + "Z", + "[", + "\\", + "]", + "^", + "_", + "`", + "a", + "b", + "c", + "d", + "e", + "f", + "g", + "h", + "i", + "j", + "k", + "l", + "m", + "n", + "o", + "p", + "q", + "r", + "s", + "t", + "u", + "v", + "w", + "x", + "y", + "z", + "{", + "|", + "}", + "~", + "¡", + "¢", + "£", + "¤", + "¥", + "¦", + "§", + "¨", + "©", + "ª", + "«", + "¬", + "®", + "¯", + "°", + "±", + "²", + "³", + "´", + "µ", + "¶", + "·", + "¸", + "¹", + "º", + "»", + "¼", + "½", + "¾", + "¿", + "À", + "Á", + "Â", + "Ã", + "Ä", + "Å", + "Æ", + "Ç", + "È", + "É", + "Ê", + "Ë", + "Ì", + "Í", + "Î", + "Ï", + "Ð", + "Ñ", + "Ò", + "Ó", + "Ô", + "Õ", + "Ö", + "×", + "Ø", + "Ù", + "Ú", + "Û", + "Ü", + "Ý", + "Þ", + "ß", + "à", + "á", + "â", + "ã", + "ä", + "å", + "æ", + "ç", + "è", + "é", + "ê", + "ë", + "ì", + "í", + "î", + "ï", + "ð", + "ñ", + "ò", + "ó", + "ô", + "õ", + "ö", + "÷", + "ø", + "ù", + "ú", + "û", + "ü", + "ý", + "þ", + "ÿ", + "Ā", + "ā", + "Ă", + "ă", + "Ą", + "ą", + "Ć", + "ć", + "Ĉ", + "ĉ", + "Ċ", + "ċ", + "Č", + "č", + "Ď", + "ď", + "Đ", + "đ", + "Ē", + "ē", + "Ĕ", + "ĕ", + "Ė", + "ė", + "Ę", + "ę", + "Ě", + "ě", + "Ĝ", + "ĝ", + "Ğ", + "ğ", + "Ġ", + "ġ", + "Ģ", + "ģ", + "Ĥ", + "ĥ", + "Ħ", + "ħ", + "Ĩ", + "ĩ", + "Ī", + "ī", + "Ĭ", + "ĭ", + "Į", + "į", + "İ", + "ı", + "IJ", + "ij", + "Ĵ", + "ĵ", + "Ķ", + "ķ", + "ĸ", + "Ĺ", + "ĺ", + "Ļ", + "ļ", + "Ľ", + "ľ", + "Ŀ", + "ŀ", + "Ł", + "ł", + "Ń", ]; - vocab = [...vocab, ...vocab.map((v) => v + '')]; + vocab = [...vocab, ...vocab.map((v) => v + "")]; for (const merge of mergedMerges) { - vocab.push(merge.join('')); + vocab.push(merge.join("")); } - vocab.push('<|startoftext|>', '<|endoftext|>'); + vocab.push("<|startoftext|>", "<|endoftext|>"); this.encoder = Object.fromEntries(vocab.map((v, i) => [v, i])); this.decoder = Object.fromEntries( - Object.entries(this.encoder).map(([k, v]) => [v, k]) + Object.entries(this.encoder).map(([k, v]) => [v, k]), ); this.bpeRanks = Object.fromEntries( - mergedMerges.map((v, i) => [v.join('·😎·'), i]) + mergedMerges.map((v, i) => [v.join("·😎·"), i]), ); // ·😎· because js doesn't yet have tuples this.cache = { - '<|startoftext|>': '<|startoftext|>', - '<|endoftext|>': '<|endoftext|>', + "<|startoftext|>": "<|startoftext|>", + "<|endoftext|>": "<|endoftext|>", }; this.pat = /<\|startoftext\|>|<\|endoftext\|>|'s|'t|'re|'ve|'m|'ll|'d|[\p{L}]+|[\p{N}]|[^\s\p{L}\p{N}]+/giu; @@ -364,11 +364,11 @@ export default class { return this.cache[token]; } - let word = [...token.slice(0, -1), token.slice(-1) + '']; + let word = [...token.slice(0, -1), token.slice(-1) + ""]; let pairs = getPairs(word); if (pairs.length === 0) { - return token + ''; + return token + ""; } // eslint-disable-next-line no-constant-condition @@ -376,7 +376,7 @@ export default class { let bigram: [string, string] | null = null; let minRank = Infinity; for (const p of pairs) { - const r = this.bpeRanks[p.join('·😎·')]; + const r = this.bpeRanks[p.join("·😎·")]; if (r === undefined) continue; if (r < minRank) { minRank = r; @@ -421,7 +421,7 @@ export default class { pairs = getPairs(word); } } - const joinedWord = word.join(' '); + const joinedWord = word.join(" "); this.cache[token] = joinedWord; return joinedWord; } @@ -432,11 +432,11 @@ export default class { for (let token of [...text.matchAll(this.pat)].map((m) => m[0])) { token = [...token] .map((b) => this.byteEncoder[b.charCodeAt(0) as number]) - .join(''); + .join(""); bpeTokens.push( ...this.bpe(token) - .split(' ') - .map((bpeToken: string) => this.encoder[bpeToken]) + .split(" ") + .map((bpeToken: string) => this.encoder[bpeToken]), ); } return bpeTokens; @@ -455,12 +455,12 @@ export default class { decode(tokens: any[]) { let text = tokens .map((token: string | number) => this.decoder[token]) - .join(''); + .join(""); text = [...text] .map((c) => this.byteDecoder[c]) .map((v) => String.fromCharCode(v)) - .join('') - .replace(/<\/w>/g, ' '); + .join("") + .replace(/<\/w>/g, " "); return text; } } diff --git a/desktop/src/utils/common/index.ts b/desktop/src/utils/common/index.ts index 6813ce432..e970dfec4 100644 --- a/desktop/src/utils/common/index.ts +++ b/desktop/src/utils/common/index.ts @@ -1,10 +1,10 @@ -import { CustomErrors } from '../../constants/errors'; -import { app } from 'electron'; +import { app } from "electron"; +import { CustomErrors } from "../../constants/errors"; export const isDev = !app.isPackaged; export const promiseWithTimeout = async ( request: Promise, - timeout: number + timeout: number, ): Promise => { const timeoutRef: { current: NodeJS.Timeout; @@ -12,7 +12,7 @@ export const promiseWithTimeout = async ( const rejectOnTimeout = new Promise((_, reject) => { timeoutRef.current = setTimeout( () => reject(Error(CustomErrors.WAIT_TIME_EXCEEDED)), - timeout + timeout, ); }); const requestWithTimeOutCancellation = async () => { diff --git a/desktop/src/utils/common/platform.ts b/desktop/src/utils/common/platform.ts index 0326ba0ad..1c3bb4584 100644 --- a/desktop/src/utils/common/platform.ts +++ b/desktop/src/utils/common/platform.ts @@ -1,19 +1,19 @@ -export function isPlatform(platform: 'mac' | 'windows' | 'linux') { +export function isPlatform(platform: "mac" | "windows" | "linux") { return getPlatform() === platform; } -export function getPlatform(): 'mac' | 'windows' | 'linux' { +export function getPlatform(): "mac" | "windows" | "linux" { switch (process.platform) { - case 'aix': - case 'freebsd': - case 'linux': - case 'openbsd': - case 'android': - return 'linux'; - case 'darwin': - case 'sunos': - return 'mac'; - case 'win32': - return 'windows'; + case "aix": + case "freebsd": + case "linux": + case "openbsd": + case "android": + return "linux"; + case "darwin": + case "sunos": + return "mac"; + case "win32": + return "windows"; } } diff --git a/desktop/src/utils/cors.ts b/desktop/src/utils/cors.ts index a2da3b3f6..25f76211a 100644 --- a/desktop/src/utils/cors.ts +++ b/desktop/src/utils/cors.ts @@ -1,4 +1,4 @@ -import { BrowserWindow } from 'electron'; +import { BrowserWindow } from "electron"; function lowerCaseHeaders(responseHeaders: Record) { const headers: Record = {}; @@ -12,10 +12,10 @@ export function addAllowOriginHeader(mainWindow: BrowserWindow) { mainWindow.webContents.session.webRequest.onHeadersReceived( (details, callback) => { details.responseHeaders = lowerCaseHeaders(details.responseHeaders); - details.responseHeaders['access-control-allow-origin'] = ['*']; + details.responseHeaders["access-control-allow-origin"] = ["*"]; callback({ responseHeaders: details.responseHeaders, }); - } + }, ); } diff --git a/desktop/src/utils/createWindow.ts b/desktop/src/utils/createWindow.ts index c31fc0df5..c7d44e6c9 100644 --- a/desktop/src/utils/createWindow.ts +++ b/desktop/src/utils/createWindow.ts @@ -1,37 +1,36 @@ -import { app, BrowserWindow, nativeImage } from 'electron'; -import * as path from 'path'; -import { isDev } from './common'; -import { isAppQuitting } from '../main'; -import { PROD_HOST_URL } from '../config'; -import { isPlatform } from './common/platform'; -import { getHideDockIconPreference } from '../services/userPreference'; -import autoLauncher from '../services/autoLauncher'; -import ElectronLog from 'electron-log'; -import { logErrorSentry } from '../services/sentry'; +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"; export async function createWindow(): Promise { const appImgPath = isDev - ? 'build/window-icon.png' - : path.join(process.resourcesPath, 'window-icon.png'); + ? "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: { sandbox: false, - preload: path.join(__dirname, '../preload.js'), + preload: path.join(__dirname, "../preload.js"), contextIsolation: false, }, icon: appIcon, show: false, // don't show the main window on load, }); const wasAutoLaunched = await autoLauncher.wasAutoLaunched(); - ElectronLog.log('wasAutoLaunched', wasAutoLaunched); + ElectronLog.log("wasAutoLaunched", wasAutoLaunched); const splash = new BrowserWindow({ transparent: true, show: false, }); - if (isPlatform('mac') && wasAutoLaunched) { + if (isPlatform("mac") && wasAutoLaunched) { app.dock.hide(); } if (!wasAutoLaunched) { @@ -40,27 +39,27 @@ export async function createWindow(): Promise { } if (isDev) { - splash.loadFile(`../build/splash.html`); - mainWindow.loadURL(PROD_HOST_URL); + splash.loadFile(`../resources/splash.html`); + mainWindow.loadURL(rendererURL); // Open the DevTools. mainWindow.webContents.openDevTools(); } else { splash.loadURL( - `file://${path.join(process.resourcesPath, 'splash.html')}` + `file://${path.join(process.resourcesPath, "splash.html")}`, ); - mainWindow.loadURL(PROD_HOST_URL); + mainWindow.loadURL(rendererURL); } - mainWindow.webContents.on('did-fail-load', () => { + mainWindow.webContents.on("did-fail-load", () => { splash.close(); isDev - ? mainWindow.loadFile(`../../build/error.html`) + ? mainWindow.loadFile(`../resources/error.html`) : splash.loadURL( - `file://${path.join(process.resourcesPath, 'error.html')}` + `file://${path.join(process.resourcesPath, "error.html")}`, ); mainWindow.maximize(); mainWindow.show(); }); - mainWindow.once('ready-to-show', async () => { + mainWindow.once("ready-to-show", async () => { try { splash.destroy(); if (!wasAutoLaunched) { @@ -71,18 +70,18 @@ export async function createWindow(): Promise { // ignore } }); - mainWindow.webContents.on('render-process-gone', (event, details) => { + mainWindow.webContents.on("render-process-gone", (event, details) => { mainWindow.webContents.reload(); logErrorSentry( - Error('render-process-gone'), - 'webContents event render-process-gone', - { details } + Error("render-process-gone"), + "webContents event render-process-gone", + { details }, ); - ElectronLog.log('webContents event render-process-gone', details); + ElectronLog.log("webContents event render-process-gone", details); }); - mainWindow.webContents.on('unresponsive', () => { + mainWindow.webContents.on("unresponsive", () => { mainWindow.webContents.forcefullyCrashRenderer(); - ElectronLog.log('webContents event unresponsive'); + ElectronLog.log("webContents event unresponsive"); }); setTimeout(() => { @@ -96,21 +95,21 @@ export async function createWindow(): Promise { // ignore } }, 2000); - mainWindow.on('close', function (event) { + mainWindow.on("close", function (event) { if (!isAppQuitting()) { event.preventDefault(); mainWindow.hide(); } return false; }); - mainWindow.on('hide', () => { + mainWindow.on("hide", () => { const shouldHideDockIcon = getHideDockIconPreference(); - if (isPlatform('mac') && shouldHideDockIcon) { + if (isPlatform("mac") && shouldHideDockIcon) { app.dock.hide(); } }); - mainWindow.on('show', () => { - if (isPlatform('mac')) { + mainWindow.on("show", () => { + if (isPlatform("mac")) { app.dock.show(); } }); diff --git a/desktop/src/utils/error.ts b/desktop/src/utils/error.ts index 71904ea0f..1922045a2 100644 --- a/desktop/src/utils/error.ts +++ b/desktop/src/utils/error.ts @@ -1,15 +1,15 @@ -import { CustomErrors } from '../constants/errors'; +import { CustomErrors } from "../constants/errors"; export const isExecError = (err: any) => { - return err.message.includes('Command failed:'); + return err.message.includes("Command failed:"); }; export const parseExecError = (err: any) => { const errMessage = err.message; - if (errMessage.includes('Bad CPU type in executable')) { + if (errMessage.includes("Bad CPU type in executable")) { return CustomErrors.UNSUPPORTED_PLATFORM( process.platform, - process.arch + process.arch, ); } else { return errMessage; diff --git a/desktop/src/utils/events.ts b/desktop/src/utils/events.ts index 6ce259639..4c7ffe7d8 100644 --- a/desktop/src/utils/events.ts +++ b/desktop/src/utils/events.ts @@ -1,8 +1,8 @@ -import { BrowserWindow } from 'electron'; +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'); + mainWindow.on("focus", () => { + mainWindow.webContents.send("app-in-foreground"); }); } diff --git a/desktop/src/utils/ipcComms.ts b/desktop/src/utils/ipcComms.ts index b4c3a80c2..eec644aeb 100644 --- a/desktop/src/utils/ipcComms.ts +++ b/desktop/src/utils/ipcComms.ts @@ -1,87 +1,85 @@ +import chokidar from "chokidar"; import { + app, BrowserWindow, dialog, ipcMain, - Tray, Notification, safeStorage, - app, shell, -} from 'electron'; -import { createWindow } from './createWindow'; -import { getSentryUserID, logErrorSentry } from '../services/sentry'; -import chokidar from 'chokidar'; -import path from 'path'; -import { getDirFilePaths } from '../services/fs'; -import { - convertToJPEG, - generateImageThumbnail, -} from '../services/imageProcessor'; + Tray, +} from "electron"; +import path from "path"; import { getAppVersion, muteUpdateNotification, skipAppUpdate, updateAndRestart, -} from '../services/appUpdater'; -import { deleteTempFile, runFFmpegCmd } from '../services/ffmpeg'; -import { generateTempFilePath } from './temp'; -import { - getCustomCacheDirectory, - setCustomCacheDirectory, - setOptOutOfCrashReports, -} from '../services/userPreference'; -import { updateOptOutOfCrashReports } from '../main'; +} from "../services/appUpdater"; import { computeImageEmbedding, computeTextEmbedding, -} from '../services/clipService'; -import { getPlatform } from './common/platform'; +} 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 { + getCustomCacheDirectory, + setCustomCacheDirectory, +} from "../services/userPreference"; +import { getPlatform } from "./common/platform"; +import { createWindow } from "./createWindow"; +import { generateTempFilePath } from "./temp"; export default function setupIpcComs( tray: Tray, mainWindow: BrowserWindow, - watcher: chokidar.FSWatcher + watcher: chokidar.FSWatcher, ): void { - ipcMain.handle('select-dir', async () => { + ipcMain.handle("select-dir", async () => { const result = await dialog.showOpenDialog({ - properties: ['openDirectory'], + properties: ["openDirectory"], }); if (result.filePaths && result.filePaths.length > 0) { return result.filePaths[0]?.split(path.sep)?.join(path.posix.sep); } }); - ipcMain.on('send-notification', (_, args) => { + ipcMain.on("send-notification", (_, args) => { const notification = { - title: 'ente', + title: "ente", body: args, }; new Notification(notification).show(); }); - ipcMain.on('reload-window', async () => { + ipcMain.on("reload-window", async () => { const secondWindow = await createWindow(); mainWindow.destroy(); mainWindow = secondWindow; }); - ipcMain.handle('show-upload-files-dialog', async () => { + ipcMain.handle("show-upload-files-dialog", async () => { const files = await dialog.showOpenDialog({ - properties: ['openFile', 'multiSelections'], + properties: ["openFile", "multiSelections"], }); return files.filePaths; }); - ipcMain.handle('show-upload-zip-dialog', async () => { + ipcMain.handle("show-upload-zip-dialog", async () => { const files = await dialog.showOpenDialog({ - properties: ['openFile', 'multiSelections'], - filters: [{ name: 'Zip File', extensions: ['zip'] }], + properties: ["openFile", "multiSelections"], + filters: [{ name: "Zip File", extensions: ["zip"] }], }); return files.filePaths; }); - ipcMain.handle('show-upload-dirs-dialog', async () => { + ipcMain.handle("show-upload-dirs-dialog", async () => { const dir = await dialog.showOpenDialog({ - properties: ['openDirectory', 'multiSelections'], + properties: ["openDirectory", "multiSelections"], }); let files: string[] = []; @@ -92,27 +90,27 @@ export default function setupIpcComs( return files; }); - ipcMain.handle('add-watcher', async (_, args: { dir: string }) => { + ipcMain.handle("add-watcher", async (_, args: { dir: string }) => { watcher.add(args.dir); }); - ipcMain.handle('remove-watcher', async (_, args: { dir: string }) => { + ipcMain.handle("remove-watcher", async (_, args: { dir: string }) => { watcher.unwatch(args.dir); }); - ipcMain.handle('log-error', (_, err, msg, info?) => { + ipcMain.handle("log-error", (_, err, msg, info?) => { logErrorSentry(err, msg, info); }); - ipcMain.handle('safeStorage-encrypt', (_, message) => { + ipcMain.handle("safeStorage-encrypt", (_, message) => { return safeStorage.encryptString(message); }); - ipcMain.handle('safeStorage-decrypt', (_, message) => { + ipcMain.handle("safeStorage-decrypt", (_, message) => { return safeStorage.decryptString(message); }); - ipcMain.handle('get-path', (_, message) => { + ipcMain.handle("get-path", (_, message) => { // By default, these paths are at the following locations: // // * macOS: `~/Library/Application Support/ente` @@ -124,80 +122,73 @@ export default function setupIpcComs( return app.getPath(message); }); - ipcMain.handle('convert-to-jpeg', (_, fileData, filename) => { + ipcMain.handle("convert-to-jpeg", (_, fileData, filename) => { return convertToJPEG(fileData, filename); }); - ipcMain.handle('open-log-dir', () => { - shell.openPath(app.getPath('logs')); + ipcMain.handle("open-log-dir", () => { + shell.openPath(app.getPath("logs")); }); - ipcMain.handle('open-dir', (_, dirPath) => { + ipcMain.handle("open-dir", (_, dirPath) => { shell.openPath(path.normalize(dirPath)); }); - ipcMain.on('update-and-restart', () => { + ipcMain.on("update-and-restart", () => { updateAndRestart(); }); - ipcMain.on('skip-app-update', (_, version) => { + ipcMain.on("skip-app-update", (_, version) => { skipAppUpdate(version); }); - ipcMain.on('mute-update-notification', (_, version) => { + ipcMain.on("mute-update-notification", (_, version) => { muteUpdateNotification(version); }); - ipcMain.handle('get-sentry-id', () => { - return getSentryUserID(); - }); - ipcMain.handle('get-app-version', () => { + ipcMain.handle("get-app-version", () => { return getAppVersion(); }); ipcMain.handle( - 'run-ffmpeg-cmd', + "run-ffmpeg-cmd", (_, cmd, inputFilePath, outputFileName, dontTimeout) => { return runFFmpegCmd( cmd, inputFilePath, outputFileName, - dontTimeout + dontTimeout, ); - } + }, ); - ipcMain.handle('get-temp-file-path', (_, formatSuffix) => { + ipcMain.handle("get-temp-file-path", (_, formatSuffix) => { return generateTempFilePath(formatSuffix); }); - ipcMain.handle('remove-temp-file', (_, tempFilePath: string) => { + ipcMain.handle("remove-temp-file", (_, tempFilePath: string) => { return deleteTempFile(tempFilePath); }); ipcMain.handle( - 'generate-image-thumbnail', + "generate-image-thumbnail", (_, fileData, maxDimension, maxSize) => { return generateImageThumbnail(fileData, maxDimension, maxSize); - } + }, ); - ipcMain.handle('update-opt-out-crash-reports', (_, optOut) => { - setOptOutOfCrashReports(optOut); - updateOptOutOfCrashReports(optOut); - }); - ipcMain.handle('compute-image-embedding', (_, model, inputFilePath) => { + ipcMain.handle("compute-image-embedding", (_, model, inputFilePath) => { return computeImageEmbedding(model, inputFilePath); }); - ipcMain.handle('compute-text-embedding', (_, model, text) => { + ipcMain.handle("compute-text-embedding", (_, model, text) => { return computeTextEmbedding(model, text); }); - ipcMain.handle('get-platform', () => { + ipcMain.handle("get-platform", () => { return getPlatform(); }); - ipcMain.handle('set-custom-cache-directory', (_, directory: string) => { + ipcMain.handle("set-custom-cache-directory", (_, directory: string) => { setCustomCacheDirectory(directory); }); - ipcMain.handle('get-custom-cache-directory', async () => { + ipcMain.handle("get-custom-cache-directory", async () => { return getCustomCacheDirectory(); }); } diff --git a/desktop/src/utils/logging.ts b/desktop/src/utils/logging.ts index 5924224eb..351a1aef8 100644 --- a/desktop/src/utils/logging.ts +++ b/desktop/src/utils/logging.ts @@ -1,38 +1,24 @@ -import log from 'electron-log'; -import { LOG_FILENAME, MAX_LOG_SIZE } from '../config'; +import log from "electron-log"; export function setupLogging(isDev?: boolean) { - log.transports.file.fileName = LOG_FILENAME; - log.transports.file.maxSize = MAX_LOG_SIZE; + 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}'; -} - -export function makeID(length: number) { - let result = ''; - const characters = - 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; - const charactersLength = characters.length; - for (let i = 0; i < length; i++) { - result += characters.charAt( - Math.floor(Math.random() * charactersLength) - ); - } - return result; + "[{y}-{m}-{d}T{h}:{i}:{s}{z}] [{level}]{scope} {text}"; } export function convertBytesToHumanReadable( bytes: number, - precision = 2 + precision = 2, ): string { if (bytes === 0 || isNaN(bytes)) { - return '0 MB'; + return "0 MB"; } const i = Math.floor(Math.log(bytes) / Math.log(1024)); - const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; - return (bytes / Math.pow(1024, i)).toFixed(precision) + ' ' + sizes[i]; + const sizes = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]; + return (bytes / Math.pow(1024, i)).toFixed(precision) + " " + sizes[i]; } diff --git a/desktop/src/utils/main.ts b/desktop/src/utils/main.ts index 80aa654d7..569752326 100644 --- a/desktop/src/utils/main.ts +++ b/desktop/src/utils/main.ts @@ -1,19 +1,17 @@ -import { PROD_HOST_URL, RENDERER_OUTPUT_DIR } from '../config'; -import { nativeImage, Tray, app, BrowserWindow, Menu } from 'electron'; -import electronReload from 'electron-reload'; -import serveNextAt from 'next-electron-server'; -import path from 'path'; -import { existsSync } from 'promise-fs'; -import { isDev } from './common'; -import { buildContextMenu, buildMenuBar } from './menu'; -import autoLauncher from '../services/autoLauncher'; -import { getHideDockIconPreference } from '../services/userPreference'; -import { setupAutoUpdater } from '../services/appUpdater'; -import ElectronLog from 'electron-log'; -import os from 'os'; -import util from 'util'; -import { isPlatform } from './common/platform'; -const execAsync = util.promisify(require('child_process').exec); +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(); @@ -22,28 +20,39 @@ export async function handleUpdates(mainWindow: BrowserWindow) { } } export function setupTrayItem(mainWindow: BrowserWindow) { - const iconName = isPlatform('mac') - ? 'taskbar-icon-Template.png' - : 'taskbar-icon.png'; + const iconName = isPlatform("mac") + ? "taskbar-icon-Template.png" + : "taskbar-icon.png"; const trayImgPath = path.join( - isDev ? 'build' : process.resourcesPath, - iconName + isDev ? "build" : process.resourcesPath, + iconName, ); const trayIcon = nativeImage.createFromPath(trayImgPath); const tray = new Tray(trayIcon); - tray.setToolTip('ente'); + tray.setToolTip("ente"); tray.setContextMenu(buildContextMenu(mainWindow)); return tray; } export function handleDownloads(mainWindow: BrowserWindow) { - mainWindow.webContents.session.on('will-download', (_, item) => { + mainWindow.webContents.session.on("will-download", (_, item) => { item.setSavePath( - getUniqueSavePath(item.getFilename(), app.getPath('downloads')) + 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 } = @@ -58,14 +67,14 @@ export function getUniqueSavePath(filename: string, directory: string): string { extension, ] .filter((x) => x) // filters out undefined/null values - .join(''); + .join(""); uniqueFileSavePath = path.join(directory, fileNameWithNumberedSuffix); } return uniqueFileSavePath; } export function setupMacWindowOnDockIconClick() { - app.on('activate', function () { + app.on("activate", function () { const windows = BrowserWindow.getAllWindows(); // we allow only one window windows[0].show(); @@ -76,29 +85,17 @@ export async function setupMainMenu(mainWindow: BrowserWindow) { Menu.setApplicationMenu(await buildMenuBar(mainWindow)); } -export function setupMainHotReload() { - if (isDev) { - electronReload(__dirname, {}); - } -} - -export function setupNextElectronServe() { - serveNextAt(PROD_HOST_URL, { - outputDir: RENDERER_OUTPUT_DIR, - }); -} - export async function handleDockIconHideOnAutoLaunch() { const shouldHideDockIcon = getHideDockIconPreference(); const wasAutoLaunched = await autoLauncher.wasAutoLaunched(); - if (isPlatform('mac') && shouldHideDockIcon && wasAutoLaunched) { + if (isPlatform("mac") && shouldHideDockIcon && wasAutoLaunched) { app.dock.hide(); } } export function enableSharedArrayBufferSupport() { - app.commandLine.appendSwitch('enable-features', 'SharedArrayBuffer'); + app.commandLine.appendSwitch("enable-features", "SharedArrayBuffer"); } export function logSystemInfo() { @@ -110,27 +107,16 @@ export function logSystemInfo() { ElectronLog.info({ appVersion }); } -export function handleExternalLinks(mainWindow: BrowserWindow) { - mainWindow.webContents.setWindowOpenHandler(({ url }) => { - if (!url.startsWith(PROD_HOST_URL)) { - require('electron').shell.openExternal(url); - return { action: 'deny' }; - } else { - return { action: 'allow' }; - } - }); -} - export async function checkIfInstalledViaBrew() { - if (!isPlatform('mac')) { + if (!isPlatform("mac")) { return false; } try { - await execAsync('brew list --cask ente'); - ElectronLog.info('ente installed via brew'); + await execAsync("brew list --cask ente"); + ElectronLog.info("ente installed via brew"); return true; } catch (e) { - ElectronLog.info('ente not installed via brew'); + ElectronLog.info("ente not installed via brew"); return false; } } diff --git a/desktop/src/utils/menu.ts b/desktop/src/utils/menu.ts index 2d1deca6d..c86786ff6 100644 --- a/desktop/src/utils/menu.ts +++ b/desktop/src/utils/menu.ts @@ -1,34 +1,34 @@ import { - Menu, app, - shell, BrowserWindow, + Menu, MenuItemConstructorOptions, -} from 'electron'; + 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 { setIsAppQuitting } from '../main'; -import autoLauncher from '../services/autoLauncher'; -import { isPlatform } from './common/platform'; -import ElectronLog from 'electron-log'; -import { forceCheckForUpdateAndNotify } from '../services/appUpdater'; +} 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', + label: "Open ente", click: function () { mainWindow.maximize(); mainWindow.show(); }, }, { - label: 'Quit ente', + label: "Quit ente", click: function () { - ElectronLog.log('user quit the app'); + ElectronLog.log("user quit the app"); setIsAppQuitting(true); app.quit(); }, @@ -39,43 +39,43 @@ export function buildContextMenu(mainWindow: BrowserWindow): Menu { export async function buildMenuBar(mainWindow: BrowserWindow): Promise { let isAutoLaunchEnabled = await autoLauncher.isEnabled(); - const isMac = isPlatform('mac'); + const isMac = isPlatform("mac"); let shouldHideDockIcon = getHideDockIconPreference(); const template: MenuItemConstructorOptions[] = [ { - label: 'ente', + label: "ente", submenu: [ ...((isMac ? [ { - label: 'About ente', - role: 'about', + label: "About ente", + role: "about", }, ] : []) as MenuItemConstructorOptions[]), - { type: 'separator' }, + { type: "separator" }, { - label: 'Check for updates...', + label: "Check for updates...", click: () => { forceCheckForUpdateAndNotify(mainWindow); }, }, { - label: 'View Changelog', + label: "View Changelog", click: () => { shell.openExternal( - 'https://github.com/ente-io/ente/blob/main/desktop/CHANGELOG.md' + "https://github.com/ente-io/ente/blob/main/desktop/CHANGELOG.md", ); }, }, - { type: 'separator' }, + { type: "separator" }, { - label: 'Preferences', + label: "Preferences", submenu: [ { - label: 'Open ente on startup', - type: 'checkbox', + label: "Open ente on startup", + type: "checkbox", checked: isAutoLaunchEnabled, click: () => { autoLauncher.toggleAutoLaunch(); @@ -83,8 +83,8 @@ export async function buildMenuBar(mainWindow: BrowserWindow): Promise { }, }, { - label: 'Hide dock icon', - type: 'checkbox', + label: "Hide dock icon", + type: "checkbox", checked: shouldHideDockIcon, click: () => { setHideDockIconPreference(!shouldHideDockIcon); @@ -94,121 +94,121 @@ export async function buildMenuBar(mainWindow: BrowserWindow): Promise { ], }, - { type: 'separator' }, + { type: "separator" }, ...((isMac ? [ { - label: 'Hide ente', - role: 'hide', + label: "Hide ente", + role: "hide", }, { - label: 'Hide others', - role: 'hideOthers', + label: "Hide others", + role: "hideOthers", }, ] : []) as MenuItemConstructorOptions[]), - { type: 'separator' }, + { type: "separator" }, { - label: 'Quit ente', - role: 'quit', + label: "Quit ente", + role: "quit", }, ], }, { - label: 'Edit', + 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' }, + { 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: "pasteAndMatchStyle", + label: "Paste and match style", }, - { role: 'delete', label: 'Delete' }, - { role: 'selectAll', label: 'Select all' }, - { type: 'separator' }, + { role: "delete", label: "Delete" }, + { role: "selectAll", label: "Select all" }, + { type: "separator" }, { - label: 'Speech', + label: "Speech", submenu: [ { - role: 'startSpeaking', - label: 'start speaking', + role: "startSpeaking", + label: "start speaking", }, { - role: 'stopSpeaking', - label: 'stop speaking', + role: "stopSpeaking", + label: "stop speaking", }, ], }, ] : [ - { type: 'separator' }, - { role: 'selectAll', label: 'Select all' }, + { type: "separator" }, + { role: "selectAll", label: "Select all" }, ]) as MenuItemConstructorOptions[]), ], }, { - label: 'View', + 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' }, + { 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', + label: "Window", submenu: [ - { role: 'close', label: 'Close' }, - { role: 'minimize', label: 'Minimize' }, + { role: "close", label: "Close" }, + { role: "minimize", label: "Minimize" }, ...((isMac ? [ - { type: 'separator' }, - { role: 'front', label: 'Bring to front' }, - { type: 'separator' }, - { role: 'window', label: 'ente' }, + { type: "separator" }, + { role: "front", label: "Bring to front" }, + { type: "separator" }, + { role: "window", label: "ente" }, ] : []) as MenuItemConstructorOptions[]), ], }, { - label: 'Help', + label: "Help", submenu: [ { - label: 'FAQ', - click: () => shell.openExternal('https://ente.io/faq/'), + label: "FAQ", + click: () => shell.openExternal("https://ente.io/faq/"), }, - { type: 'separator' }, + { type: "separator" }, { - label: 'Support', - click: () => shell.openExternal('mailto:support@ente.io'), + label: "Support", + click: () => shell.openExternal("mailto:support@ente.io"), }, { - label: 'Product updates', - click: () => shell.openExternal('https://ente.io/blog/'), + label: "Product updates", + click: () => shell.openExternal("https://ente.io/blog/"), }, - { type: 'separator' }, + { type: "separator" }, { - label: 'View crash reports', + label: "View crash reports", click: () => { - shell.openPath(app.getPath('crashDumps')); + shell.openPath(app.getPath("crashDumps")); }, }, { - label: 'View logs', + label: "View logs", click: () => { - shell.openPath(app.getPath('logs')); + shell.openPath(app.getPath("logs")); }, }, ], diff --git a/desktop/src/utils/preload.ts b/desktop/src/utils/preload.ts deleted file mode 100644 index f29a1d543..000000000 --- a/desktop/src/utils/preload.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { webFrame } from 'electron'; - -export const fixHotReloadNext12 = () => { - webFrame.executeJavaScript(`Object.defineProperty(globalThis, 'WebSocket', { - value: new Proxy(WebSocket, { - construct: (Target, [url, protocols]) => { - if (url.endsWith('/_next/webpack-hmr')) { - // Fix the Next.js hmr client url - return new Target("ws://localhost:3000/_next/webpack-hmr", protocols) - } else { - return new Target(url, protocols) - } - } - }) - });`); -}; diff --git a/desktop/src/utils/processStats.ts b/desktop/src/utils/processStats.ts index 8bb61c262..b10238eea 100644 --- a/desktop/src/utils/processStats.ts +++ b/desktop/src/utils/processStats.ts @@ -1,6 +1,6 @@ -import ElectronLog from 'electron-log'; -import { webFrame } from 'electron/renderer'; -import { convertBytesToHumanReadable } from './logging'; +import ElectronLog from "electron-log"; +import { webFrame } from "electron/renderer"; +import { convertBytesToHumanReadable } from "./logging"; const LOGGING_INTERVAL_IN_MICROSECONDS = 30 * 1000; // 30 seconds @@ -16,14 +16,14 @@ const HIGH_RENDERER_MEMORY_USAGE_THRESHOLD_IN_KILOBYTES = 1024 * 1024; // 1 GB async function logMainProcessStats() { const processMemoryInfo = await getNormalizedProcessMemoryInfo( - await process.getProcessMemoryInfo() + await process.getProcessMemoryInfo(), ); const cpuUsage = process.getCPUUsage(); const heapStatistics = getNormalizedHeapStatistics( - process.getHeapStatistics() + process.getHeapStatistics(), ); - ElectronLog.log('main process stats', { + ElectronLog.log("main process stats", { processMemoryInfo, heapStatistics, cpuUsage, @@ -42,11 +42,11 @@ async function logSpikeMainMemoryUsage() { const processMemoryInfo = await process.getProcessMemoryInfo(); const currentMemoryUsage = Math.max( processMemoryInfo.residentSet ?? 0, - processMemoryInfo.private + processMemoryInfo.private, ); const previousMemoryUsage = Math.max( previousMainProcessMemoryInfo.residentSet ?? 0, - previousMainProcessMemoryInfo.private + previousMainProcessMemoryInfo.private, ); const isSpiking = currentMemoryUsage - previousMemoryUsage >= @@ -66,10 +66,10 @@ async function logSpikeMainMemoryUsage() { await getNormalizedProcessMemoryInfo(previousMainProcessMemoryInfo); const cpuUsage = process.getCPUUsage(); const heapStatistics = getNormalizedHeapStatistics( - process.getHeapStatistics() + process.getHeapStatistics(), ); - ElectronLog.log('reporting main memory usage spike', { + ElectronLog.log("reporting main memory usage spike", { currentProcessMemoryInfo: normalizedCurrentProcessMemoryInfo, previousProcessMemoryInfo: normalizedPreviousProcessMemoryInfo, heapStatistics, @@ -94,12 +94,12 @@ async function logSpikeRendererMemoryUsage() { const processMemoryInfo = await process.getProcessMemoryInfo(); const currentMemoryUsage = Math.max( processMemoryInfo.residentSet ?? 0, - processMemoryInfo.private + processMemoryInfo.private, ); const previousMemoryUsage = Math.max( previousRendererProcessMemoryInfo.private, - previousRendererProcessMemoryInfo.residentSet ?? 0 + previousRendererProcessMemoryInfo.residentSet ?? 0, ); const isSpiking = currentMemoryUsage - previousMemoryUsage >= @@ -117,14 +117,14 @@ async function logSpikeRendererMemoryUsage() { await getNormalizedProcessMemoryInfo(processMemoryInfo); const normalizedPreviousProcessMemoryInfo = await getNormalizedProcessMemoryInfo( - previousRendererProcessMemoryInfo + previousRendererProcessMemoryInfo, ); const cpuUsage = process.getCPUUsage(); const heapStatistics = getNormalizedHeapStatistics( - process.getHeapStatistics() + process.getHeapStatistics(), ); - ElectronLog.log('reporting renderer memory usage spike', { + ElectronLog.log("reporting renderer memory usage spike", { currentProcessMemoryInfo: normalizedCurrentProcessMemoryInfo, previousProcessMemoryInfo: normalizedPreviousProcessMemoryInfo, heapStatistics, @@ -140,13 +140,13 @@ async function logSpikeRendererMemoryUsage() { async function logRendererProcessStats() { const blinkMemoryInfo = getNormalizedBlinkMemoryInfo(); const heapStatistics = getNormalizedHeapStatistics( - process.getHeapStatistics() + process.getHeapStatistics(), ); const webFrameResourceUsage = getNormalizedWebFrameResourceUsage(); const processMemoryInfo = await getNormalizedProcessMemoryInfo( - await process.getProcessMemoryInfo() + await process.getProcessMemoryInfo(), ); - ElectronLog.log('renderer process stats', { + ElectronLog.log("renderer process stats", { blinkMemoryInfo, heapStatistics, processMemoryInfo, @@ -157,7 +157,7 @@ async function logRendererProcessStats() { export function setupMainProcessStatsLogger() { setInterval( logSpikeMainMemoryUsage, - SPIKE_DETECTION_INTERVAL_IN_MICROSECONDS + SPIKE_DETECTION_INTERVAL_IN_MICROSECONDS, ); setInterval(logMainProcessStats, LOGGING_INTERVAL_IN_MICROSECONDS); } @@ -165,7 +165,7 @@ export function setupMainProcessStatsLogger() { export function setupRendererProcessStatsLogger() { setInterval( logSpikeRendererMemoryUsage, - SPIKE_DETECTION_INTERVAL_IN_MICROSECONDS + SPIKE_DETECTION_INTERVAL_IN_MICROSECONDS, ); setInterval(logRendererProcessStats, LOGGING_INTERVAL_IN_MICROSECONDS); } @@ -174,21 +174,21 @@ export async function logRendererProcessMemoryUsage(message: string) { const processMemoryInfo = await process.getProcessMemoryInfo(); const processMemory = Math.max( processMemoryInfo.private, - processMemoryInfo.residentSet ?? 0 + processMemoryInfo.residentSet ?? 0, ); ElectronLog.log( - 'renderer ProcessMemory', + "renderer ProcessMemory", message, - convertBytesToHumanReadable(processMemory * 1024) + convertBytesToHumanReadable(processMemory * 1024), ); } const getNormalizedProcessMemoryInfo = async ( - processMemoryInfo: Electron.ProcessMemoryInfo + processMemoryInfo: Electron.ProcessMemoryInfo, ) => { return { residentSet: convertBytesToHumanReadable( - processMemoryInfo.residentSet * 1024 + processMemoryInfo.residentSet * 1024, ), private: convertBytesToHumanReadable(processMemoryInfo.private * 1024), shared: convertBytesToHumanReadable(processMemoryInfo.shared * 1024), @@ -199,40 +199,40 @@ const getNormalizedBlinkMemoryInfo = () => { const blinkMemoryInfo = process.getBlinkMemoryInfo(); return { allocated: convertBytesToHumanReadable( - blinkMemoryInfo.allocated * 1024 + blinkMemoryInfo.allocated * 1024, ), total: convertBytesToHumanReadable(blinkMemoryInfo.total * 1024), }; }; const getNormalizedHeapStatistics = ( - heapStatistics: Electron.HeapStatistics + heapStatistics: Electron.HeapStatistics, ) => { return { totalHeapSize: convertBytesToHumanReadable( - heapStatistics.totalHeapSize * 1024 + heapStatistics.totalHeapSize * 1024, ), totalHeapSizeExecutable: convertBytesToHumanReadable( - heapStatistics.totalHeapSizeExecutable * 1024 + heapStatistics.totalHeapSizeExecutable * 1024, ), totalPhysicalSize: convertBytesToHumanReadable( - heapStatistics.totalPhysicalSize * 1024 + heapStatistics.totalPhysicalSize * 1024, ), totalAvailableSize: convertBytesToHumanReadable( - heapStatistics.totalAvailableSize * 1024 + heapStatistics.totalAvailableSize * 1024, ), usedHeapSize: convertBytesToHumanReadable( - heapStatistics.usedHeapSize * 1024 + heapStatistics.usedHeapSize * 1024, ), heapSizeLimit: convertBytesToHumanReadable( - heapStatistics.heapSizeLimit * 1024 + heapStatistics.heapSizeLimit * 1024, ), mallocedMemory: convertBytesToHumanReadable( - heapStatistics.mallocedMemory * 1024 + heapStatistics.mallocedMemory * 1024, ), peakMallocedMemory: convertBytesToHumanReadable( - heapStatistics.peakMallocedMemory * 1024 + heapStatistics.peakMallocedMemory * 1024, ), doesZapGarbage: heapStatistics.doesZapGarbage, }; @@ -244,51 +244,51 @@ const getNormalizedWebFrameResourceUsage = () => { images: { count: webFrameResourceUsage.images.count, size: convertBytesToHumanReadable( - webFrameResourceUsage.images.size + webFrameResourceUsage.images.size, ), liveSize: convertBytesToHumanReadable( - webFrameResourceUsage.images.liveSize + webFrameResourceUsage.images.liveSize, ), }, scripts: { count: webFrameResourceUsage.scripts.count, size: convertBytesToHumanReadable( - webFrameResourceUsage.scripts.size + webFrameResourceUsage.scripts.size, ), liveSize: convertBytesToHumanReadable( - webFrameResourceUsage.scripts.liveSize + webFrameResourceUsage.scripts.liveSize, ), }, cssStyleSheets: { count: webFrameResourceUsage.cssStyleSheets.count, size: convertBytesToHumanReadable( - webFrameResourceUsage.cssStyleSheets.size + webFrameResourceUsage.cssStyleSheets.size, ), liveSize: convertBytesToHumanReadable( - webFrameResourceUsage.cssStyleSheets.liveSize + webFrameResourceUsage.cssStyleSheets.liveSize, ), }, xslStyleSheets: { count: webFrameResourceUsage.xslStyleSheets.count, size: convertBytesToHumanReadable( - webFrameResourceUsage.xslStyleSheets.size + webFrameResourceUsage.xslStyleSheets.size, ), liveSize: convertBytesToHumanReadable( - webFrameResourceUsage.xslStyleSheets.liveSize + webFrameResourceUsage.xslStyleSheets.liveSize, ), }, fonts: { count: webFrameResourceUsage.fonts.count, size: convertBytesToHumanReadable(webFrameResourceUsage.fonts.size), liveSize: convertBytesToHumanReadable( - webFrameResourceUsage.fonts.liveSize + webFrameResourceUsage.fonts.liveSize, ), }, other: { count: webFrameResourceUsage.other.count, size: convertBytesToHumanReadable(webFrameResourceUsage.other.size), liveSize: convertBytesToHumanReadable( - webFrameResourceUsage.other.liveSize + webFrameResourceUsage.other.liveSize, ), }, }; diff --git a/desktop/src/utils/temp.ts b/desktop/src/utils/temp.ts index 35a261e63..91496ce13 100644 --- a/desktop/src/utils/temp.ts +++ b/desktop/src/utils/temp.ts @@ -1,14 +1,14 @@ -import { app } from 'electron'; -import path from 'path'; -import { existsSync, mkdir } from 'promise-fs'; +import { app } from "electron"; +import path from "path"; +import { existsSync, mkdir } from "promise-fs"; -const ENTE_TEMP_DIRECTORY = 'ente'; +const ENTE_TEMP_DIRECTORY = "ente"; const CHARACTERS = - 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; export async function getTempDirPath() { - const tempDirPath = path.join(app.getPath('temp'), ENTE_TEMP_DIRECTORY); + const tempDirPath = path.join(app.getPath("temp"), ENTE_TEMP_DIRECTORY); if (!existsSync(tempDirPath)) { await mkdir(tempDirPath); } @@ -16,12 +16,12 @@ export async function getTempDirPath() { } function generateTempName(length: number) { - let result = ''; + let result = ""; const charactersLength = CHARACTERS.length; for (let i = 0; i < length; i++) { result += CHARACTERS.charAt( - Math.floor(Math.random() * charactersLength) + Math.floor(Math.random() * charactersLength), ); } return result; @@ -32,7 +32,7 @@ export async function generateTempFilePath(formatSuffix: string) { do { const tempDirPath = await getTempDirPath(); const namePrefix = generateTempName(10); - tempFilePath = path.join(tempDirPath, namePrefix + '-' + formatSuffix); + tempFilePath = path.join(tempDirPath, namePrefix + "-" + formatSuffix); } while (existsSync(tempFilePath)); return tempFilePath; } diff --git a/desktop/src/utils/watch.ts b/desktop/src/utils/watch.ts index 89ddd66b5..d8575ebd7 100644 --- a/desktop/src/utils/watch.ts +++ b/desktop/src/utils/watch.ts @@ -1,11 +1,11 @@ -import { WatchMapping } from '../types'; +import { WatchMapping } from "../types"; export function isMappingPresent( watchMappings: WatchMapping[], - folderPath: string + folderPath: string, ) { const watchMapping = watchMappings?.find( - (mapping) => mapping.folderPath === folderPath + (mapping) => mapping.folderPath === folderPath, ); return !!watchMapping; } diff --git a/desktop/thirdparty/next-electron-server b/desktop/thirdparty/next-electron-server deleted file mode 160000 index a88030295..000000000 --- a/desktop/thirdparty/next-electron-server +++ /dev/null @@ -1 +0,0 @@ -Subproject commit a88030295c89dd8f43d9e3a45025678d95c78a45 diff --git a/desktop/tsconfig.json b/desktop/tsconfig.json index 283ef57b4..142c36005 100644 --- a/desktop/tsconfig.json +++ b/desktop/tsconfig.json @@ -3,13 +3,15 @@ "target": "es2021", "module": "commonjs", "esModuleInterop": true, + /* Emit the generated JS into app */ + "outDir": "app", "noImplicitAny": true, "sourceMap": true, - "outDir": "app", "baseUrl": "src", "paths": { "*": ["node_modules/*"] } }, - "include": ["src/**/*"] + /* Transpile all ts files in src/ */ + "include": ["src/**/*.ts"] } diff --git a/desktop/yarn.lock b/desktop/yarn.lock index d748f79e2..c8080d23f 100644 --- a/desktop/yarn.lock +++ b/desktop/yarn.lock @@ -183,109 +183,10 @@ resolved "https://registry.yarnpkg.com/@octetstream/promisify/-/promisify-2.0.2.tgz#29ac3bd7aefba646db670227f895d812c1a19615" integrity sha512-7XHoRB61hxsz8lBQrjC1tq/3OEIgpvGWg6DKAdwi7WRzruwkmsdwmOoUXbU4Dtd4RSOMDwed0SkP3y8UlMt1Bg== -"@sentry/browser@6.7.1": - version "6.7.1" - resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-6.7.1.tgz#e01144a08984a486ecc91d7922cc457e9c9bd6b7" - integrity sha512-R5PYx4TTvifcU790XkK6JVGwavKaXwycDU0MaAwfc4Vf7BLm5KCNJCsDySu1RPAap/017MVYf54p6dWvKiRviA== - dependencies: - "@sentry/core" "6.7.1" - "@sentry/types" "6.7.1" - "@sentry/utils" "6.7.1" - tslib "^1.9.3" - -"@sentry/cli@^1.68.0": - version "1.74.4" - resolved "https://registry.yarnpkg.com/@sentry/cli/-/cli-1.74.4.tgz#7df82f68045a155e1885bfcbb5d303e5259eb18e" - integrity sha512-BMfzYiedbModsNBJlKeBOLVYUtwSi99LJ8gxxE4Bp5N8hyjNIN0WVrozAVZ27mqzAuy6151Za3dpmOLO86YlGw== - dependencies: - https-proxy-agent "^5.0.0" - mkdirp "^0.5.5" - node-fetch "^2.6.7" - npmlog "^4.1.2" - progress "^2.0.3" - proxy-from-env "^1.1.0" - which "^2.0.2" - -"@sentry/core@6.7.1": - version "6.7.1" - resolved "https://registry.yarnpkg.com/@sentry/core/-/core-6.7.1.tgz#c3aaa6415d06bec65ac446b13b84f073805633e3" - integrity sha512-VAv8OR/7INn2JfiLcuop4hfDcyC7mfL9fdPndQEhlacjmw8gRrgXjR7qyhnCTgzFLkHI7V5bcdIzA83TRPYQpA== - dependencies: - "@sentry/hub" "6.7.1" - "@sentry/minimal" "6.7.1" - "@sentry/types" "6.7.1" - "@sentry/utils" "6.7.1" - tslib "^1.9.3" - -"@sentry/electron@^2.5.1": - version "2.5.4" - resolved "https://registry.yarnpkg.com/@sentry/electron/-/electron-2.5.4.tgz#337b2f7e228e805a3e4eb3611c7b12c6cf87c618" - integrity sha512-tCCK+P581TmdjsDpHBQz7qYcldzGdUk1Fd6FPxPy1JKGzeY4uf/uSLKzR80Lzs5kTpEZFOwiMHSA8yjwFp5qoA== - dependencies: - "@sentry/browser" "6.7.1" - "@sentry/core" "6.7.1" - "@sentry/minimal" "6.7.1" - "@sentry/node" "6.7.1" - "@sentry/types" "6.7.1" - "@sentry/utils" "6.7.1" - tslib "^2.2.0" - -"@sentry/hub@6.7.1": - version "6.7.1" - resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-6.7.1.tgz#d46d24deec67f0731a808ca16796e6765b371bc1" - integrity sha512-eVCTWvvcp6xa0A5GGNHMQEWslmKPlisE5rGmsV/kjvSUv3zSrI0eIDfb51ikdnCiBjHpK2NBWP8Vy8cZOEJegg== - dependencies: - "@sentry/types" "6.7.1" - "@sentry/utils" "6.7.1" - tslib "^1.9.3" - -"@sentry/minimal@6.7.1": - version "6.7.1" - resolved "https://registry.yarnpkg.com/@sentry/minimal/-/minimal-6.7.1.tgz#babf85ee2f167e9dcf150d750d7a0b250c98449c" - integrity sha512-HDDPEnQRD6hC0qaHdqqKDStcdE1KhkFh0RCtJNMCDn0zpav8Dj9AteF70x6kLSlliAJ/JFwi6AmQrLz+FxPexw== - dependencies: - "@sentry/hub" "6.7.1" - "@sentry/types" "6.7.1" - tslib "^1.9.3" - -"@sentry/node@6.7.1": - version "6.7.1" - resolved "https://registry.yarnpkg.com/@sentry/node/-/node-6.7.1.tgz#b09e2eca8e187168feba7bd865a23935bf0f5cc0" - integrity sha512-rtZo1O8ROv4lZwuljQz3iFZW89oXSlgXCG2VqkxQyRspPWu89abROpxLjYzsWwQ8djnur1XjFv51kOLDUTS6Qw== - dependencies: - "@sentry/core" "6.7.1" - "@sentry/hub" "6.7.1" - "@sentry/tracing" "6.7.1" - "@sentry/types" "6.7.1" - "@sentry/utils" "6.7.1" - cookie "^0.4.1" - https-proxy-agent "^5.0.0" - lru_map "^0.3.3" - tslib "^1.9.3" - -"@sentry/tracing@6.7.1": - version "6.7.1" - resolved "https://registry.yarnpkg.com/@sentry/tracing/-/tracing-6.7.1.tgz#b11f0c17a6c5dc14ef44733e5436afb686683268" - integrity sha512-wyS3nWNl5mzaC1qZ2AIp1hjXnfO9EERjMIJjCihs2LWBz1r3efxrHxJHs8wXlNWvrT3KLhq/7vvF5CdU82uPeQ== - dependencies: - "@sentry/hub" "6.7.1" - "@sentry/minimal" "6.7.1" - "@sentry/types" "6.7.1" - "@sentry/utils" "6.7.1" - tslib "^1.9.3" - -"@sentry/types@6.7.1": - version "6.7.1" - resolved "https://registry.yarnpkg.com/@sentry/types/-/types-6.7.1.tgz#c8263e1886df5e815570c4668eb40a1cfaa1c88b" - integrity sha512-9AO7HKoip2MBMNQJEd6+AKtjj2+q9Ze4ooWUdEvdOVSt5drg7BGpK221/p9JEOyJAZwEPEXdcMd3VAIMiOb4MA== - -"@sentry/utils@6.7.1": - version "6.7.1" - resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-6.7.1.tgz#909184ad580f0f6375e1e4d4a6ffd33dfe64a4d1" - integrity sha512-Tq2otdbWlHAkctD+EWTYKkEx6BL1Qn3Z/imkO06/PvzpWvVhJWQ5qHAzz5XnwwqNHyV03KVzYB6znq1Bea9HuA== - dependencies: - "@sentry/types" "6.7.1" - tslib "^1.9.3" +"@pkgr/core@^0.1.0": + version "0.1.1" + resolved "https://registry.yarnpkg.com/@pkgr/core/-/core-0.1.1.tgz#1ec17e2edbec25c8306d424ecfbf13c7de1aaa31" + integrity sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA== "@sindresorhus/is@^4.0.0": version "4.6.0" @@ -539,14 +440,6 @@ agent-base@6: dependencies: debug "4" -aggregate-error@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a" - integrity sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA== - dependencies: - clean-stack "^2.0.0" - indent-string "^4.0.0" - ajv-formats@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/ajv-formats/-/ajv-formats-2.1.1.tgz#6e669400659eb74973bbf2e33327180a0996b520" @@ -584,13 +477,6 @@ ansi-colors@^4.1.1: resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.3.tgz#37611340eb2243e70cc604cad35d63270d48781b" integrity sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw== -ansi-escapes@^4.3.0: - version "4.3.2" - resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" - integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ== - dependencies: - type-fest "^0.21.3" - ansi-regex@^2.0.0: version "2.1.1" resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" @@ -601,11 +487,6 @@ 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" @@ -620,11 +501,6 @@ ansi-styles@^4.0.0, ansi-styles@^4.1.0: dependencies: color-convert "^2.0.1" -ansi-styles@^6.0.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.1.0.tgz#87313c102b8118abd57371afab34618bf7350ed3" - integrity sha512-VbqNsoz55SYGczauuup0MFUyXNQviSpFTj1RQtFzmQLk18qbVSpTFFGMT293rmDaQuKCT6InmbuEyUne4mTuxQ== - any-shell-escape@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/any-shell-escape/-/any-shell-escape-0.1.1.tgz#d55ab972244c71a9a5e1ab0879f30bf110806959" @@ -682,19 +558,6 @@ applescript@^1.0.0: resolved "https://registry.yarnpkg.com/applescript/-/applescript-1.0.0.tgz#bb87af568cad034a4e48c4bdaf6067a3a2701317" integrity sha512-yvtNHdWvtbYEiIazXAdp/NY+BBb65/DAseqlNiJQjOx9DynuzOYDbVLBJvuc0ve0VL9x6B3OHF6eH52y9hCBtQ== -aproba@^1.0.3: - version "1.2.0" - resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" - integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw== - -are-we-there-yet@~1.1.2: - version "1.1.7" - resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.7.tgz#b15474a932adab4ff8a50d9adfa7e4e926f21146" - integrity sha512-nxwy40TuMiUGqMyRHgCSWZ9FM4VAoRP4xUYSTv5ImRog+h9yISPbVH7H8fASCIzYn9wlEv4zvFL7uKDMCFQm3g== - dependencies: - delegates "^1.0.0" - readable-stream "^2.0.6" - argparse@^1.0.7: version "1.0.10" resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" @@ -972,18 +835,6 @@ ci-info@^3.2.0: resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.3.2.tgz#6d2967ffa407466481c6c90b6e16b3098f080128" integrity sha512-xmDt/QIAdeZ9+nfdPsaBCpMvHNLFiLdjj59qjqn+6iPe6YmHGQ35sBnQ8uslRBXFmXkiZQOJRjvQeoGppoTjjg== -clean-stack@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" - integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== - -cli-cursor@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307" - integrity sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw== - dependencies: - restore-cursor "^3.1.0" - cli-truncate@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/cli-truncate/-/cli-truncate-2.1.0.tgz#c39e28bf05edcde5be3b98992a22deed5a2b93c7" @@ -992,14 +843,6 @@ cli-truncate@^2.1.0: slice-ansi "^3.0.0" string-width "^4.2.0" -cli-truncate@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/cli-truncate/-/cli-truncate-3.1.0.tgz#3f23ab12535e3d73e839bb43e73c9de487db1389" - integrity sha512-wfOBkjXteqSnI59oPcJkcPl/ZmwvMMOj340qUIY1SKZCv0B9Cf4D4fAucRkIKQmsIuYK3x1rrgU7MeGRruiuiA== - dependencies: - slice-ansi "^5.0.0" - string-width "^5.0.0" - cliui@^7.0.2: version "7.0.4" resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" @@ -1054,11 +897,6 @@ color-name@~1.1.4: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== -colorette@^2.0.16, colorette@^2.0.17: - version "2.0.19" - resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.19.tgz#cdf044f47ad41a0f4b56b3a0d5b4e6e1a2d5a798" - integrity sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ== - combined-stream@^1.0.6, combined-stream@^1.0.8, combined-stream@~1.0.6: version "1.0.8" resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" @@ -1071,11 +909,6 @@ commander@^5.0.0: resolved "https://registry.yarnpkg.com/commander/-/commander-5.1.0.tgz#46abbd1652f8e059bddaef99bbdcb2ad9cf179ae" integrity sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg== -commander@^9.3.0: - version "9.3.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-9.3.0.tgz#f619114a5a2d2054e0d9ff1b31d5ccf89255e26b" - integrity sha512-hv95iU5uXPbK83mjrJKuZyFM/LBAoCV/XhVGkS5Je6tl7sxr6A0ITMw5WoRV46/UaJ46Nllm3Xt7IaJhXTIkzw== - compare-version@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/compare-version/-/compare-version-0.1.2.tgz#0162ec2d9351f5ddd59a9202cba935366a725080" @@ -1140,16 +973,6 @@ config-file-ts@^0.2.4: glob "^7.1.6" typescript "^4.0.2" -console-control-strings@^1.0.0, console-control-strings@~1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" - integrity sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ== - -cookie@^0.4.1: - version "0.4.2" - resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.2.tgz#0e41f24de5ecf317947c82fc789e06a884824432" - integrity sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA== - core-util-is@1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" @@ -1167,13 +990,6 @@ crc@^3.8.0: dependencies: buffer "^5.1.0" -cross-env@^7.0.3: - version "7.0.3" - resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-7.0.3.tgz#865264b29677dc015ba8418918965dd232fc54cf" - integrity sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw== - dependencies: - cross-spawn "^7.0.1" - 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" @@ -1258,10 +1074,15 @@ delayed-stream@~1.0.0: resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== -delegates@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" - integrity sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ== +detect-indent@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-7.0.1.tgz#cbb060a12842b9c4d333f1cac4aa4da1bb66bc25" + integrity sha512-Mc7QhQ8s+cLrnUfU/Ji94vG/r8M26m8f++vyres4ZoojaRDpZ1eSIh/EpzLNwlWuvzSZ3UbDFspjFvTDXe6e/g== + +detect-newline@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-4.0.1.tgz#fcefdb5713e1fb8cb2839b8b6ee22e6716ab8f23" + integrity sha512-qE3Veg1YXzGHQhlA6jzebZN2qVf6NX+A7m7qlhCGG30dJixrAQhYOsJjsnBjJkCSmuOPpCk30145fr8FV0bzog== detect-node@^2.0.4: version "2.1.0" @@ -1340,11 +1161,6 @@ 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== - ecc-jsbn@~0.1.1: version "0.1.2" resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" @@ -1471,11 +1287,6 @@ 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" @@ -1668,21 +1479,6 @@ esutils@^2.0.2: resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== -execa@^6.1.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/execa/-/execa-6.1.0.tgz#cea16dee211ff011246556388effa0818394fb20" - integrity sha512-QVWlX2e50heYJcCPG0iWtf8r0xjEYfz/OYLGDYH+IyjWezzPNxz63qNFOu0l4YftGWuizFVZHHs8PrLU5p2IDA== - dependencies: - cross-spawn "^7.0.3" - get-stream "^6.0.1" - human-signals "^3.0.1" - is-stream "^3.0.0" - merge-stream "^2.0.0" - npm-run-path "^5.1.0" - onetime "^6.0.0" - signal-exit "^3.0.7" - strip-final-newline "^3.0.0" - extend@~3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" @@ -1725,6 +1521,17 @@ fast-glob@^3.2.9: merge2 "^1.3.0" micromatch "^4.0.4" +fast-glob@^3.3.0: + version "3.3.2" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129" + integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.2" + merge2 "^1.3.0" + micromatch "^4.0.4" + fast-json-stable-stringify@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" @@ -1909,20 +1716,6 @@ gar@^1.0.4: resolved "https://registry.yarnpkg.com/gar/-/gar-1.0.4.tgz#f777bc7db425c0572fdeb52676172ca1ae9888b8" integrity sha512-w4n9cPWyP7aHxKxYHFQMegj7WIAsL/YX/C4Bs5Rr8s1H9M1rNtRWRsw+ovYMkXDQ5S4ZbYHsHAPmevPjPgw44w== -gauge@~2.7.3: - version "2.7.4" - resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7" - integrity sha512-14x4kjc6lkD3ltw589k0NrPD6cCNTD6CWoVUNpB85+DrtONoZn+Rug6xZU5RvSC4+TZPxA5AnBibQYAvZn41Hg== - dependencies: - aproba "^1.0.3" - console-control-strings "^1.0.0" - has-unicode "^2.0.0" - object-assign "^4.1.0" - signal-exit "^3.0.0" - string-width "^1.0.1" - strip-ansi "^3.0.1" - wide-align "^1.1.0" - 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" @@ -1945,6 +1738,11 @@ get-intrinsic@^1.1.1: has "^1.0.3" has-symbols "^1.0.3" +get-stdin@^9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-9.0.0.tgz#3983ff82e03d56f1b2ea0d3e60325f39d703a575" + integrity sha512-dVKBjfWisLAicarI2Sf+JuBE/DghV4UzNAVe9yhEJuzeREd3JhOTE9cUaJTeSa77fsbQUK3pcOpJfM59+VKZaA== + get-stream@^5.1.0: version "5.2.0" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3" @@ -1952,11 +1750,6 @@ get-stream@^5.1.0: dependencies: pump "^3.0.0" -get-stream@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" - integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== - getpass@^0.1.1: version "0.1.7" resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" @@ -1964,6 +1757,11 @@ getpass@^0.1.1: dependencies: assert-plus "^1.0.0" +git-hooks-list@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/git-hooks-list/-/git-hooks-list-3.1.0.tgz#386dc531dcc17474cf094743ff30987a3d3e70fc" + integrity sha512-LF8VeHeR7v+wAbXqfgRlTSX/1BJR9Q1vEMR8JAz1cEg6GX07+zyj3sAdDvYjj/xnlIfVuGgj4qBei1K3hKH+PA== + glob-parent@^5.1.2, glob-parent@~5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" @@ -2021,6 +1819,17 @@ globby@^11.1.0: merge2 "^1.4.1" slash "^3.0.0" +globby@^13.1.2: + version "13.2.2" + resolved "https://registry.yarnpkg.com/globby/-/globby-13.2.2.tgz#63b90b1bf68619c2135475cbd4e71e66aa090592" + integrity sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w== + dependencies: + dir-glob "^3.0.1" + fast-glob "^3.3.0" + ignore "^5.2.4" + merge2 "^1.4.1" + slash "^4.0.0" + got@^11.8.5: version "11.8.6" resolved "https://registry.yarnpkg.com/got/-/got-11.8.6.tgz#276e827ead8772eddbcfc97170590b841823233a" @@ -2078,11 +1887,6 @@ has-symbols@^1.0.3: resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== -has-unicode@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" - integrity sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ== - has@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" @@ -2153,16 +1957,6 @@ https-proxy-agent@^5.0.0, https-proxy-agent@^5.0.1: agent-base "6" debug "4" -human-signals@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-3.0.1.tgz#c740920859dafa50e5a3222da9d3bf4bb0e5eef5" - integrity sha512-rQLskxnM/5OCldHo+wNXbpVgDn5A17CUoKX+7Sokwaknlq7CdSnphy0W39GU8dw59XiCXmFXDg4fRuckQRKewQ== - -husky@^8.0.1: - version "8.0.1" - resolved "https://registry.yarnpkg.com/husky/-/husky-8.0.1.tgz#511cb3e57de3e3190514ae49ed50f6bc3f50b3e9" - integrity sha512-xs7/chUH/CKdOCs7Zy0Aev9e/dKOMZf3K1Az1nar3tzlv0jfqnYtu235bstsWTmXOR0EfINrPa97yy4Lz6RiKw== - iconv-corefoundation@^1.1.7: version "1.1.7" resolved "https://registry.yarnpkg.com/iconv-corefoundation/-/iconv-corefoundation-1.1.7.tgz#31065e6ab2c9272154c8b0821151e2c88f1b002a" @@ -2193,6 +1987,11 @@ ignore@^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: + version "5.3.1" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.1.tgz#5073e554cd42c5b33b394375f538b8593e34d4ef" + integrity sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw== + import-fresh@^3.0.0, import-fresh@^3.2.1: version "3.3.0" resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" @@ -2206,11 +2005,6 @@ imurmurhash@^0.1.4: resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== -indent-string@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" - integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== - inflight@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" @@ -2219,7 +2013,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@^2.0.3, inherits@~2.0.1, inherits@~2.0.3: +inherits@2, inherits@^2.0.3, inherits@~2.0.1: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -2272,11 +2066,6 @@ is-fullwidth-code-point@^3.0.0: resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== -is-fullwidth-code-point@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz#fae3167c729e7463f8461ce512b080a49268aa88" - integrity sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ== - is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: version "4.0.3" resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" @@ -2294,10 +2083,10 @@ is-obj@^2.0.0: resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-2.0.0.tgz#473fb05d973705e3fd9620545018ca8e22ef4982" integrity sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w== -is-stream@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-3.0.0.tgz#e6bfd7aa6bef69f4f472ce9bb681e3e57b4319ac" - integrity sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA== +is-plain-obj@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-4.1.0.tgz#d65025edec3657ce032fd7db63c97883eaed71f0" + integrity sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg== is-typedarray@~1.0.0: version "1.0.0" @@ -2309,11 +2098,6 @@ isarray@0.0.1: resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" integrity sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ== -isarray@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" - integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== - isbinaryfile@^4.0.8: version "4.0.10" resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-4.0.10.tgz#0c5b5e30c2557a2f06febd37b7322946aaee42b3" @@ -2465,49 +2249,11 @@ levn@^0.4.1: prelude-ls "^1.2.1" type-check "~0.4.0" -lilconfig@2.0.5: - version "2.0.5" - resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.0.5.tgz#19e57fd06ccc3848fd1891655b5a447092225b25" - integrity sha512-xaYmXZtTHPAw5m+xLN8ab9C+3a8YmV3asNSPOATITbtwrfbwaLJj8h66H1WMIpALCkqsIzK3h7oQ+PdX+LQ9Eg== - lines-and-columns@^1.1.6: version "1.2.4" resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== -lint-staged@^13.0.1: - version "13.0.3" - resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-13.0.3.tgz#d7cdf03a3830b327a2b63c6aec953d71d9dc48c6" - integrity sha512-9hmrwSCFroTSYLjflGI8Uk+GWAwMB4OlpU4bMJEAT5d/llQwtYKoim4bLOyLCuWFAhWEupE0vkIFqtw/WIsPug== - dependencies: - cli-truncate "^3.1.0" - colorette "^2.0.17" - commander "^9.3.0" - debug "^4.3.4" - execa "^6.1.0" - lilconfig "2.0.5" - listr2 "^4.0.5" - micromatch "^4.0.5" - normalize-path "^3.0.0" - object-inspect "^1.12.2" - pidtree "^0.6.0" - string-argv "^0.3.1" - yaml "^2.1.1" - -listr2@^4.0.5: - version "4.0.5" - resolved "https://registry.yarnpkg.com/listr2/-/listr2-4.0.5.tgz#9dcc50221583e8b4c71c43f9c7dfd0ef546b75d5" - integrity sha512-juGHV1doQdpNT3GSTs9IUN43QJb7KHdF9uqg7Vufs/tG9VTzpFphqF4pm/ICdAABGQxsyNn9CiYA3StkI6jpwA== - dependencies: - cli-truncate "^2.1.0" - colorette "^2.0.16" - log-update "^4.0.0" - p-map "^4.0.0" - rfdc "^1.3.0" - rxjs "^7.5.5" - through "^2.3.8" - wrap-ansi "^7.0.0" - locate-path@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e" @@ -2548,16 +2294,6 @@ lodash@^4.17.15, lodash@^4.17.21: resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== -log-update@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/log-update/-/log-update-4.0.0.tgz#589ecd352471f2a1c0c570287543a64dfd20e0a1" - integrity sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg== - dependencies: - ansi-escapes "^4.3.0" - cli-cursor "^3.1.0" - slice-ansi "^4.0.0" - wrap-ansi "^6.2.0" - lowercase-keys@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-2.0.0.tgz#2603e78b7b4b0006cbca2fbcc8a3202558ac9479" @@ -2570,11 +2306,6 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" -lru_map@^0.3.3: - version "0.3.3" - resolved "https://registry.yarnpkg.com/lru_map/-/lru_map-0.3.3.tgz#b5c8351b9464cbd750335a79650a0ec0e56118dd" - integrity sha512-Pn9cox5CsMYngeDbmChANltQl+5pi6XmTrraMSzhPmMBbmgcxmqWry0U3PGapCU1yB4/LqCcom7qhHZiF/jGfQ== - matcher@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/matcher/-/matcher-3.0.0.tgz#bd9060f4c5b70aa8041ccc6f80368760994f30ca" @@ -2582,17 +2313,12 @@ matcher@^3.0.0: dependencies: escape-string-regexp "^4.0.0" -merge-stream@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" - integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== - merge2@^1.3.0, merge2@^1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== -micromatch@^4.0.4, micromatch@^4.0.5: +micromatch@^4.0.4: version "4.0.5" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== @@ -2627,11 +2353,6 @@ mimic-fn@^3.0.0: resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-3.1.0.tgz#65755145bbf3e36954b949c16450427451d5ca74" integrity sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ== -mimic-fn@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-4.0.0.tgz#60a90550d5cb0b239cca65d893b1a53b29871ecc" - integrity sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw== - mimic-response@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b" @@ -2688,7 +2409,7 @@ minizlib@^2.1.1: minipass "^3.0.0" yallist "^4.0.0" -mkdirp@^0.5.1, mkdirp@^0.5.5: +mkdirp@^0.5.1: version "0.5.6" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6" integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw== @@ -2720,8 +2441,10 @@ natural-compare@^1.4.0: resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== -"next-electron-server@file:./thirdparty/next-electron-server": - version "0.0.8" +next-electron-server@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/next-electron-server/-/next-electron-server-1.0.0.tgz#03e133ed64a5ef671b6c6409f908c4901b1828cb" + integrity sha512-fTUaHwT0Jry2fbdUSIkAiIqgDAInI5BJFF4/j90/okvZCYlyx6yxpXB30KpzmOG6TN/ESwyvsFJVvS2WHT8PAA== node-addon-api@^1.6.3: version "1.7.2" @@ -2760,23 +2483,6 @@ normalize-url@^6.0.1: resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-6.1.0.tgz#40d0885b535deffe3f3147bec877d05fe4c5668a" integrity sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A== -npm-run-path@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-5.1.0.tgz#bc62f7f3f6952d9894bd08944ba011a6ee7b7e00" - integrity sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q== - dependencies: - path-key "^4.0.0" - -npmlog@^4.1.2: - version "4.1.2" - resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" - integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg== - dependencies: - are-we-there-yet "~1.1.2" - console-control-strings "~1.1.0" - gauge "~2.7.3" - set-blocking "~2.0.0" - nugget@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/nugget/-/nugget-2.0.2.tgz#398b591377b740b3dd308fabecd5ea09cf3443da" @@ -2800,16 +2506,6 @@ oauth-sign@~0.9.0: resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== -object-assign@^4.1.0: - version "4.1.1" - resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" - integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== - -object-inspect@^1.12.2: - version "1.12.2" - resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.2.tgz#c0641f26394532f28ab8d796ab954e43c009a8ea" - integrity sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ== - object-keys@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" @@ -2827,20 +2523,13 @@ once@^1.3.0, once@^1.3.1, once@^1.4.0: dependencies: wrappy "1" -onetime@^5.1.0, onetime@^5.1.2: +onetime@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== dependencies: mimic-fn "^2.1.0" -onetime@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/onetime/-/onetime-6.0.0.tgz#7c24c18ed1fd2e9bca4bd26806a33613c77d34b4" - integrity sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ== - dependencies: - mimic-fn "^4.0.0" - onnxruntime-common@~1.16.3: version "1.16.3" resolved "https://registry.yarnpkg.com/onnxruntime-common/-/onnxruntime-common-1.16.3.tgz#216bd1318d171496f1e92906a801c95bd2fb1aaa" @@ -2891,13 +2580,6 @@ p-locate@^4.1.0: dependencies: p-limit "^2.2.0" -p-map@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/p-map/-/p-map-4.0.0.tgz#bb2f95a5eda2ec168ec9274e06a747c3e2904d2b" - integrity sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ== - dependencies: - aggregate-error "^3.0.0" - p-try@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" @@ -2945,11 +2627,6 @@ path-key@^3.1.0: resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== -path-key@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/path-key/-/path-key-4.0.0.tgz#295588dc3aee64154f877adb9d780b81c554bf18" - integrity sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ== - path-parse@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" @@ -2980,11 +2657,6 @@ picomatch@^2.2.1: resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz" integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg== -pidtree@^0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/pidtree/-/pidtree-0.6.0.tgz#90ad7b6d42d5841e69e0a2419ef38f8883aa057c" - integrity sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g== - pkg-up@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/pkg-up/-/pkg-up-3.1.0.tgz#100ec235cc150e4fd42519412596a28512a0def5" @@ -3014,21 +2686,29 @@ prelude-ls@^1.2.1: resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== -prettier@2.5.1: - version "2.5.1" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.5.1.tgz#fff75fa9d519c54cf0fce328c1017d94546bc56a" - integrity sha512-vBZcPRUR5MZJwoyi3ZoyQlc1rXeEck8KgeC9AwwOn+exuxLxq5toTRDTSaVrXHxelDMHy9zlicw8u66yxoSUFg== +prettier-plugin-organize-imports@^3.2: + version "3.2.4" + resolved "https://registry.yarnpkg.com/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-3.2.4.tgz#77967f69d335e9c8e6e5d224074609309c62845e" + integrity sha512-6m8WBhIp0dfwu0SkgfOxJqh+HpdyfqSSLfKKRZSFbDuEQXDDndb8fTpRWkUrX/uBenkex3MgnVk0J3b3Y5byog== + +prettier-plugin-packagejson@^2.4: + version "2.4.12" + resolved "https://registry.yarnpkg.com/prettier-plugin-packagejson/-/prettier-plugin-packagejson-2.4.12.tgz#eeb917dad83ae42d0caccc9f26d3728b5c4f2434" + integrity sha512-hifuuOgw5rHHTdouw9VrhT8+Nd7UwxtL1qco8dUfd4XUFQL6ia3xyjSxhPQTsGnSYFraTWy5Omb+MZm/OWDTpQ== + dependencies: + sort-package-json "2.8.0" + synckit "0.9.0" + +prettier@^3: + version "3.2.5" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.2.5.tgz#e52bc3090586e824964a8813b09aba6233b28368" + integrity sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A== pretty-bytes@^4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-4.0.2.tgz#b2bf82e7350d65c6c33aa95aaa5a4f6327f61cd9" integrity sha512-yJAF+AjbHKlxQ8eezMd/34Mnj/YTQ3i6kLzvVsH4l/BfIFtp444n0wVbnsn66JimZ9uBofv815aRp1zCppxlWw== -process-nextick-args@~2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" - integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== - progress-stream@^1.1.0: version "1.2.0" resolved "https://registry.yarnpkg.com/progress-stream/-/progress-stream-1.2.0.tgz#2cd3cfea33ba3a89c9c121ec3347abe9ab125f77" @@ -3057,11 +2737,6 @@ promise-retry@^2.0.1: err-code "^2.0.2" retry "^0.12.0" -proxy-from-env@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" - integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== - psl@^1.1.28: version "1.9.0" resolved "https://registry.yarnpkg.com/psl/-/psl-1.9.0.tgz#d0df2a137f00794565fcaf3b2c00cd09f8d5a5a7" @@ -3136,19 +2811,6 @@ read-pkg@^5.2.0: parse-json "^5.0.0" type-fest "^0.6.0" -readable-stream@^2.0.6: - version "2.3.7" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" - integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== - dependencies: - core-util-is "~1.0.0" - inherits "~2.0.3" - isarray "~1.0.0" - process-nextick-args "~2.0.0" - safe-buffer "~5.1.1" - string_decoder "~1.1.1" - util-deprecate "~1.0.1" - readable-stream@^3.0.2: version "3.6.0" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" @@ -3242,14 +2904,6 @@ responselike@^2.0.0: dependencies: lowercase-keys "^2.0.0" -restore-cursor@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e" - integrity sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA== - dependencies: - onetime "^5.1.0" - signal-exit "^3.0.2" - retry@^0.12.0: version "0.12.0" resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b" @@ -3260,11 +2914,6 @@ reusify@^1.0.4: resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== -rfdc@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.3.0.tgz#d0b7c441ab2720d05dc4cf26e01c89631d9da08b" - integrity sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA== - rimraf@^3.0.0, rimraf@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" @@ -3291,7 +2940,7 @@ run-parallel@^1.1.9: dependencies: queue-microtask "^1.2.2" -rxjs@^7.0.0, rxjs@^7.5.5: +rxjs@^7.0.0: version "7.5.6" resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.5.6.tgz#0446577557862afd6903517ce7cae79ecb9662bc" integrity sha512-dnyv2/YsXhnm461G+R/Pe5bWP41Nm6LBXEYWI6eiFP4fiwx6WRI/CD0zbdVAudd9xwLEF2IDcKXLHit0FYjUzw== @@ -3303,11 +2952,6 @@ safe-buffer@^5.0.1, safe-buffer@^5.1.2, safe-buffer@~5.2.0: resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== -safe-buffer@~5.1.0, safe-buffer@~5.1.1: - version "5.1.2" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" - integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== - "safer-buffer@>= 2.1.2 < 3.0.0", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: version "2.1.2" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" @@ -3361,11 +3005,6 @@ serialize-error@^7.0.1: dependencies: type-fest "^0.13.1" -set-blocking@~2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" - integrity sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw== - shebang-command@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" @@ -3383,11 +3022,6 @@ shell-quote@^1.7.3: resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.7.3.tgz#aa40edac170445b9a431e17bb62c0b881b9c4123" integrity sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw== -signal-exit@^3.0.0, signal-exit@^3.0.2, signal-exit@^3.0.7: - version "3.0.7" - resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" - integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== - 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" @@ -3407,6 +3041,11 @@ slash@^3.0.0: resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== +slash@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-4.0.0.tgz#2422372176c4c6c5addb5e2ada885af984b396a7" + integrity sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew== + slice-ansi@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-3.0.0.tgz#31ddc10930a1b7e0b67b08c96c2f49b77a789787" @@ -3425,19 +3064,29 @@ slice-ansi@^4.0.0: astral-regex "^2.0.0" is-fullwidth-code-point "^3.0.0" -slice-ansi@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-5.0.0.tgz#b73063c57aa96f9cd881654b15294d95d285c42a" - integrity sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ== - dependencies: - ansi-styles "^6.0.0" - is-fullwidth-code-point "^4.0.0" - smart-buffer@^4.0.2: version "4.2.0" resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.2.0.tgz#6e1d71fa4f18c05f7d0ff216dd16a481d0e8d9ae" integrity sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg== +sort-object-keys@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/sort-object-keys/-/sort-object-keys-1.1.3.tgz#bff833fe85cab147b34742e45863453c1e190b45" + integrity sha512-855pvK+VkU7PaKYPc+Jjnmt4EzejQHyhhF33q31qG8x7maDzkeFhAAThdCYay11CISO+qAMwjOBP+fPZe0IPyg== + +sort-package-json@2.8.0: + version "2.8.0" + resolved "https://registry.yarnpkg.com/sort-package-json/-/sort-package-json-2.8.0.tgz#6a46439ad0fef77f091e678e103f03ecbea575c8" + integrity sha512-PxeNg93bTJWmDGnu0HADDucoxfFiKkIr73Kv85EBThlI1YQPdc0XovBgg2llD0iABZbu2SlKo8ntGmOP9wOj/g== + dependencies: + detect-indent "^7.0.1" + detect-newline "^4.0.0" + get-stdin "^9.0.0" + git-hooks-list "^3.0.0" + globby "^13.1.2" + is-plain-obj "^4.1.0" + sort-object-keys "^1.1.3" + source-map-support@^0.5.19: version "0.5.21" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" @@ -3517,11 +3166,6 @@ 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-argv@^0.3.1: - version "0.3.1" - resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.3.1.tgz#95e2fbec0427ae19184935f816d74aaa4c5c19da" - integrity sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg== - string-width@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" @@ -3531,7 +3175,7 @@ string-width@^1.0.1: is-fullwidth-code-point "^1.0.0" strip-ansi "^3.0.0" -"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +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== @@ -3540,15 +3184,6 @@ string-width@^1.0.1: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.1" -string-width@^5.0.0: - 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" @@ -3561,14 +3196,7 @@ string_decoder@~0.10.x: resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" integrity sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ== -string_decoder@~1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" - integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== - dependencies: - safe-buffer "~5.1.0" - -strip-ansi@^3.0.0, strip-ansi@^3.0.1: +strip-ansi@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" integrity sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg== @@ -3582,18 +3210,6 @@ strip-ansi@^6.0.0, strip-ansi@^6.0.1: dependencies: ansi-regex "^5.0.1" -strip-ansi@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.0.1.tgz#61740a08ce36b61e50e65653f07060d000975fb2" - integrity sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw== - dependencies: - ansi-regex "^6.0.1" - -strip-final-newline@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-3.0.0.tgz#52894c313fbff318835280aed60ff71ebf12b8fd" - integrity sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw== - strip-json-comments@^3.1.0, 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" @@ -3644,6 +3260,14 @@ supports-preserve-symlinks-flag@^1.0.0: resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== +synckit@0.9.0: + version "0.9.0" + resolved "https://registry.yarnpkg.com/synckit/-/synckit-0.9.0.tgz#5b33b458b3775e4466a5b377fba69c63572ae449" + integrity sha512-7RnqIMq572L8PeEzKeBINYEJDDxpcH8JEgLwUqBd3TkofhFRbkq4QLR0u+36avGAhCRbk2nnmjcW9SE531hPDg== + dependencies: + "@pkgr/core" "^0.1.0" + tslib "^2.6.2" + table@^6.0.9: version "6.8.0" resolved "https://registry.yarnpkg.com/table/-/table-6.8.0.tgz#87e28f14fa4321c3377ba286f07b79b281a3b3ca" @@ -3693,11 +3317,6 @@ through2@~0.2.3: readable-stream "~1.1.9" xtend "~2.1.1" -through@^2.3.8: - version "2.3.8" - resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" - integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg== - 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" @@ -3749,16 +3368,21 @@ truncate-utf8-bytes@^1.0.0: dependencies: utf8-byte-length "^1.0.1" -tslib@^1.8.1, tslib@^1.9.3: +tslib@^1.8.1: version "1.14.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== -tslib@^2.1.0, tslib@^2.2.0: +tslib@^2.1.0: version "2.4.0" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3" integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ== +tslib@^2.6.2: + version "2.6.2" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" + integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== + tsutils@^3.21.0: version "3.21.0" resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623" @@ -3795,11 +3419,6 @@ type-fest@^0.20.2: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== -type-fest@^0.21.3: - version "0.21.3" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" - integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== - type-fest@^0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.6.0.tgz#8d2a2370d3df886eb5c90ada1c5bf6188acf838b" @@ -3857,7 +3476,7 @@ utf8-byte-length@^1.0.1: resolved "https://registry.yarnpkg.com/utf8-byte-length/-/utf8-byte-length-1.0.4.tgz#f45f150c4c66eee968186505ab93fcbb8ad6bf61" integrity sha512-4+wkEYLBbWxqTahEsWrhxepcoVOJ+1z5PGIjPZxRkytcdSUaNjIjBM7Xn8E+pdSuV7SzvWovBFA54FO0JSoqhA== -util-deprecate@^1.0.1, util-deprecate@~1.0.1: +util-deprecate@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== @@ -3911,20 +3530,13 @@ whatwg-url@^5.0.0: tr46 "~0.0.3" webidl-conversions "^3.0.0" -which@^2.0.1, which@^2.0.2: +which@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== dependencies: isexe "^2.0.0" -wide-align@^1.1.0: - version "1.1.5" - resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.5.tgz#df1d4c206854369ecf3c9a4898f1b23fbd9d15d3" - integrity sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg== - dependencies: - string-width "^1.0.2 || 2 || 3 || 4" - winreg@1.2.4: version "1.2.4" resolved "https://registry.yarnpkg.com/winreg/-/winreg-1.2.4.tgz#ba065629b7a925130e15779108cf540990e98d1b" @@ -3935,15 +3547,6 @@ word-wrap@^1.2.3: resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== -wrap-ansi@^6.2.0: - version "6.2.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53" - integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.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" @@ -3980,11 +3583,6 @@ yallist@^4.0.0: resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== -yaml@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.1.1.tgz#1e06fb4ca46e60d9da07e4f786ea370ed3c3cfec" - integrity sha512-o96x3OPo8GjWeSLF+wOAbrPfhFOGY0W00GNaxCDv+9hkcDJEnev1yh8S7pgHF0ik6zc8sQLuL8hjHjJULZp8bw== - yargs-parser@^21.0.0: version "21.0.1" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.0.1.tgz#0267f286c877a4f0f728fceb6f8a3e4cb95c6e35" diff --git a/docs/README.md b/docs/README.md index 7af229177..6d3f92636 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,14 +1,33 @@ # Docs -Help and documentation for Ente's products +Help and documentation for Ente's products. -> [!CAUTION] -> -> **Currently not published**. There are bits we need to clean up before -> publishing these docs. They'll likely be available at help.ente.io once we -> wrap those loose ends up. +You can find the live version of these at +**[help.ente.io](https://help.ente.io)**. -## Running +## Quick edits + +You can edit these files directly on GitHub and open a pull request. +[help.ente.io](https://help.ente.io) will automatically get updated with your +changes in a few minutes after your pull request is merged. + +## Running locally + +The above workflow is great since it doesn't require you to setup anything on +your local machine. But if you plan on contributing frequently, you might find +it easier to run things locally. + +Clone this repository + +```sh +git clone https://github.com/ente-io/ente +``` + +Change to this directory + +```sh +cd ente/docs +``` Install dependencies @@ -22,27 +41,12 @@ Then start a local server yarn dev ``` -## Workflow +For an editor, VSCode is a good choice. Also install the Prettier extension for +VSCode, and set VSCode to format on save. This way the editor will automatically +format and wrap the text using the project's standard, so you can just focus on +the content. -You can edit these files directly on GitHub and open a pull request. That is the -easiest workflow to get started without needing to install anything on your -local machine. - -If you plan on contributing frequently, we recommend using an editor. VSCode is -a good choice. Also install the Prettier extension for VSCode, and set VSCode to -format on save. This way the editor will automatically format and wrap the text -using the project's standard, so you can just focus on the content. - -Note that we currently don't enforce these formatting standards to make it easy -for people unfamiliar with programming to also be able to make edits from GitHub -directly. - -This is a common theme - unlike the rest of the codebase where we expect some -baseline understanding of the tools involved, the docs are meant to be a place -for non-technical people to also provide their input. The reason for this is not -to increase the number of docs, but to bring more diversity to them. Such -diversity of viewpoints is essential for evolving documents that can be of help -to people of varying level of familiarity with tech. +## Have fun! If you're unsure about how to do something, just look around in the other files and copy paste whatever seems to match the look of what you're trying to do. And diff --git a/docs/docs/.vitepress/config.ts b/docs/docs/.vitepress/config.ts index 460dacc8a..4914ab625 100644 --- a/docs/docs/.vitepress/config.ts +++ b/docs/docs/.vitepress/config.ts @@ -7,6 +7,7 @@ export default defineConfig({ description: "Documentation and help for Ente's products", head: [["link", { rel: "icon", type: "image/png", href: "/favicon.png" }]], cleanUrls: true, + ignoreDeadLinks: "localhostLinks", themeConfig: { // We use the default theme (with some CSS color overrides). This // themeConfig block can be used to further customize the default theme. @@ -18,10 +19,6 @@ export default defineConfig({ pattern: "https://github.com/ente-io/ente/edit/main/docs/docs/:path", }, - // nav: [ - // { text: "Photos", link: "/photos/index" }, - // { text: "Authenticator", link: "/authenticator/index" }, - // ], search: { provider: "local", options: { diff --git a/docs/docs/.vitepress/sidebar.ts b/docs/docs/.vitepress/sidebar.ts index 8f2d556f4..ca531716b 100644 --- a/docs/docs/.vitepress/sidebar.ts +++ b/docs/docs/.vitepress/sidebar.ts @@ -1,27 +1,160 @@ -// When adding new pages, they need to manually inserted into their appropriate -// place here if you wish them to also appear in the sidebar. +// When adding new pages, they need to be manually inserted into their +// appropriate place here. -export const sidebar = []; +export const sidebar = [ + { + text: "Photos", + items: [ + { text: "Introduction", link: "/photos/" }, + { + text: "Features", + collapsed: true, + items: [ + { text: "Albums", link: "/photos/features/albums" }, + { text: "Archiving", link: "/photos/features/archive" }, + { text: "Cast", link: "/photos/features/cast/" }, + { + text: "Collecting photos", + link: "/photos/features/collect", + }, + { + text: "Family plans", + link: "/photos/features/family-plans", + }, + { text: "Hidden photos", link: "/photos/features/hide" }, + { + text: "Location tags", + link: "/photos/features/location-tags", + }, + { text: "Map", link: "/photos/features/map" }, + { + text: "Public link", + link: "/photos/features/public-link", + }, + { text: "Quick link", link: "/photos/features/quick-link" }, + { text: "Referrals", link: "/photos/features/referrals" }, + { text: "Sharing", link: "/photos/features/sharing" }, + { text: "Trash", link: "/photos/features/trash" }, + { + text: "Uncategorized", + link: "/photos/features/uncategorized", + }, + { + text: "Watch folder", + link: "/photos/features/watch-folder", + }, + ], + }, + { text: "FAQ", link: "/photos/faq/" }, + { + text: "Troubleshooting", + collapsed: true, + items: [ + { + text: "Files not uploading", + link: "/photos/troubleshooting/files-not-uploading", + }, + { + text: "Sharing debug logs", + link: "/photos/troubleshooting/sharing-logs", + }, + ], + }, + ], + }, + { + text: "Auth", + items: [ + { text: "Introduction", link: "/auth/" }, + { text: "FAQ", link: "/auth/faq/" }, + { + text: "Migration guides", + collapsed: false, + items: [ + { text: "Introduction", link: "/auth/migration-guides/" }, + { + text: "From Authy", + link: "/auth/migration-guides/authy/", + }, + { + text: "Exporting your data", + link: "/auth/migration-guides/export", + }, + ], + }, + ], + }, + { + text: "Self hosting", + collapsed: true, + items: [ + { text: "Getting started", link: "/self-hosting/" }, + { + text: "Guides", + items: [ + { text: "Introduction", link: "/self-hosting/guides/" }, + { + text: "Connect to custom server", + link: "/self-hosting/guides/custom-server/", + }, + { + text: "Administering your server", + link: "/self-hosting/guides/admin", + }, + + { + text: "Mobile build", + link: "/self-hosting/guides/mobile-build", + }, + { + text: "System requirements", + link: "/self-hosting/guides/system-requirements", + }, + { + text: "Using external S3", + link: "/self-hosting/guides/external-s3", + }, + ], + }, + { + text: "FAQ", + items: [ + { + text: "Verification code", + link: "/self-hosting/faq/otp", + }, + { + text: "Increase storage space", + link: "/self-hosting/faq/storage-space", + }, + ], + }, + { + text: "Troubleshooting", + items: [ + { + text: "Yarn", + link: "/self-hosting/troubleshooting/yarn", + }, + ], + }, + ], + }, + { + text: "About", + link: "/about/", + }, + { + text: "Contribute", + link: "/about/contribute", + }, +]; function sidebarOld() { return [ { text: "Welcome", items: [ - { - text: "About", - collapsed: true, - link: "/about/company", - items: [ - { text: "Company", link: "/about/company" }, - { text: "Products", link: "/about/products" }, - { text: "Plans", link: "/about/plans" }, - { text: "Support", link: "/about/support" }, - { text: "Community", link: "/about/community" }, - { text: "Open source", link: "/about/open-source" }, - { text: "Contribute", link: "/about/contribute" }, - ], - }, { text: "Features", collapsed: true, diff --git a/docs/docs/about/contribute.md b/docs/docs/about/contribute.md new file mode 100644 index 000000000..3addd2e60 --- /dev/null +++ b/docs/docs/about/contribute.md @@ -0,0 +1,15 @@ +--- +title: Contribute +description: Details about how to contribute to Ente's docs +--- + +# Contributing + +To contribute to these docs, you can use the "Edit this page" button at the +bottom of each page. This will allow you to directly edit the markdown file that +is used to generate this documentation and open a quick pull request directly +from GitHub's UI. + +If you're more comfortable in contributing with your text editor, see the +`docs/` folder of our GitHub repository, +[github.com/ente-io/ente](https://github.com/ente-io/ente). diff --git a/docs/docs/about/index.md b/docs/docs/about/index.md index d52fbf1c9..8cffbe048 100644 --- a/docs/docs/about/index.md +++ b/docs/docs/about/index.md @@ -5,19 +5,26 @@ description: > that we make. --- -Ente is a platform for privately, reliably, and securely storing your data on -the cloud. On top of this platform, Ente offers two products currently: +# About -* Ente Photos - An alternative to Google Photos and Apple Photos +Ente is a end-to-end encrypted platform for privately, reliably, and securely +storing your data on the cloud. On top of this platform, Ente offers two +products: -* Ente Auth - A (free!) app for storing your 2FA codes. +- **Ente Photos** - An alternative to Google Photos and Apple Photos -and more products are in the pipeline. +- **Ente Auth** - A free 2FA alternative to Authy + +Both these apps are available for all desktop (Linux, Mac, Windows) and mobile +(Android, iOS and F-Droid) platforms. They also work directly in your web +browser without you needing to install anything. + +More products are in the pipeline. ## History -Ente was the founded by Vishnu Mohandas, who is also the Ente's CEO, in response -to privacy concerns with major tech companies. The underlying motivation was the +Ente was the founded by Vishnu Mohandas (he's also Ente's CEO) in response to +privacy concerns with major tech companies. The underlying motivation was the understanding that big tech had no incentive to fix their act, but with end-to-end encrypted cross platform apps, there was a way for people to take back control over their own data without sacrificing on features. @@ -29,9 +36,9 @@ has the literal meaning "my photos". This was a good name, but still Vishnu looked around for better ones. But one day, he discovered that "ente" means "duck" in German. This unexpected -connection sealed the deal! We should ask him why he likes ducks so much, but +connection sealed the deal. We should ask him why he likes ducks so much, but apparently he does, so this dual meaning ("mine" / "duck") led him to finalize -the name, and also led to the adoption of the duck as Ente's mascot, "Ducky": +the name, and also led to the adoption of "Ducky", Ente's mascot:
@@ -43,80 +50,20 @@ the name, and also led to the adoption of the duck as Ente's mascot, "Ducky": en-_tay_. Like cafe. ---- +## Get in touch -# Products +If you have a support query that is not answered by these docs, please reach out +to our Customer Support by sending an email to support@ente.io -Ente currently offers Photo and Auth. Additionally, there are some other -products (Lockers and Legacy) that are being considered. +To stay up to date with new product launches, and behind the scenes details of +how we're building Ente, you can read our [blog](https://ente.io/blog) (or +subscribe to it via [RSS](https://ente.io/blog/rss.xml)) -## Ente Photos - -Ente Photos goes beyond traditional cloud storage, prioritizing your privacy and -the safety of your cherished memories. All your photos, along with their -metadata, are stored end-to-end encrypted, ensuring that only you have access to -your data. - -Ente preserves your encrypted photos across three different clouds in three -separate locations, including an underground fallout shelter. This multi-layered -backup strategy provides a high level of reliability. - -Ente photos is available for Android, iOS, Linux, Mac, Windows and the web. - -# Ente Auth - -Ente auth is ust an authenticator app; it's an open-source initiative -dedicated to securing your 2FA secrets. Now, you can backup and view your -two-factor authentication secrets seamlessly. find more information about the -project on GitHub at github.com/ente-io/auth. - -As a token of gratitude to the community that has supported us, Ente Auth is -offered as a free service. If in the future we convert this to a paid service, -existing users will be grandfathered in. - -Ente auth is available on Android, iOS, and the web - -# Connect with Ente - -## Customer support - -Connect with our support team for swift assistance and expert guidance email us -@support@ente.io. -Reach out to our dev team @team@ente.io, even our CEO and CTO personally -responds here. - -## Blog - -As Ente continues to evolve, So does our story. Follow our blog @ -https://ente.io/blog As Ente undergoes continuous growth and development, our -narrative unfolds. Explore our blog for exclusive company updates that offer an -insider's perspective. Regularly visit the Ente blog to maintain your -connection, stay well informed, and draw insipration. - -## Roadmap - -You plays a pivotal role in shaping the future direction of Ente's product, and -we invite you to be an integral part of it. - -Take a look at our roadmap to see where we're headed -https://roadmap.ente.io/roadmap/ - -# Community - -#### Join our vibrant community and stay updated on all things on Ente! Follow us on various platforms for the latest news, updates, and engaging content - -#### Discord - -Join our Discord for real-time discussions, solutions to queries, and a thriving -camaraderie. Stay updated on all things on Ente! - -🐦 Twitter: https://twitter.com/enteio - -🔗 Reddit: https://www.reddit.com/r/enteio - -📸 Instagram: https://www.instagram.com/ente.app/ - -🐘 Mastodon: https://mstdn.social/@ente - -🔗 LinkedIn: https://www.linkedin.com/company/ente-io/ +To suggest new features and/or offer your perspective on how we should design +planned and upcoming features, use our +[GitHub discussions](https://github.com/ente-io/ente/discussions) +Or if you'd just like to hang out, join our +[Discord](https://discord.gg/z2YVKkycX3), follow us on +[Twitter](https://twitter.com/enteio) or give us a shout out on +[Mastodon](https://mstdn.social/@ente) diff --git a/docs/docs/auth/faq/index.md b/docs/docs/auth/faq/index.md new file mode 100644 index 000000000..23564e2e3 --- /dev/null +++ b/docs/docs/auth/faq/index.md @@ -0,0 +1,33 @@ +--- +title: FAQ - Auth +description: Frequently asked questions about Ente Auth +--- + +# Frequently Asked Questions + +### How secure is Ente Auth? + +All codes you backup via Ente is stored end-to-end encrypted. This means only +you can access your codes. Our apps are open source and our cryptography has +been externally audited. + +### Can I access my codes on desktop? + +You can access your codes on the web at [auth.ente.io](https://auth.ente.io). + +### How can I delete or edit codes? + +You can delete or edit a code by swiping left on that item. + +### How can I support this project? + +You can support the development of this project by subscribing to our Photos app +at [ente.io](https://ente.io). + +### How can I enable FaceID lock in Ente Auth? + +You can enable FaceID lock under Settings → Security → Lockscreen. + +### Why does the desktop and mobile app displays different code? + +Please verify that the time on both your mobile and desktop is same. diff --git a/docs/docs/auth/index.md b/docs/docs/auth/index.md index 491d1d11c..8800c5422 100644 --- a/docs/docs/auth/index.md +++ b/docs/docs/auth/index.md @@ -3,13 +3,7 @@ title: Ente Auth description: User guide for Ente Auth --- -# Welcome to the Ente Auth's User Guide! +# Ente Auth -Ente Auth is a free, cross-platform, end-to-end encrypted authenticator app for -everyone. You can use it to safely store your 2FA codes (second-factor -authentication codes). - -> [!CAUTION] -> -> These docs are still incomplete. If you feel like contributing though, help us -> [fill them in](https://github.com/ente-io/ente/docs). +Ente Auth is a free, cross-platform, end-to-end encrypted authenticator app. You +can use it to safely store your 2FA codes (second-factor authentication codes). diff --git a/docs/docs/auth/migration-guides/authy/index.md b/docs/docs/auth/migration-guides/authy/index.md new file mode 100644 index 000000000..7a938bfa1 --- /dev/null +++ b/docs/docs/auth/migration-guides/authy/index.md @@ -0,0 +1,175 @@ +--- +title: Migrating from Authy +description: Guide for importing your existing Authy 2FA tokens into Ente Auth +--- + +# Migrating from Authy + +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 +> recommend you export your codes as soon as you can. + +--- + +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. + +> [!CAUTION] +> +> Under any circumstances, do **NOT** share any JSON and TXT files generated +> using this guide, as they contain your **unencrypted** TOTP secrets! +> +> Also, there is **NO GUARANTEE** that these methods will export ALL of your +> codes. Make sure that all your accounts have been imported successfully before +> deleting any codes from your Authy account! + +--- + +## Method 1: Use Neeraj's export tool + +**Who should use this?** General users who want to save time by skipping the +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 export from Authy, download the tool for your specific OS, then type the +following in your terminal: + +``` +./ +``` + +Assuming the filename of the binary remains unmodified and the working directory +of the terminal is the location of the binary, you should type this for MacOS: + +> [!NOTE] On Apple Silicon devices, Rosetta 2 may be required to run the binary. + +``` +./authy-export-darwin-amd64 authy_codes.txt +``` + +For Linux: + +``` +./authy-export-linux-amd64 authy_codes.txt +``` + +For Windows: + +``` +./authy-export-windows-amd64.exe authy_codes.txt +``` + +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 +Authenticator! + +## Method 2: Use gboudreau's GitHub guide + +**Who should use this?** Power users who have spare time on their hands and +prefer a more "known and established" solution to exporting Authy codes.

+ +A user on GitHub (gboudreau) wrote a guide to export codes from Authy (morpheus +on Discord found this and showed it to us), so we are going to be using that for +the migration. + +To export your data, please follow +[this guide](https://gist.github.com/gboudreau/94bb0c11a6209c82418d01a59d958c93). + +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! + +### 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 +> 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 +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 +Authenticator can use, so you can try importing using plain text import instead. + +You can download my program +[here](https://github.com/gweeeen/ducky/blob/main/duckys_other_stuff/authy_to_ente.py), +or you can copy the program below: + +```py +import json +import os + +totp = [] + +accounts = json.load(open('authy-to-bitwarden-export.json','r',encoding='utf-8')) + +for account in accounts['items']: + totp.append(account['login']['totp']+'\n') + +writer = open('auth_codes.txt','w+',encoding='utf-8') +writer.writelines(totp) +writer.close() + +print('Saved to ' + os.getcwd() + '/auth_codes.txt') +``` + +To convert the file with this program, you will need to install +[Python](https://www.python.org/downloads/) on your computer. + +Before you run the program, make sure that both the Python program and the JSON +file are in the same directory, otherwise this will not work! + +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! + +--- + +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 +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) + +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 + "Data", then press "Import codes". +4. Select the "Plain text" option. +5. Select the TXT file that was made earlier. + +## Importing to ente Authenticator (Method 2) + +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 + "Data", then press "Import codes". +4. Select the "Bitwarden" option. +5. Select the JSON file that was made earlier. + +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 +Authenticator. + +Now that your secrets are safely stored, I recommend you delete the unencrypted +JSON and TXT files that were made during the migration process for security. diff --git a/docs/docs/auth/migration-guides/export.md b/docs/docs/auth/migration-guides/export.md new file mode 100644 index 000000000..a66bea7b6 --- /dev/null +++ b/docs/docs/auth/migration-guides/export.md @@ -0,0 +1,76 @@ +--- +title: Exporting your data from Ente Auth +description: Guide for exporting your 2FA codes out from Ente Auth +--- + +# Exporting your data out of Ente Auth + +## Auth Encrypted Export format + +### Overview + +When we export the auth codes, the data is encrypted using a key derived from +the user's password. This document describes the JSON structure used to organize +exported data, including versioning and key derivation parameters. + +### Export JSON Sample + +```json +{ + "version": 1, + "kdfParams": { + "memLimit": 4096, + "opsLimit": 3, + "salt": "example_salt" + }, + "encryptedData": "encrypted_data_here", + "encryptionNonce": "nonce_here" +} +``` + +The main object used to represent the export data. It contains the following +key-value pairs: + +- `version`: The version of the export format. +- `kdfParams`: Key derivation function parameters. +- `encryptedData"`: The encrypted authentication data. +- `encryptionNonce`: The nonce used for encryption. + +#### Version + +Export version is used to identify the format of the export data. + +##### Ver: 1 + +- KDF Algorithm: `ARGON2ID` +- Decrypted data format: `otpauth://totp/...`, separated by a new line. +- Encryption Algo: `XChaCha20-Poly1305` + +##### Key Derivation Function Params (KDF) + +This section contains the parameters that were using during KDF operation: + +- `memLimit`: Memory limit for the algorithm. +- `opsLimit`: Operations limit for the algorithm. +- `salt`: The salt used in the derivation process. + +##### Encrypted Data + +As mentioned above, the auth data is encrypted using a key that's derived by +using user provided password & kdf params. For encryption, we are using +`XChaCha20-Poly1305` algorithm. + +## How to use the exported data + +- **Ente Authenticator app**: You can directly import the codes in the Ente + Authenticator app. + + > Settings -> Data -> Import Codes -> ente Encrypted export. + +- **Decrypt using Ente CLI** : Download the latest version of + [Ente CLI](https://github.com/ente-io/ente/releases?q=tag%3Acli-v0), and run + the following command + +``` + ./ente auth decrypt +``` diff --git a/docs/docs/auth/migration-guides/index.md b/docs/docs/auth/migration-guides/index.md new file mode 100644 index 000000000..f10d9db41 --- /dev/null +++ b/docs/docs/auth/migration-guides/index.md @@ -0,0 +1,10 @@ +--- +title: Migrating to Ente Auth +description: + Guides for migrating your existing 2FA tokens into or out of Ente Auth +--- + +# Migrating to/from Ente Auth + +- [Migrating from Authy](authy/) +- [Exporting your data out of Ente Auth](export) diff --git a/docs/docs/auth/support/contribute.md b/docs/docs/auth/support/contribute.md deleted file mode 100644 index dd208e1f3..000000000 --- a/docs/docs/auth/support/contribute.md +++ /dev/null @@ -1,8 +0,0 @@ -## Translation - -## Icons - -## Support Development - -If you wish to support the development of the project, please consider switching -to paid plan of [Ente Photos](https://ente.io) diff --git a/docs/docs/de/auth/index.md b/docs/docs/de/auth/index.md index 3dfee073d..1948a9126 100644 --- a/docs/docs/de/auth/index.md +++ b/docs/docs/de/auth/index.md @@ -3,11 +3,11 @@ title: Ente Auth description: Ente Auth-Benutzerhandbuch --- -# Willkommen beim Ente Auth-Benutzerhandbuch! +# Ente Auth Ente Authenticator ist eine kostenlose, plattformübergreifende, Ende-zu-Ende-verschlüsselte Authenticator-App für jedermann. Wir sind froh, dass du hier bist! **Please note that this German translation is currently just a placeholder.** -Know German? [Help us fill this in!](https://github.com/ente-io/ente/docs). +Know German? [Help us fill this in!](/about/contribute). diff --git a/docs/docs/index.md b/docs/docs/index.md new file mode 100644 index 000000000..af5fd2cdd --- /dev/null +++ b/docs/docs/index.md @@ -0,0 +1,15 @@ +--- +title: Home +--- + +# Welcome! + +This site contains documentation and help for Ente Photos and Ente Auth. It +describes various features, and also offers various troubleshooting suggestions. + +Use the **sidebar** menu to navigate to information about the product (Photos or +Auth) you'd like to know more about. Or use the **search** at the top to try and +jump directly to page that might contain the information you need. + +To know more about Ente, see [about](/about/) or visit our website +[ente.io](https://ente.io). diff --git a/docs/docs/photos/faq/faq.md b/docs/docs/photos/faq/faq.md deleted file mode 100644 index e69de29bb..000000000 diff --git a/docs/docs/photos/faq/index.md b/docs/docs/photos/faq/index.md new file mode 100644 index 000000000..c5a4e2cf9 --- /dev/null +++ b/docs/docs/photos/faq/index.md @@ -0,0 +1,9 @@ +--- +title: FAQ +description: Frequently asked questions about Ente Photos +--- + +# FAQ + +_Coming soon_. On this page we'll document some help items in a question and +answer format. diff --git a/docs/docs/photos/features/albums.md b/docs/docs/photos/features/albums.md index 18d9b4722..c368e6cdf 100644 --- a/docs/docs/photos/features/albums.md +++ b/docs/docs/photos/features/albums.md @@ -1,67 +1,81 @@ +--- +title: Albums +description: Using albums in Ente Photos +--- + # Albums -This guide will show you how to make the most of your albums with simple yet -effective features. Below are the features that allow you to personailze your -albums according to your preferences: +Make the most of your albums and personalize them to your preferences with these +simple yet effective features. -## 1. Rename album: Personalize your albums by giving them a meaningful name +## Rename album -### How to Rename an album on your mobile? +Personalize your albums by giving them a meaningful name. + +### How to rename an album on your mobile? - Open the album - Tap the three dots button in the top right corner of the screen -- Tap rename album, then type in a new name -- Tap on Rename button +- Tap _Rename album_, then type in a new name +- Tap on _Rename_ button -### How to Rename an album on your Desktop? +### How to rename an album on your web / desktop? - Open the album - Click on the overflow menu in the top right corner -- Click the Rename album +- Click the _Rename album_ button - Type in a new name -- Click on Rename or Press enter +- Click on _Rename_ or press enter -## 2. Set cover: Select any photo you want to use as the cover for your album. +## Set album cover + +Select any photo you want to use as the cover for your album. ### How to set an album cover on mobile? - Open the album you want to change - Tap the three dots button in the top right corner -- From the menu options, select Set cover +- From the menu options, select _Set cover_ - A new screen will appear, propmpting you to select the cover photo - Browse through your photos and tap on the image you want to set as the album cover -- Then tap on Use selected photo +- Then tap on _Use selected photo_ -## 3. Map: Explore the memories based on their location +## View your photos on a map -### How to explore the album's photo in map view? +Explore your memories based on their location. + +### How to explore the album's photos in map view? - Open the album - Tap on the three dots button in the top right corner -- Select map -- View all photos of the album in map view +- Select _Map_ +- This will show all photos of the album in a map view -## 4. Sort by: Maintain order in your albums by arranging them from the newest to the oldest +## Sort albums -### How to sort by on mobile? +Maintain order in your albums by arranging them from the newest to the oldest. + +### How to change the sort order on mobile? - Open the album - Tap on the three dots button in the top right corner -- Select sort by -- Tap on the Newst first for the latest, Or Oldest first for the oldest +- Select _Sort by_ +- Tap on the _Newest first_ for the latest, Or _Oldest first_ for the oldest -### How to sort by on desktop? +### How to change the sort order on web / desktop? - Open the album - Click on the three dots button in the top right corner -- Click sort by -- Click on the Newest first for the latest, Or oldest first for the oldest +- Click _Sort by_ +- Tap on the _Newest first_ for the latest, Or _Oldest first_ for the oldest -## 5. Pin album: Keep your favorite album at the top by pinning them for quick access. +## Pin albums -### How to Pin/Unpin an album on Mobile? +Keep your favorite album at the top by pinning them for quick access. + +### How to pin/unpin an album on mobile? - Open the album - Tap on the three dots button in the top right corner -- Tap on Pin album/Unpin album +- Tap on _Pin album_ / _Unpin album_ diff --git a/docs/docs/photos/features/archive.md b/docs/docs/photos/features/archive.md index 5464bf15a..87d5a4ff2 100644 --- a/docs/docs/photos/features/archive.md +++ b/docs/docs/photos/features/archive.md @@ -1,3 +1,10 @@ +--- +title: Archive +description: | + Archiving photos and albums in Ente Photos to remove them from your home + timeline +--- + # Archive You can remove a photo (owned by you) or an album from your **home timeline** by @@ -8,30 +15,30 @@ mobile app. when some of the photos are also present in a non-archived album. - You can archive albums that are owned by you or shared with you. - Search results will include archived photos. If you want to hide photos from - search result, use [Hide](./hidden.md) feature. + search result, use [Hide](./hide) feature. -### How to +## How to -#### Archive Album +### Archive Album - Open the album - Click on the overflow menu - Click on Archive album -#### Archive Photo +### Archive Photo - Long press to select the photo - Select Archive option from the bottom menu. -#### View Archived Photos and Albums +### View Archived Photos and Albums -**Mobile** +#### Mobile - Go to Albums tab - Scroll down to bottom - Click on Archive button. -**Desktop** +#### Web / Desktop - Click on the topleft hamburger menu - Click on Archive @@ -39,8 +46,8 @@ mobile app. ### Metadata Privacy Both Ente and the person with whom you are sharing an album or photo have no -information about whether you have +information about whether you have: - Archived a photo - Archived an album -- Archived a shared album. +- Archived a shared album diff --git a/docs/docs/photos/features/cast.md b/docs/docs/photos/features/cast.md deleted file mode 100644 index faec1c9db..000000000 --- a/docs/docs/photos/features/cast.md +++ /dev/null @@ -1,36 +0,0 @@ -# Cast - -With ente Cast, you can play a slideshow of your favourite albums on your Google -Chromecast TVs or other Internet-connected large screen devices. - -## Get Started - -1. Open ente on the web or on your mobile device. -2. Select the album you want to play on your large screen device. -3. Click "Play album on TV" in the album menu. - -On the web, you can find this menu in the balls menu on the right hand side. -![Balls menu](/assets/cast/web-play-album-on-tv.webp) - -4. Choose how you want to pair your device with your large screen device. - ![Pairing options](/assets/cast/web-pairing-options.webp) - -On Chromium browsers, you will see a button labeled "Auto Pair". This option -will prompt you to select a Chromecast supported device on your local network. -Note: this option requires connecting to Google servers to load necessary -dependencies. This option does not transmit any sensitive data through Google -servers, such as your photos. Once your Chromecast device is connected, you're -all set. - -On all browsers, you'll see the option to "Pair with PIN". This option works -with all devices, Chromecast-enabled or not. You'll be required to load up -[cast.ente.io](https://cast.ente.io) on your large screen device. - -5. Enter the PIN displayed on your large screen device into the input field on - your mobile or web device. - -On your large screen device, you'll see the following screen. -![Pairing screen](/assets/cast/tv-pairing-screen.webp) - -6. If you entered your PIN correctly, you'll see a screen on your TV with a - green checkmark confirming the connection. diff --git a/docs/docs/photos/features/cast/index.md b/docs/docs/photos/features/cast/index.md new file mode 100644 index 000000000..89dc801f6 --- /dev/null +++ b/docs/docs/photos/features/cast/index.md @@ -0,0 +1,65 @@ +--- +title: Archive +description: | + Archiving photos and albums in Ente Photos to remove them from your home + timeline +--- + +> [!CAUTION] +> +> This is preview documentation for an upcoming feature. This feature has not +> yet been released yet, so the steps below will not work currently. + +# Cast + +With Ente Cast, you can play a slideshow of your favourite albums on your Google +Chromecast TVs or other Internet-connected large screen devices. + +## Get Started + +1. Open ente on the web or on your mobile device. +2. Select the album you want to play on your large screen device. +3. Click "Play album on TV" in the album menu. + + On the web, you can find this option in the three dots menu on the right + hand side. + +
+ +![Album options menu](web-play-album-on-tv.webp){width=300px} + +
+ +4. Choose how you want to pair your device with your large screen device. + +
+ + ![Pairing options](web-pairing-options.webp){width=300px} + +
+ + On Google Chrome and other Chromium browsers, you will see a button labeled + "Auto Pair". This option will prompt you to select a Chromecast supported + device on your local network. Note: this option requires connecting to + Google servers to load necessary dependencies. This option does not transmit + any sensitive data through Google servers, such as your photos. Once your + Chromecast device is connected, you're all set. + + On all browsers, you'll see the option to "Pair with PIN". This option works + with all devices, Chromecast-enabled or not. You'll be required to load up + [cast.ente.io](https://cast.ente.io) on your large screen device. + +5. Enter the PIN displayed on your large screen device into the input field on + your mobile or web device. + + On your large screen device, you'll see the following screen. + +
+ +![Pairing screen](tv-pairing-screen.webp) + +
+ +6. Once you enter the correct PIN, you will see a screen on your TV with a green + checkmark confirming the connection. Your photos will start showing up in a + bit. diff --git a/docs/docs/public/assets/cast/tv-pairing-screen.webp b/docs/docs/photos/features/cast/tv-pairing-screen.webp similarity index 100% rename from docs/docs/public/assets/cast/tv-pairing-screen.webp rename to docs/docs/photos/features/cast/tv-pairing-screen.webp diff --git a/docs/docs/public/assets/cast/web-pairing-options.webp b/docs/docs/photos/features/cast/web-pairing-options.webp similarity index 100% rename from docs/docs/public/assets/cast/web-pairing-options.webp rename to docs/docs/photos/features/cast/web-pairing-options.webp diff --git a/docs/docs/public/assets/cast/web-play-album-on-tv.webp b/docs/docs/photos/features/cast/web-play-album-on-tv.webp similarity index 100% rename from docs/docs/public/assets/cast/web-play-album-on-tv.webp rename to docs/docs/photos/features/cast/web-play-album-on-tv.webp diff --git a/docs/docs/photos/features/collect.md b/docs/docs/photos/features/collect.md index 829775043..7f08d8ce6 100644 --- a/docs/docs/photos/features/collect.md +++ b/docs/docs/photos/features/collect.md @@ -1,4 +1,11 @@ -# Collect photos: Collecting memories from events is now a breeze! +--- +title: Collect +description: Collecting photos from others using Ente Photos +--- + +# Collect photos + +Collecting memories from events is now a breeze! - Whether it's a birthday party, vacation trip or wedding, easily share your album using a unique, secure, end-to-end encrypted link. @@ -6,23 +13,21 @@ contribute without an ente account. - This allows them to effortlessly add, view, and download photos from the link without an ente account. -- Also preserves metadata and photo quality +- Also preserves metadata and photo quality. -## How to Collect photos on mobile? +## How to collect photos on mobile? - Open the album you want to share with - Tap on the Share album icon in the top right corner of the screen -- Select 'Collect photos' -- Tap 'Copy link' -- The link has been copied to your clipboard. Now, feel free to share it +- Select _Collect photos_ +- Tap _Copy link_ +- The link will get copied to your clipboard. Now, feel free to share it -## How to Collect photos on Web/Desktop? - -To collect photos on the web/desktop: +## How to Collect photos on web / desktop? - Open the album - Click on the share album icon -- Select Collect photos -- Click on Copy link -- The link has been copied to your clipboard. Share it effortlessly with +- Select _Collect photos_ +- Click on _Copy link_ +- The link will get copied to your clipboard. Share it effortlessly with others! diff --git a/docs/docs/photos/features/family-plan.md b/docs/docs/photos/features/family-plans.md similarity index 83% rename from docs/docs/photos/features/family-plan.md rename to docs/docs/photos/features/family-plans.md index cda8382aa..8405c7dcf 100644 --- a/docs/docs/photos/features/family-plan.md +++ b/docs/docs/photos/features/family-plans.md @@ -1,9 +1,15 @@ +--- +title: Family plans +description: + Share your Ente Photos plan with your family members with no extra cost +--- + # Family plans Paid subscribers of Ente can share the storage with their family, **at no additional cost** as you have already paid for the storage. -In breif, +In brief, - Your family members can use storage space from your plan without paying extra. diff --git a/docs/docs/photos/features/hidden.md b/docs/docs/photos/features/hide.md similarity index 56% rename from docs/docs/photos/features/hidden.md rename to docs/docs/photos/features/hide.md index 46a99ec96..aa88f0b64 100644 --- a/docs/docs/photos/features/hidden.md +++ b/docs/docs/photos/features/hide.md @@ -1,59 +1,66 @@ -# Hidden +--- +title: Hiding photos and albums +description: Hide photos and albums in Ente Photos +--- + +# Hiding photos and albums You can further protect extra sensitive photos or albums by hiding them. Hidden photos or albums will **only be viewable** after an extra layer of -authentication inside the app. Hidden differs from [Archive](./archive.md) in -the fact that hidden photos won't be surfaced anywhere in the app without -explicit authentication, whereas Archive only removes them from the home -timeline and memories sections. +authentication inside the app. Hiding differs from [Archiving](./archive.md) in +that the hidden photos won't be surfaced anywhere in the app without explicit +authentication, whereas archiving only removes them from the home timeline and +memories sections. - Hidden photos and albums will be removed from home timelines, memories, albums tab, search results, and any other visable place in the app. + - Hidden photos will be removed from all regular albums. If you want to unhide again you will have to specify the album to move it to. + - You cannot hide photos or albums shared with you. You can archive shared albums instead. -### How to +## How to -#### Hide album +### Hide album - Open the album - Click on the overflow menu -- Click on Hide album +- Click on _Hide album_ -#### Hide photo +### Hide photo - Select the photo -- Click on Hide option from the select menu +- Click on _Hide_ option from the select menu -#### View hidden photos and albums +### View hidden photos and albums -_Mobile_: +#### Mobile - Go to Albums tab - Scroll down to bottom -- Click on Hidden button +- Click on _Hidden_ button - Authenticate in app -_Desktop_: +#### Web / Desktop - Click on the topleft hamburger menu -- Click on Hidden +- Click on _Hidden_ - Authenticate in app -#### Unhide album +### Unhide album - Open the hidden album - Click on the overflow menu -- Click on Unhide album +- Click on _Unhide album_ -#### Unhide photo +### Unhide photo - Select the hidden photo -- Click on Unhide option from the select menu +- Click on _Unhide_ option from the select menu - Click on the album the photo should be restored to -### Metadata Privacy +## Metadata Privacy Ente has no information about whether you have hidden a photo or album. diff --git a/docs/docs/photos/features/live-photos.md b/docs/docs/photos/features/live-photos.md deleted file mode 100644 index e69de29bb..000000000 diff --git a/docs/docs/photos/features/location-tags.md b/docs/docs/photos/features/location-tags.md new file mode 100644 index 000000000..c81a60b27 --- /dev/null +++ b/docs/docs/photos/features/location-tags.md @@ -0,0 +1,43 @@ +--- +title: Location tags +description: Search and organize your photos by location in Ente Photos +--- + +# Location Tags + +_Search and organize your photos by their location_ + +Location Tags allow you to search and organize your photos based on their +geographical location. Instead of sending your queries to our servers, we've +implemented a privacy-friendly solution that runs searches locally on your +device. + +## How to use Location Tags + +### Method 1: Adding Location Tags from a Photo + +1. Open a photo +2. Click on _Info_ +3. Select _Add Location_" +4. Enter the location name and define a radius + +### Method 2: Adding Location Tags from the Search Tab + +1. Go to the search tab +2. Click on _Add new_ at the end of the location tags section +3. Select a photo as the center point for the location tag +4. Enter the location name and define a radius + +## Tips + +- The app will automatically cluster photos falling within a radius under a + specified location. + +- Location Tags provide a seamless way to filter photos without compromising + your privacy. + +- Location tags are stored end-to-end encrypted, ensuring the security of your + data. + +- Enjoy a more organized photo library by tagging important places like home, + office, and vacation spots. diff --git a/docs/docs/photos/features/location.md b/docs/docs/photos/features/location.md deleted file mode 100644 index 9dfcfbdd7..000000000 --- a/docs/docs/photos/features/location.md +++ /dev/null @@ -1,35 +0,0 @@ -# Location Tags: Search and Organize Your Photos - -## Overview: - -The Location Tags feature allows you to efficiently search and organize your -photos based on their geographical location. Instead of sending your queries to -our servers, we've implemented a privacy-friendly solution that runs searches -locally on your device. - -## How to Use Location Tags: - -### Method 1: Adding Location Tags from a Photo - -1. Open a photo. -2. Click on "Info." -3. Select "Add Location." -4. Enter the location name and define a radius. - -### Method 2: Adding Location Tags from the Search Tab - -1. Go to the search tab. -2. Click on "Add new" at the end of the location tags section. -3. Select a photo as the center point for the location tag. -4. Enter the location name and define a radius. - -## Note: - -- The app will automatically cluster photos falling within a radius under a - specified location. -- Location Tags provide a seamless way to filter photos without compromising - your privacy. -- Location tags are stored end-to-end encrypted, ensuring the security of your - data. -- Enjoy a more organized photo library by tagging important places like home, - office, and vacation spots. diff --git a/docs/docs/photos/features/map.md b/docs/docs/photos/features/map.md index 3890b1c47..01c5b6445 100644 --- a/docs/docs/photos/features/map.md +++ b/docs/docs/photos/features/map.md @@ -1,34 +1,39 @@ -# Map : View and explore your photos on the map +--- +title: Maps +description: View and explore your photos on a map within Ente Photos +--- + +# Map + +_View and explore your photos on the map_ ## How can I view photos on the map on mobile? -- Step 1. Find the search icon located at the bottom right corner of your - screen. -- Step 2. Tap on the globe icon (Your map) withing the location -- Step 3. Enter the map view and start exploring your photos from around the - world. +- Find the search icon located at the bottom right corner of your screen. +- Tap on the globe icon (Your map) withing the location +- Enter the map view and start exploring your photos from around the world. ## How to enable map on your mobile app? -- Step 1. Tap the three horizontal lines located at the top left corner of - your home screen or swipe left on the home screen. -- Step 2. Select "General" settings. -- Step 3. Enter the "Advanced" settings. -- Step 4. Use the toggle switch to turn the map feature on or off +- Tap the three horizontal lines located at the top left corner of your home + screen or swipe left on the home screen. +- Select _General_ settings. +- Enter the _Advanced_ settings. +- Use the toggle switch to turn the map feature on or off. ## How to view Album photos on the map? -- Step 1. Open the album containing the photos you want to view -- Step 2. Tap on the three horizontal lines located in the top right corner of - the screen. -- Step 3. Select "Map" from the options. -- Step 4. View and explore your photos on the map. +- Open the album containing the photos you want to view +- Tap on the three horizontal lines located in the top right corner of the + screen. +- Select _Map_ from the options. +- View and explore your photos on the map. ## How to enable map on desktop? -- Step 1. Click on the three horizontal lines located in the top left corner - of the app. -- Step 2. Navigate to "preferences" from the menu. -- Step 3. Select "Advanced" in the preference menu. -- Step 4. Click on "Map" to access map settings. -- Step 5. Toggle the map settings on and off based on your preferences. +- Click on the three horizontal lines located in the top left corner of the + app. +- Navigate to _Preferences_ from the menu. +- Select _Advanced_ in the preferences menu. +- Click on _Map_ to access map settings. +- Toggle the map settings on and off based on your preferences. diff --git a/docs/docs/photos/features/public-links.md b/docs/docs/photos/features/public-links.md index 2afce455e..7f079b82f 100644 --- a/docs/docs/photos/features/public-links.md +++ b/docs/docs/photos/features/public-links.md @@ -1,3 +1,10 @@ +--- +title: Public links +description: + Share photos with your friends and family without them needing to install + Ente Photos +--- + # Public Links Ente lets you share your photos via links, that can be accessed by anyone, diff --git a/docs/docs/photos/features/quick-link.md b/docs/docs/photos/features/quick-link.md index 286b6d282..e32863f0f 100644 --- a/docs/docs/photos/features/quick-link.md +++ b/docs/docs/photos/features/quick-link.md @@ -1,12 +1,22 @@ +--- +title: Quick links +description: Share photos with your friends and family without creating albums +--- + # Quick Links -Quick links allows you to select single or multiple photos & create a link. -Behind the scene, ente creates a special album and put all the selected files in -that special album. +Quick links allows you to select single or multiple photos and create a link +that you can then share. You don't need to create an album first. + +> Behind the scene, Ente creates a special album and put all the selected files +> in that special album. - You can view all quick links created by you in the sharing tab, under Quick links section. + - Quick links can be converted to regular album. -- Remove link will not delete the photos that are present in that link. -- Similar to [public-links](./public-links.md), you can set link expirty, set + +- Removing a link will not delete the photos that are present in that link. + +- Similar to [public-links](./public-links), you can set link expiry, passwords or device limits. diff --git a/docs/docs/photos/features/referral.md b/docs/docs/photos/features/referrals.md similarity index 53% rename from docs/docs/photos/features/referral.md rename to docs/docs/photos/features/referrals.md index f0818bb9b..085a0b19b 100644 --- a/docs/docs/photos/features/referral.md +++ b/docs/docs/photos/features/referrals.md @@ -1,6 +1,13 @@ -# Referral plan: Earn and Expand Your Storage +--- +title: Referral plan +description: + Earn and expand your storage by referring Ente Photos to your friends and + family +--- -## Overview: +# Referral plan + +_Earn and Expand Your Storage_ Did you know you can boost your storage on Ente simply by referring your friends? Our referral program lets you earn 10 GB of free storage for each @@ -13,29 +20,29 @@ On the Home Page: - Click on the hamburger menu in the top left corner - Open the sidebar -- Tap on 'General' -- Select Referrals +- Tap on _General_ +- Select _Referrals_ - Share the code with your friend or family Note: - Once your friend upgrades to a paid plan, both you and your friend receive - an additional 10 GB of storage -- Keep track of your earned storage and referral details on Claim free storage - screen + an additional 10 GB of storage. +- You can keep track of your earned storage and referral details on _Claim + free storage_ screen. - If you refer more friends than your plan allows, the extra storage earned - will be reserved until you upgrade your plan -- Earned storage remains accessible as long as your subscription is active + will be reserved until you upgrade your plan. +- Earned storage remains accessible as long as your subscription is active. -## How to apply Refferal code of a friend? +## How to apply referral code given by a friend? On the Home Page: - Click on the hamburger menu inthe top left corner -- Tap on 'General' from the options -- Select 'Referrals' from the menu -- Find and tap on 'Apply Code.' +- Tap on _General_ from the options +- Select _Referrals_ from the menu +- Find and tap on _Apply Code_ - Enter the referral code provided by your friend. -Note: Please note that referral codes should be applied within one month of -account creation to claim free storage. +Please note that referral codes should be applied within one month of account +creation to claim the free storage. diff --git a/docs/docs/photos/features/sharing.md b/docs/docs/photos/features/sharing.md new file mode 100644 index 000000000..9f32830ec --- /dev/null +++ b/docs/docs/photos/features/sharing.md @@ -0,0 +1,41 @@ +--- +title: Sharing +description: + Ente allows you to share albums and collaborate with your loved ones +--- + +# Sharing + +It is easy to share your albums on Ente, end-to-end encrypted. + +## Links + +You can create links to your albums by opening an album and clicking on the +Share icon. They are publicly accessible by anyone who you share the link with. +They don't need an app or account. + +These links can be password protected, or set to expire after a while. + +You can read more about the features supported by Links +[here](https://ente.io/blog/powerful-links/). + +## Albums + +If your loved ones are already on Ente, you can share an album with their +registered email address. + +If they are your partner, you can share your `Camera` folder on Android, or +`Recents` on iOS. Whenever you click new photos, they will automatically be +accessible on your partner's device. + +## Collaboration + +You can allow other Ente users to add photos to your album. This is a great way +for you to build an album together with someone. You can control access to the +same album - someone can be added as a `Collaborator`, while someone else as a +`Viewer`. + +If you wish to collect photos from folks who are not Ente, you can do so with +our Links. Simply tick the box that says "Allow uploads", and anyone who has +access to the link will be able to add photos to your album. +[Read more](https://ente.io/blog/collect-photos/) diff --git a/docs/docs/photos/features/trash.md b/docs/docs/photos/features/trash.md index f52ed676d..f324b48e9 100644 --- a/docs/docs/photos/features/trash.md +++ b/docs/docs/photos/features/trash.md @@ -1,7 +1,12 @@ +--- +title: Trash +description: Deleting items and trash +--- + # Trash -Whenever you delete an item from ente, it is moved to Trash. These items will be -automatically deleted from Trash after 30 days. You can manaully select or -completely empty the trash, if you wish. +Whenever you delete an item from Ente, it is moved to Trash. These items will be +automatically deleted from Trash after 30 days. You can manaully select photos +to permanently delete or completely empty the trash if you wish. Items in trash are included in your used storage calculation. diff --git a/docs/docs/photos/features/uncategorized.md b/docs/docs/photos/features/uncategorized.md index e73333c9a..235924f44 100644 --- a/docs/docs/photos/features/uncategorized.md +++ b/docs/docs/photos/features/uncategorized.md @@ -1,18 +1,24 @@ -## Uncategoried +--- +title: Uncategorized +description: Uncategorized items in Ente Photos +--- -"Uncategorized" is a special album type where photos are automatically added +# Uncategorized + +_Uncategorized_ is a special album type where photos are automatically added under the following circumstances: - When you remove a photo from the last album, it is relocated to - "Uncategorized." + _Uncategorized_ section. + - During album deletion, if you choose to keep photos but delete the album, - all photos exclusive to the current album are moved to the "Uncategorized" + all photos exclusive to the current album are moved to the _Uncategorized_ section. Note: This does not include photos uploaded by others. -### Clean up Uncategorized +### Cleaning up Uncategorized items -In the mobile app, you can click on the overflow menu and click -`Clean Uncategorized` option. All files that are also present in another album, -that is owned by the user, will be removed from the Uncategorized section. +In the mobile app, you can click on the overflow menu and click _Clean +Uncategorized_ option. All files that are also present in another album, that is +owned by the user, will be removed from the _Uncategorized_ section. diff --git a/docs/docs/photos/features/watch-folder.md b/docs/docs/photos/features/watch-folder.md index 52a33bd52..966f35be5 100644 --- a/docs/docs/photos/features/watch-folder.md +++ b/docs/docs/photos/features/watch-folder.md @@ -1,30 +1,36 @@ -# Watched Folders: Effortless Syncing +--- +title: Watch folder +description: Automatic syncing of certain folders in the Ente Photos desktop app +--- -## Overview: +# Watch folder -The ente desktop app now allows you to "watch" a folder on your computer for any -changes, creating a one-way sync from your device to the Ente cloud. This will -make photo management and backup a breeze. +_Automatic syncing_ + +The Ente desktop app allows you to "watch" a folder on your computer for any +changes, creating a one-way sync from your device to the Ente cloud. This is +intended to automate your photo management and backup. ## How to add Watch folders? - Click on the hamburger menu in the top left corner - Open the sidebar -- Select "Watch Folders" -- Choose "Add Watch Folders" -- Pick the folder from your system that you want to add as Watch folder +- Select _Watch Folders_ +- Choose _Add Watch Folders_ +- Pick the folder from your system that you want to add as a watched folder ## How to remove Watch folders? - Click on the hamburger menu in the top left corner - Open the sidebar -- Select "Watch Folders" +- Select _Watch Folders_ - Click on the three dots menu next to the folders on the right side -- Choose "Stop Watching" from the menu +- Choose _Stop Watching_ from the menu -# Note: +# Tips: + +- You will get an option to choose whether to sync nested folders to a single + album or separate albums. -- Option to choose whether to sync nested folders to a single album or - separate albums. - The app continuously monitors changes in the watched folder, such as the - addition or removal of files + addition or removal of files. diff --git a/docs/docs/photos/getting-started/index.md b/docs/docs/photos/getting-started/index.md deleted file mode 100644 index e69de29bb..000000000 diff --git a/docs/docs/photos/index.md b/docs/docs/photos/index.md index 7fd7214a1..2b5d2e752 100644 --- a/docs/docs/photos/index.md +++ b/docs/docs/photos/index.md @@ -1,42 +1,18 @@ -# Welcome to Help! +--- +title: Ente Photos +description: User guide for Ente Photos +--- -Welcome to Ente Photos Help! If you're new to Ente Photos, our -[Quick Start](./getting-started/index.md) and [FAQs](./faq/faq.md) are great -places to begin. +# Ente Photos -If you can’t find an answer, please [ask the community](https://ente.io/discord) -or write to **support@ente.io**. +Ente Photos is an end-to-end encrypted alternative to Google Photos and Apple +Photos. You can use it to safely and securely store your photos on the cloud. -To send a bug report or a feature suggestion, you can use -[Github Issues](https://github.com/ente-io/photos-app/issues). +While security and privacy form the bedrock of Ente Photos, it is not at the +cost of usability. The user interface is simple, and we are continuously working +to make it even simpler. The goal is a product that can be used by people with +all sorts of technical ability and background. -Feedback about this documentation can be shared on our -[Discord Server](https://ente.io/discord) in the **\#docs** channel. We would -love to hear your thoughts on anything we can fix or improve. - -## About Ente Photos - -[Ente Photos](https://ente.io) is a safe home for your photos and videos. - -You can store, share, and re-discover your moments with absolute privacy. - -## About Ente Auth - -[Ente Auth](https://ente.io/auth) is a secure, end-to-end encrypted 2FA app with -multi-platform sync. - -Learn more about Auth [here](../authenticator/). - -## Contributing - -The source for this documentation is available at -[github.com/ente-io/docs](https://github.com/ente-io/docs). - -Please see our -[contribution guide](https://github.com/ente-io/docs#contributing). We'd be -grateful for any fixes and improvements! - -Once your contributions are accepted, please add yourself to the list of -[Contributors](./misc/contributors.md). - -Thank you! +These help docs are divided into three sections: Features, FAQ and +Troubleshooting. Choose the relevant page from the sidebar menu, or use the +search at the top. diff --git a/docs/docs/photos/misc/contributors.md b/docs/docs/photos/misc/contributors.md deleted file mode 100644 index e69de29bb..000000000 diff --git a/docs/docs/photos/troubleshooting/files-not-uploading.md b/docs/docs/photos/troubleshooting/files-not-uploading.md index c3f538b4a..82c8bced0 100644 --- a/docs/docs/photos/troubleshooting/files-not-uploading.md +++ b/docs/docs/photos/troubleshooting/files-not-uploading.md @@ -1,13 +1,20 @@ +--- +title: Files not uploading +description: + Troubleshooting when files are not uploading from your Ente Photos app +--- + +# Files not uploading + ## Network Issue If you are using VPN, please try disabling the VPN or switching provider. -## Web/Desktop +## Web / Desktop -**Certain file types are not uploading** +### Certain file types are not uploading The desktop/web app tries to detect if a particular file is video or image. If -the detection fails, then the app skips the upload. Please share either the -sample file or logs with us @support.ente.io - -## Mobile +the detection fails, then the app skips the upload. Please contact our +[support](mailto:support@ente.io) if you find that a valid file did not get +detected and uploaded. diff --git a/docs/docs/photos/troubleshooting/report-bug.md b/docs/docs/photos/troubleshooting/report-bug.md deleted file mode 100644 index 239cb30d9..000000000 --- a/docs/docs/photos/troubleshooting/report-bug.md +++ /dev/null @@ -1,15 +0,0 @@ -## Report Bug - -Guide to help the user in sharing logs. - -### Mobile - -Placeholder - -### Desktop - -Placeholder - -### Web - -Placeholder diff --git a/docs/docs/photos/troubleshooting/sharing-logs.md b/docs/docs/photos/troubleshooting/sharing-logs.md new file mode 100644 index 000000000..65090a76a --- /dev/null +++ b/docs/docs/photos/troubleshooting/sharing-logs.md @@ -0,0 +1,26 @@ +--- +title: Sharing logs with support +description: How to report bugs and share the logs from your Ente Photos app +--- + +# Sharing debug logs + +In some cases when you report a bug, our customer support might request you to +share debug logs from your app to help our developers find the issue. + +Note that the debug logs contain potentially sensitive information like the file +names, so please feel free to not share them if you have any hesitation or want +to keep these private. We will try to diagnose the issue even without the logs, +the logs just make the process a bit faster and easier. + +### Mobile + +Steps for mobile. Still a placeholder. + +### Desktop + +Placeholder + +### Web + +Placeholder diff --git a/docs/docs/photos/troubleshooting/video-not-playing.md b/docs/docs/photos/troubleshooting/video-not-playing.md deleted file mode 100644 index ff0251043..000000000 --- a/docs/docs/photos/troubleshooting/video-not-playing.md +++ /dev/null @@ -1,5 +0,0 @@ -## Video Playback Issue - -### Web - -### Desktop / Mobile diff --git a/docs/docs/self-hosting/faq/otp.md b/docs/docs/self-hosting/faq/otp.md new file mode 100644 index 000000000..a224d1f66 --- /dev/null +++ b/docs/docs/self-hosting/faq/otp.md @@ -0,0 +1,27 @@ +--- +title: Verification code +description: Getting the OTP for a self host Ente +--- + +# Verification code + +The self-hosted Ente by default does not send out emails, so you can pick the +verification code by: + +- Getting it from the server logs, or + +- Reading it from the DB (otts table) + +You can also set pre-defined hardcoded OTTs for certain users when running +locally by creating a `museum.yaml` and adding the `internal.hardcoded-ott` +configuration setting to it. See +[local.yaml](https://github.com/ente-io/ente/blob/main/server/configurations/local.yaml) +in the server source code for details about how to define this. + +> [!NOTE] +> +> If you're not able to get the OTP with the above methods, make sure that you +> are actually connecting to your self hosted instance and not to Ente's +> production servers. e.g. you can use the network requests tab in the browser +> console to verify that the API requests are going to your server instead of +> `api.ente.io`. diff --git a/docs/docs/self-hosting/faq/storage-space.md b/docs/docs/self-hosting/faq/storage-space.md new file mode 100644 index 000000000..f1ad78c71 --- /dev/null +++ b/docs/docs/self-hosting/faq/storage-space.md @@ -0,0 +1,12 @@ +--- +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/guides/admin.md b/docs/docs/self-hosting/guides/admin.md new file mode 100644 index 000000000..91ea4d0f8 --- /dev/null +++ b/docs/docs/self-hosting/guides/admin.md @@ -0,0 +1,43 @@ +--- +title: Server admin +description: Administering your custom self-hosted Ente instance using the CLI +--- + +# Administering your custom server + +You can use +[Ente's CLI](https://github.com/ente-io/ente/releases?q=tag%3Acli-v0) to +administer your self hosted server. + +First we need to get your CLI to connect to your custom server. Define a +config.yaml and put it either in the same directory as CLI or path defined in +env variable `ENTE_CLI_CONFIG_PATH` + +```yaml +endpoint: + api: "http://localhost:8080" +``` + +Now you should be able to +[add an account](https://github.com/ente-io/ente/blob/main/cli/docs/generated/ente_account_add.md), +and subsequently increase the +[storage and account validity](https://github.com/ente-io/ente/blob/main/cli/docs/generated/ente_admin_update-subscription.md) +using the CLI. + +For the admin actions, you can create `server/museum.yaml`, and whitelist add +the admin userID `internal.admins`. See +[local.yaml](https://github.com/ente-io/ente/blob/main/server/configurations/local.yaml#L211C1-L232C1) +in the server source code for details about how to define this. + +```yaml +.... +internal: + admins: + # - 1580559962386440 + +.... +``` + +You can use +[account list](https://github.com/ente-io/ente/blob/main/cli/docs/generated/ente_account_list.md) +command to find the user id of any account. diff --git a/docs/docs/self-hosting/guides/custom-server/custom-server.png b/docs/docs/self-hosting/guides/custom-server/custom-server.png new file mode 100644 index 000000000..5e4e83f09 Binary files /dev/null and b/docs/docs/self-hosting/guides/custom-server/custom-server.png differ diff --git a/docs/docs/self-hosting/guides/custom-server/index.md b/docs/docs/self-hosting/guides/custom-server/index.md new file mode 100644 index 000000000..7f7ba50fa --- /dev/null +++ b/docs/docs/self-hosting/guides/custom-server/index.md @@ -0,0 +1,39 @@ +--- +title: Custom server +description: Using a custom self-hosted server with Ente client apps and CLI +--- + +# Connecting to a custom server + +You can modify various Ente client apps and CLI to connect to a self hosted +custom server endpoint. + +## Mobile apps + +The pre-built Ente apps from GitHub / App Store / Play Store / F-Droid can be +easily configured to use a custom server. + +You can tap 7 times on the onboarding screen to bring up a page where you can +configure the endpoint the app should be connecting to. + +![Setting a custom server on the onboarding screen](custom-server.png) + +> [!IMPORTANT] +> +> This is only supported by the Ente Auth app currently. We'll add this same +> functionality to the Ente Photos app soon. + +## CLI + +> [!NOTE] +> +> You can download the CLI from +> [here](https://github.com/ente-io/ente/releases?q=tag%3Acli-v0) + +Define a config.yaml and put it either in the same directory as CLI or path +defined in env variable `ENTE_CLI_CONFIG_PATH` + +```yaml +endpoint: + api: "http://localhost:8080" +``` diff --git a/docs/docs/self-hosting/guides/external-s3.md b/docs/docs/self-hosting/guides/external-s3.md new file mode 100644 index 000000000..40b401724 --- /dev/null +++ b/docs/docs/self-hosting/guides/external-s3.md @@ -0,0 +1,238 @@ +--- +title: External S3 buckets +description: + Self hosting Ente's server and photos web app when using an external S3 + bucket +--- + +# Hosting server and web app using external S3 + +This guide is for self hosting the server and the web application of Ente Photos +using docker compose and an external S3 bucket. So we assume that you already +have the keys and secrets for the S3 bucket. The plan is as follows: + +1. Create a `compose.yaml` file +2. Set up the `.credentials.env` file +3. Run `docker-compose up` +4. Create an account and increase storage quota +5. Fix potential CORS issue with your bucket + +## 1. Create a `compose.yaml` file + +After cloning the main repository with + +```bash +git clone https://github.com/ente-io/ente.git +# Or git clone git@github.com:ente-io/ente.git +cd ente +``` + +Create a `compose.yaml` file at the root of the project with the following +content (there is nothing to change here): + +```yaml +version: "3" +services: + museum: + build: + context: server + args: + GIT_COMMIT: local + ports: + - 8080:8080 # API + - 2112:2112 # Prometheus metrics + depends_on: + postgres: + condition: service_healthy + + # Wait for museum to ping pong before starting the webapp. + healthcheck: + test: [ + "CMD", + "echo", + "1", # I don't know what to put here + ] + environment: + # no need to touch these + ENTE_DB_HOST: postgres + ENTE_DB_PORT: 5432 + ENTE_DB_NAME: ente_db + ENTE_DB_USER: pguser + ENTE_DB_PASSWORD: pgpass + env_file: + - ./.credentials.env + volumes: + - custom-logs:/var/logs + networks: + - internal + + web: + build: + context: web + ports: + - 8081:80 + - 8082:80 + depends_on: + museum: + condition: service_healthy + env_file: + - ./.credentials.env + + postgres: + image: postgres:12 + ports: + - 5432:5432 + environment: + POSTGRES_USER: pguser + POSTGRES_PASSWORD: pgpass + POSTGRES_DB: ente_db + # Wait for postgres to be accept connections before starting museum. + healthcheck: + test: ["CMD", "pg_isready", "-q", "-d", "ente_db", "-U", "pguser"] + interval: 1s + timeout: 5s + retries: 20 + volumes: + - postgres-data:/var/lib/postgresql/data + networks: + - internal +volumes: + custom-logs: + postgres-data: +networks: + internal: +``` + +It maybe be added in the future, but if it does not exist, create a `Dockerfile` +in the `web` directory with the following content: + +```Dockerfile +# syntax=docker/dockerfile:1 +FROM node:21-bookworm-slim as ente-builder +WORKDIR /app +RUN apt update && apt install -y ca-certificates && rm -rf /var/lib/apt/lists/* +COPY . . +RUN yarn install +ENV NEXT_PUBLIC_ENTE_ENDPOINT=DOCKER_RUNTIME_REPLACE_ENDPOINT +ENV NEXT_PUBLIC_ENTE_ALBUMS_ENDPOINT=DOCKER_RUNTIME_REPLACE_ALBUMS_ENDPOINT +RUN yarn build + + +FROM nginx:1.25-alpine-slim +COPY --from=ente-builder /app/apps/photos/out /usr/share/nginx/html +COPY < + +```sh +# run `go run tools/gen-random-keys/main.go` in the server directory to generate the keys +ENTE_KEY_ENCRYPTION= +ENTE_KEY_HASH= +ENTE_JWT_SECRET= + +ENTE_S3_B2-EU-CEN_KEY=YOUR_S3_KEY +ENTE_S3_B2-EU-CEN_SECRET=YOUR_S3_SECRET +ENTE_S3_B2-EU-CEN_ENDPOINT=YOUR_S3_ENDPOINT +ENTE_S3_B2-EU-CEN_REGION=YOUR_S3_REGION +ENTE_S3_B2-EU-CEN_BUCKET=YOUR_S3_BUCKET +ENTE_S3_ARE_LOCAL_BUCKETS=false + +ENTE_INTERNAL_HARDCODED-OTT_LOCAL-DOMAIN-SUFFIX="@example.com" +ENTE_INTERNAL_HARDCODED-OTT_LOCAL-DOMAIN-VALUE=123456 + +# if you deploy it on a server under a domain, you need to set the correct value of the following variables +# it can be changed later +ENDPOINT=http://localhost:8080 +ALBUMS_ENDPOINT=http://localhost:8082 +``` + +## 3. Run `docker-compose up` + +Run `docker-compose up` at the root of the project (add `-d` to run it in the +background). + +## 4. Create an account and increase storage quota + +Open `http://localhost:8081` (or the url of your server) in your browser and +create an account. Choose 123456 as the value for the one-time token if your +email has the correct domain as defined in the `.credentials.env` file. + +If you successfully log in, select any plan and increase the storage quota with +the following command: + +```bash +docker compose exec -i postgres psql -U pguser -d ente_db -c "INSERT INTO storage_bonus (bonus_id, user_id, storage, type, valid_till) VALUES ('self-hosted-myself', (SELECT user_id FROM users), 1099511627776, 'ADD_ON_SUPPORT', 0)" +``` + +After few reloads, you should see 1 To of quota. + +## 5. Fix potential CORS issue with your bucket + +If you cannot upload a photo due to a CORS issue, you need to fix the CORS +configuration of your bucket. + +Create a `cors.json` file with the following content: + +```json +{ + "CORSRules": [ + { + "AllowedOrigins": ["*"], + "AllowedHeaders": ["*"], + "AllowedMethods": ["GET", "HEAD", "POST", "PUT", "DELETE"], + "MaxAgeSeconds": 3000, + "ExposeHeaders": ["Etag"] + } + ] +} +``` + +You may want to change the `AllowedOrigins` to a more restrictive value. + +Then run the following command with the aws command to update the CORS +configuration of your bucket: + +```bash +aws s3api put-bucket-cors --bucket YOUR_S3_BUCKET --cors-configuration file://cors.json +``` + +Upload should now work. diff --git a/docs/docs/self-hosting/guides/index.md b/docs/docs/self-hosting/guides/index.md new file mode 100644 index 000000000..a8a64d960 --- /dev/null +++ b/docs/docs/self-hosting/guides/index.md @@ -0,0 +1,20 @@ +--- +title: Self Hosting +description: Guides for self hosting Ente Photos and/or Ente Auth +--- + +# Guides + +If you've figured out how to do something, help others out by adding +walkthroughs, tutorials and other FAQ pages in this directory. + +See the sidebar for existing guides. In particular: + +- If you're just looking to get started, see + [configure custom server](custom-server/). + +- For various admin related tasks, e.g. increasing the storage quota on your + self hosted instance, see [administering your custom server](admin). + +- For self hosting both the server and web app using external S3 buckets for + object storage, see [using external S3](external-s3). diff --git a/docs/docs/self-hosting/guides/mobile-build.md b/docs/docs/self-hosting/guides/mobile-build.md new file mode 100644 index 000000000..c36903a20 --- /dev/null +++ b/docs/docs/self-hosting/guides/mobile-build.md @@ -0,0 +1,54 @@ +--- +title: Building mobile apps +description: + Connecting to your custom self-hosted server when building the Ente mobile + apps from source +--- + +# Mobile: Build and connect to self-hosted server + +The up to date instructions to build the mobile apps are in the +[Ente Photos](https://github.com/ente-io/ente/tree/main/mobile#readme) and +[Ente Auth](https://github.com/ente-io/ente/tree/main/auth#readme) READMEs. When +building or running, you can use the + +```sh +--dart-define=endpoint=http://localhost:8080 +``` + +parameter to get these builds to connect to your custom self-hosted server. + +As a short summary, you can install Flutter and build the Photos app this way: + +```sh +cd ente/mobile +git submodule update --init --recursive +flutter pub get +# Android +flutter run --dart-define=endpoint=http://localhost:8080 --flavor independent --debug -t lib/main.dart +# iOS +flutter run --dart-define=endpoint=http://localhost:8080 +``` + +Or for the auth app: + +```sh +cd ente/auth +git submodule update --init --recursive +flutter pub get +flutter run --dart-define=endpoint=http://localhost:8080 +# Android +flutter run --dart-define=endpoint=http://localhost:8080 --flavor independent --debug -t lib/main.dart +# iOS +flutter run --dart-define=endpoint=http://localhost:8080 +``` + +## How to build non-debug builds + +For building APK, +[setup your keystore](https://docs.flutter.dev/deployment/android#create-an-upload-keystore) +and run + +```sh +flutter build apk --release --flavor independent -t lib/main.dart +``` diff --git a/docs/docs/self-hosting/guides/system-requirements.md b/docs/docs/self-hosting/guides/system-requirements.md new file mode 100644 index 000000000..55eb9e9e3 --- /dev/null +++ b/docs/docs/self-hosting/guides/system-requirements.md @@ -0,0 +1,14 @@ +--- +title: System requirements +description: System requirements for running Ente's server +--- + +# System requirements + +There aren't any "minimum" system requirements as such, the server process is +very light weight - it's just a single go binary, and it doesn't do any server +side ML, so I feel it should be able to run on anything reasonable. + +We've used the server quite easily on small cloud instances, old laptops etc. A +community member also reported being able to run the server on +[very low-end embedded devices](https://github.com/ente-io/ente/discussions/594). diff --git a/docs/docs/self-hosting/index.md b/docs/docs/self-hosting/index.md new file mode 100644 index 000000000..53db6ab29 --- /dev/null +++ b/docs/docs/self-hosting/index.md @@ -0,0 +1,71 @@ +--- +title: Self Hosting +description: Getting started self hosting Ente Photos and/or Ente Auth +--- + +# Self Hosting + +The entire source code for Ente is open source, including the servers. This is +the same code we use for our own cloud service. + +> [!TIP] +> +> To get some context, you might find our +> [blog post](https://ente.io/blog/open-sourcing-our-server/) announcing the +> open sourcing of our server useful. + +## Getting started + +Start the server + +```sh +git clone https://github.com/ente-io/ente +cd ente/server +docker compose up --build +``` + +Then in a separate terminal, you can run (e.g) the web client + +```sh +cd ente/web +git submodule update --init --recursive +yarn install +NEXT_PUBLIC_ENTE_ENDPOINT=http://localhost:8080 yarn dev +``` + +That's about it. If you open http://localhost:3000, you will be able to create +an account on a Ente Photos web app running on your machine, and this web app +will be connecting to the server running on your local machine at +localhost:8080. + +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 +> [here](guides/mobile-build). + +## Next steps + +- More details about the server are in its + [README](https://github.com/ente-io/ente/tree/main/server#readme) + +- More details about running the server (with or without Docker) are in + [RUNNING](https://github.com/ente-io/ente/blob/main/server/RUNNING.md) + +- If you have questions around self-hosting that are not answered in any of + the existing documentation, you can ask in our + [GitHub Discussions](https://github.com/ente-io/ente/discussions). **Please + remember to search first if the query has been already asked and answered.** + +## Contributing! + +While we would love to provide a completely seamless self-hosting experience, +right now we do not have the engineering bandwidth to answer all queries, +document everything exactly etc. We will try (that's why we're writing this!), +but we also hope that community members will step up to fill any gaps. + +One particular way in which you can help is by adding new [guides](guides/) on +this help site. The documentation is written in Markdown and adding new pages is +[easy](https://github.com/ente-io/ente/tree/main/docs#readme). Editing existing +pages is even easier: at the bottom of each page is an _Edit this page_ link. diff --git a/docs/docs/self-hosting/troubleshooting/yarn.md b/docs/docs/self-hosting/troubleshooting/yarn.md new file mode 100644 index 000000000..7d8d13b00 --- /dev/null +++ b/docs/docs/self-hosting/troubleshooting/yarn.md @@ -0,0 +1,10 @@ +--- +title: Yarn errors +description: Fixing yarn install errors when trying to self host Ente +--- + +# Yarn + +If your `yarn install` is failing, make sure you are using Yarn Classic + +- https://classic.yarnpkg.com/lang/en/docs/install diff --git a/docs/package.json b/docs/package.json index d6b4b8a3b..5d4dc3b19 100644 --- a/docs/package.json +++ b/docs/package.json @@ -9,6 +9,6 @@ }, "devDependencies": { "prettier": "^3", - "vitepress": "^1.0.0-rc.44" + "vitepress": "^1.0.0-rc.45" } } diff --git a/docs/yarn.lock b/docs/yarn.lock index 5f12adede..048a52a61 100644 --- a/docs/yarn.lock +++ b/docs/yarn.lock @@ -723,10 +723,10 @@ vite@^5.1.3: optionalDependencies: fsevents "~2.3.3" -vitepress@^1.0.0-rc.44: - version "1.0.0-rc.44" - resolved "https://registry.yarnpkg.com/vitepress/-/vitepress-1.0.0-rc.44.tgz#01bce883761c22de42b9869a95f04bd02cbb8cdb" - integrity sha512-tO5taxGI7fSpBK1D8zrZTyJJERlyU9nnt0jHSt3fywfq3VKn977Hg0wUuTkEmwXlFYwuW26+6+3xorf4nD3XvA== +vitepress@^1.0.0-rc.45: + version "1.0.0-rc.45" + resolved "https://registry.yarnpkg.com/vitepress/-/vitepress-1.0.0-rc.45.tgz#1cb41f53fa084c224dd2d910137ef7b2e8c0c191" + integrity sha512-/OiYsu5UKpQKA2c0BAZkfyywjfauDjvXyv6Mo4Ra57m5n4Bxg1HgUGoth1CLH2vwUbR/BHvDA9zOM0RDvgeSVQ== dependencies: "@docsearch/css" "^3.5.2" "@docsearch/js" "^3.5.2" diff --git a/mobile/.env.example b/mobile/.env.example deleted file mode 100644 index 329eacea7..000000000 --- a/mobile/.env.example +++ /dev/null @@ -1,3 +0,0 @@ -endpoint=https://api.ente.io/ -urltemplate=YOUR_URL_TEMPLATE -apikey=YOUR_API_KEY diff --git a/mobile/.gitignore b/mobile/.gitignore index 40821c9c1..b6ee5c616 100644 --- a/mobile/.gitignore +++ b/mobile/.gitignore @@ -9,6 +9,9 @@ .history .svn/ +# Editors +.vscode/ + # IntelliJ related *.iml *.ipr @@ -25,7 +28,6 @@ .pub/ /build/ - # Web related lib/generated_plugin_registrant.dart @@ -34,8 +36,8 @@ lib/generated_plugin_registrant.dart android/key.properties android/app/.settings/* +android/.settings/ .env - fastlane/report.xml TensorFlowLiteC.framework diff --git a/mobile/CHANGELOG.md b/mobile/CHANGELOG.md index 947f855e8..445bb9147 100644 --- a/mobile/CHANGELOG.md +++ b/mobile/CHANGELOG.md @@ -1,5 +1,31 @@ # CHANGELOG + +## v0.8.67 + +### Added +* #### Home Widget ✨ + + Introducing our new Android widget! Enjoy your favourite memories directly on your home screen. + +* #### Redesigned Discovery Tab + + We've given it a fresh new look for improved design and better visual separation between each section. + +* #### Location Clustering + + Now, see photos automatically organize into clusters around a radius of populated cities. + +* #### Ente is now fully Open Source! + + We took the final step in our open source journey. Our clients had always been open source. Now, we have released the source code for our servers. + +* #### Bug Fixes + + 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.54 ### Added @@ -78,34 +104,3 @@ * **Translations**: Add support for German language * This release contains massive improvements to how smoothly our gallery scrolls. More improvements are on the way! - - - -## 0.7.62 - -### Added -#### Collages ✨ - -Create collages out of your favorite photos! - -Select your photos, and click on "Create collage" to build a single frame that captures your whole memory. - - -#### Album sort order - -You can now choose how photos within your albums are ordered – newest or oldest first. - -This is useful for albums of trips and events, where you wish to see your stories unfold along their original timelines. - -Click on the overflow menu within an album to configure how it's sorted. - - -#### Shared album improvements - -Photos in albums that are shared with you will now be shown in your home gallery. You can hide them by simply archiving the shared album. - - -### Improvements - - -We've worked super hard to improve how smoothly our home gallery scrolls. Skimming through your memories should be a lot more enjoyable now. \ No newline at end of file diff --git a/mobile/README.md b/mobile/README.md index f9383ce77..114a3ab38 100644 --- a/mobile/README.md +++ b/mobile/README.md @@ -45,8 +45,7 @@ You can alternatively install the build from PlayStore or F-Droid. ## 🧑‍💻 Building from source -1. [Install Flutter v3.13.4](https://flutter.dev/docs/get-started/install) or - set the Path of Flutter SDK to `thirdparty/flutter/bin`. +1. [Install Flutter v3.13.4](https://flutter.dev/docs/get-started/install). 2. Pull in all submodules with `git submodule update --init --recursive` diff --git a/mobile/android/app/src/main/kotlin/io/ente/photos/SlideshowWidgetProvider.kt b/mobile/android/app/src/main/kotlin/io/ente/photos/SlideshowWidgetProvider.kt index 284ad5539..1ed15a5d0 100644 --- a/mobile/android/app/src/main/kotlin/io/ente/photos/SlideshowWidgetProvider.kt +++ b/mobile/android/app/src/main/kotlin/io/ente/photos/SlideshowWidgetProvider.kt @@ -54,7 +54,7 @@ class SlideshowWidgetProvider : HomeWidgetProvider() { val drawable = ContextCompat.getDrawable( context, - R.drawable.ic_launcher_foreground + R.drawable.ic_home_widget_default ) val bitmap = (drawable as BitmapDrawable).bitmap setImageViewBitmap(R.id.widget_placeholder, bitmap) diff --git a/mobile/android/app/src/main/res/drawable-hdpi/ic_home_widget_default.png b/mobile/android/app/src/main/res/drawable-hdpi/ic_home_widget_default.png new file mode 100644 index 000000000..f66451a49 Binary files /dev/null and b/mobile/android/app/src/main/res/drawable-hdpi/ic_home_widget_default.png differ diff --git a/mobile/android/app/src/main/res/drawable-ldpi/ic_home_widget_default.png b/mobile/android/app/src/main/res/drawable-ldpi/ic_home_widget_default.png new file mode 100644 index 000000000..7db5ac406 Binary files /dev/null and b/mobile/android/app/src/main/res/drawable-ldpi/ic_home_widget_default.png differ diff --git a/mobile/android/app/src/main/res/drawable-mdpi/ic_home_widget_default.png b/mobile/android/app/src/main/res/drawable-mdpi/ic_home_widget_default.png new file mode 100644 index 000000000..b5be0ac8e Binary files /dev/null and b/mobile/android/app/src/main/res/drawable-mdpi/ic_home_widget_default.png differ diff --git a/mobile/android/app/src/main/res/drawable-xhdpi/ic_home_widget_default.png b/mobile/android/app/src/main/res/drawable-xhdpi/ic_home_widget_default.png new file mode 100644 index 000000000..bc9a25ab9 Binary files /dev/null and b/mobile/android/app/src/main/res/drawable-xhdpi/ic_home_widget_default.png differ diff --git a/mobile/android/app/src/main/res/drawable-xxhdpi/ic_home_widget_default.png b/mobile/android/app/src/main/res/drawable-xxhdpi/ic_home_widget_default.png new file mode 100644 index 000000000..52971c10a Binary files /dev/null and b/mobile/android/app/src/main/res/drawable-xxhdpi/ic_home_widget_default.png differ diff --git a/mobile/android/app/src/main/res/drawable-xxxhdpi/ic_home_widget_default.png b/mobile/android/app/src/main/res/drawable-xxxhdpi/ic_home_widget_default.png new file mode 100644 index 000000000..ca010e9da Binary files /dev/null and b/mobile/android/app/src/main/res/drawable-xxxhdpi/ic_home_widget_default.png differ diff --git a/mobile/android/app/src/main/res/layout/slideshow_layout.xml b/mobile/android/app/src/main/res/layout/slideshow_layout.xml index 95e7e4854..2a3a8b57e 100644 --- a/mobile/android/app/src/main/res/layout/slideshow_layout.xml +++ b/mobile/android/app/src/main/res/layout/slideshow_layout.xml @@ -14,7 +14,6 @@ android:adjustViewBounds="true" android:visibility="visible" tools:visibility="visible" - android:background="@drawable/widget_background" /> [!NOTE] +> +> Use [semver](https://semver.org/) for the tags, with `photos-` as a prefix. +> Multiple beta releases for the same upcoming version can be done by adding +> build metadata at the end, e.g. `photos-v1.2.3-beta+3`. + +Once that is merged, tag main, and push the tag. + +```sh +git tag photos-v1.2.3 +git push origin photos-v1.2.3 +``` + +This'll trigger a GitHub workflow that: + +* Creates a new draft GitHub release and attaches the build artifacts to it + (mobile APKs), + +Once the workflow completes, go to the draft GitHub release that was created. + +> [!NOTE] +> +> Keep the title of the release same as the tag. + +Set "Previous tag" to the last release of auth and press "Generate release +notes". The generated release note will contain all PRs and new contributors +from all the releases in the monorepo, so you'll need to filter them to keep +only the things that relate to the Photos mobile app. diff --git a/mobile/docs/vscode/settings.json b/mobile/docs/vscode/settings.json deleted file mode 100644 index b6a9a3c02..000000000 --- a/mobile/docs/vscode/settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "dart.flutterSdkPath": "thirdparty/flutter/bin" -} diff --git a/mobile/fastlane/metadata/android/pt/full_description.txt b/mobile/fastlane/metadata/android/pt/full_description.txt index 8710f4866..2bd26df1e 100644 --- a/mobile/fastlane/metadata/android/pt/full_description.txt +++ b/mobile/fastlane/metadata/android/pt/full_description.txt @@ -1,6 +1,6 @@ ente é um aplicativo simples para fazer backup e compartilhar suas fotos e vídeos. -Se você está procurando uma alternativa ao Google Photos com foco em privacidade, veio ao lugar certo. Com ente, eles são armazenados com criptografia de ponta a ponta (e2ee). Isso significa que só você pode vê-los. +Se você está procurando uma alternativa ao Google Fotos com foco em privacidade, você veio ao lugar certo. Com ente, eles são armazenados com criptografia de ponta a ponta (e2ee). Isso significa que só você pode vê-los. Temos aplicativos de código aberto em todas as plataformas, Android, iOS, web e desktop, e suas fotos irão sincronizar perfeitamente entre todas elas de forma criptografada (e2ee). @@ -33,4 +33,4 @@ PREÇO Não oferecemos planos gratuitos para sempre, porque é importante para nós que permaneçamos sustentáveis e resistamos à prova do tempo. Em vez disso, oferecemos planos acessíveis que você pode compartilhar livremente com sua família. Você pode encontrar mais informações em ente.io. SUPORTE -Temos orgulho em oferecer apoio humano. Se você é o nosso cliente pago, você pode entrar em contato com o team@ente.io e esperar uma resposta da nossa equipe dentro de 24 horas. +Temos orgulho em oferecer apoio humano. Se você é nosso cliente pago, você pode entrar em contato com o team@ente.io e esperar uma resposta da nossa equipe dentro de 24 horas. diff --git a/mobile/fastlane/metadata/android/zh/full_description.txt b/mobile/fastlane/metadata/android/zh/full_description.txt index ed95a109c..301c5bd35 100644 --- a/mobile/fastlane/metadata/android/zh/full_description.txt +++ b/mobile/fastlane/metadata/android/zh/full_description.txt @@ -1,36 +1,36 @@ ente 是一个简单的应用程序来备份和分享您的照片和视频。 -如果你一直在寻找一个隐私友好的可以替代Google Photos,你已经来到了正确的地方。 使用 Ente,它们以端到端加密 (e2ee) 的方式存储。 这意味着只有您可以查看它们。 使用 Ente,它们以端到端加密 (e2ee) 的方式存储。 这意味着只有您可以查看它们。 +如果你一直在寻找一个隐私友好的Google Photos替代品,那么你就来对地方了。 使用 Ente,它们以端到端加密 (e2ee) 的方式存储。 这意味着只有您可以查看它们。 使用 Ente,它们以端到端加密 (e2ee) 的方式存储。 这意味着只有您可以查看它们。 -我们在Android、iOS、web 和桌面上有开源应用, 和您的照片将以端到端加密方式无缝同步 (e2ee)。 +我们在Android、iOS、web 和桌面上有开源应用, 和您的照片将以端到端加密方式 (e2ee) 无缝同步。 -如果你一直在寻找一个隐私友好的可以替代Google Photos,你已经来到了正确的地方。 使用 Ente,它们以端到端加密 (e2ee) 的方式存储。 这意味着只有您可以查看它们。 使用 Ente,它们以端到端加密 (e2ee) 的方式存储。 这意味着只有您可以查看它们。 即使您不是亲人,也可以轻松地与您的个人分享您的相册。 即使您不是亲人,也可以轻松地与您的个人分享您的相册。 您可以分享可公开查看的链接,他们可以通过添加照片来查看您的相册并进行协作,即使没有帐户或应用。 +ente也使分享相册给自己的爱人、亲人变得轻而易举,即使他们可能并不使用ente。 您可以分享可公开查看的链接,使他们可以查看您的相册,并通过添加照片来协作而不需要注册账户或下载app。 权限 -您的加密数据已复制到三个不同的地点,包括巴黎的一个铺面掩体。 我们认真对待子孙后代,并确保您回忆比您长寿。 我们认真对待子孙后代,并确保您回忆比您长寿。 +您的加密数据已复制到三个不同的地点,包括巴黎的一个安全屋。 我们认真对待子孙后代,并确保您的回忆比您长寿。 我们认真对待子孙后代,并确保您的回忆比您长寿。 -我们来这里是为了使最安全的照片应用成为新的应用程序,来加入我们的旅程! +我们来这里是为了打造有史以来最安全的照片应用,来和我们一起前行! -特色 +特点 - 原始质量备份,因为每个像素都是重要的 - 家庭计划,您可以与家人共享存储 - 协作相册,您可以在旅行后将照片汇集在一起。 -- 共享文件夹,如果您想让您的伙伴享受您的“摄像头”点击 +- 共享文件夹,如果您想让您的伙伴享受您的每一次快门 - 可以用密码保护相册链接 - 能够通过移除已经安全备份的文件释放空间 -- 人的支持,因为你值得这样做。 -- 描述,这样您可以描述您的回忆并轻松地找到 -- 图像编辑器,添加收尾工作 +- 实人支持与协助,因为你值得这一切。 +- 添加描述,这样您可以描述您的回忆并在未来轻松地找到它们 +- 图像编辑器,完成收尾工作 - 收藏、隐藏和恢复您的回忆,因为它们是宝贵的 -- 单击从谷歌、苹果、您的硬盘和更多的软件导入 -- 黑暗主题,因为您的照片看起来很好 -- 2FA,3FA,生物鉴别认证 -- 还有更多的LOT! +- 一键从谷歌、苹果、您的硬盘或更多的介质导入 +- 黑暗主题,因为您的照片在其中看着不错 +- 2FA,3FA,生物识别认证 +- 还有更多特色待你发现! 权限 -ente requests for certain permissions to serve the purpose of a photo storage provider, which can be reviewed here: https://github.com/ente-io/photos-app/blob/f-droid/android/permissions.md +ente需要特定权限以执行作为图像存储提供商的职责,相关内容可以在此链接查阅:https://github.com/ente-io/photos-app/blob/f-droid/android/permissions.md 价格 -我们不会永远提供免费计划,因为我们必须保持可持续性,经受住时间的考验。 相反,我们提供您可以自由分享的负担得起的计划。 您可以在 ente. 相反,我们提供您可以自由分享的负担得起的计划。 您可以在 ente.io找到更多信息。 您可以在 ente.io找到更多信息。 +我们不会提供永久免费计划,因为我们必须保持可持续性,经受住时间的考验。 相反,我们向您提供了价格实惠、可自由分享的订阅计划。 您可以在 ente.io 找到更多信息。 相反,我们向您提供了价格实惠、可自由分享的订阅计划。 您可以在 ente.io 找到更多信息。 支持 -我们对提供人的支持感到自豪。 我们对提供人的支持感到自豪。 如果您是我们的付费客户,您可以联系Team@ente.io并期待我们的团队在24小时内做出回应。 +我们对提供真人支持感到自豪。 我们对提供真人支持感到自豪。 如果您是我们的付费客户,您可以联系 team@ente.io 并在24小时内收到来自我们团队的回复。 diff --git a/mobile/fastlane/metadata/ios/pt/description.txt b/mobile/fastlane/metadata/ios/pt/description.txt index b1251e43e..3d948a2b2 100644 --- a/mobile/fastlane/metadata/ios/pt/description.txt +++ b/mobile/fastlane/metadata/ios/pt/description.txt @@ -1,6 +1,6 @@ Ente é um aplicativo simples para fazer backup e compartilhar suas fotos e vídeos. -Se você está procurando uma alternativa ao Google Photos com foco em privacidade, veio ao lugar certo. Com ente, eles são armazenados com criptografia de ponta a ponta (e2ee). Isso significa que só você pode vê-los. +Se você esteve procurando uma alternativa amigável à privacidade para preservar suas memórias, você veio ao lugar certo. Com Ente, elas são armazenadas com criptografia de ponta a ponta (e2ee). Isso significa que só você pode vê-las. Temos aplicativos de código aberto em Android, iOS, web e desktop, e suas fotos irão sincronizar perfeitamente entre todas elas de forma criptografada (e2ee). @@ -27,7 +27,7 @@ PREÇO Não oferecemos planos gratuitos para sempre, porque é importante para nós que permaneçamos sustentáveis e resistamos à prova do tempo. Em vez disso, oferecemos planos acessíveis que você pode compartilhar livremente com sua família. Você pode encontrar mais informações em ente.io. SUPORTE -Temos orgulho em oferecer apoio humano. Se você é o nosso cliente pago, você pode entrar em contato com o team@ente.io e esperar uma resposta da nossa equipe dentro de 24 horas. +Temos orgulho em oferecer apoio humano. Se você é nosso cliente pago, você pode entrar em contato com o team@ente.io e esperar uma resposta da nossa equipe dentro de 24 horas. TERMOS https://ente.io/terms diff --git a/mobile/fastlane/metadata/ios/th/subtitle.txt b/mobile/fastlane/metadata/ios/th/subtitle.txt new file mode 100644 index 000000000..5d22dde7f --- /dev/null +++ b/mobile/fastlane/metadata/ios/th/subtitle.txt @@ -0,0 +1 @@ +เก็บรูปภาพแบบเข้ารหัส diff --git a/mobile/fastlane/metadata/playstore/pt/full_description.txt b/mobile/fastlane/metadata/playstore/pt/full_description.txt index ee0c29434..fc02bad10 100644 --- a/mobile/fastlane/metadata/playstore/pt/full_description.txt +++ b/mobile/fastlane/metadata/playstore/pt/full_description.txt @@ -1,6 +1,6 @@ Ente é um aplicativo simples para fazer backup e compartilhar suas fotos e vídeos. -Se você está procurando uma alternativa ao Google Photos com foco em privacidade, veio ao lugar certo. Com ente, eles são armazenados com criptografados de ponta a ponta (e2ee). Isso significa que só você pode vê-los. +Se você esteve procurando uma alternativa amigável à privacidade para preservar suas memórias, você veio ao lugar certo. Com Ente, elas são armazenadas com criptografia de ponta a ponta (e2ee). Isso significa que só você pode vê-las. Temos aplicativos de código aberto em todas as plataformas, Android, iOS, web e desktop, e suas fotos irão sincronizar perfeitamente entre todas elas de forma criptografada (e2ee). @@ -27,4 +27,4 @@ Estamos aqui para se tornar o app de fotos mais seguro de todos, venha entrar em Não oferecemos planos gratuitos para sempre, porque é importante para nós que permaneçamos sustentáveis e resistamos à prova do tempo. Em vez disso, oferecemos planos acessíveis que você pode compartilhar livremente com sua família. Você pode encontrar mais informações em ente.io. 🙋 SUPORTE -Temos orgulho em oferecer apoio humano. Se você é o nosso cliente pago, você pode entrar em contato com o team@ente.io e esperar uma resposta da nossa equipe dentro de 24 horas. \ No newline at end of file +Temos orgulho em oferecer apoio humano. Se você é nosso cliente pago, você pode entrar em contato com o team@ente.io e esperar uma resposta da nossa equipe dentro de 24 horas. \ No newline at end of file diff --git a/mobile/ios/Podfile.lock b/mobile/ios/Podfile.lock index cf6d6b875..82b2c256f 100644 --- a/mobile/ios/Podfile.lock +++ b/mobile/ios/Podfile.lock @@ -212,8 +212,6 @@ PODS: - Flutter - wakelock_plus (0.0.1): - Flutter - - workmanager (0.0.1): - - Flutter DEPENDENCIES: - background_fetch (from `.symlinks/plugins/background_fetch/ios`) @@ -263,7 +261,6 @@ DEPENDENCIES: - video_thumbnail (from `.symlinks/plugins/video_thumbnail/ios`) - volume_controller (from `.symlinks/plugins/volume_controller/ios`) - wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`) - - workmanager (from `.symlinks/plugins/workmanager/ios`) SPEC REPOS: trunk: @@ -384,8 +381,6 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/volume_controller/ios" wakelock_plus: :path: ".symlinks/plugins/wakelock_plus/ios" - workmanager: - :path: ".symlinks/plugins/workmanager/ios" SPEC CHECKSUMS: background_fetch: 896944864b038d2837fc750d470e9841e1e6a363 @@ -456,7 +451,6 @@ SPEC CHECKSUMS: video_thumbnail: c4e2a3c539e247d4de13cd545344fd2d26ffafd1 volume_controller: 531ddf792994285c9b17f9d8a7e4dcdd29b3eae9 wakelock_plus: 8b09852c8876491e4b6d179e17dfe2a0b5f60d47 - workmanager: 0afdcf5628bbde6924c21af7836fed07b42e30e6 PODFILE CHECKSUM: c1a8f198a245ed1f10e40b617efdb129b021b225 diff --git a/mobile/ios/Runner.xcodeproj/project.pbxproj b/mobile/ios/Runner.xcodeproj/project.pbxproj index e9cbf0685..2ddddfd9f 100644 --- a/mobile/ios/Runner.xcodeproj/project.pbxproj +++ b/mobile/ios/Runner.xcodeproj/project.pbxproj @@ -16,7 +16,7 @@ 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; - DA6BE5E826B3BC8600656280 /* (null) in Resources */ = {isa = PBXBuildFile; }; + DA6BE5E826B3BC8600656280 /* BuildFile in Resources */ = {isa = PBXBuildFile; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -229,7 +229,7 @@ 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, - DA6BE5E826B3BC8600656280 /* (null) in Resources */, + DA6BE5E826B3BC8600656280 /* BuildFile in Resources */, 277218A0270F596900FFE3CC /* GoogleService-Info.plist in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -336,7 +336,6 @@ "${BUILT_PRODUCTS_DIR}/video_thumbnail/video_thumbnail.framework", "${BUILT_PRODUCTS_DIR}/volume_controller/volume_controller.framework", "${BUILT_PRODUCTS_DIR}/wakelock_plus/wakelock_plus.framework", - "${BUILT_PRODUCTS_DIR}/workmanager/workmanager.framework", "${PODS_XCFRAMEWORKS_BUILD_DIR}/media_kit_libs_ios_video/Ass.framework/Ass", "${PODS_XCFRAMEWORKS_BUILD_DIR}/media_kit_libs_ios_video/Avcodec.framework/Avcodec", "${PODS_XCFRAMEWORKS_BUILD_DIR}/media_kit_libs_ios_video/Avfilter.framework/Avfilter", @@ -419,7 +418,6 @@ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/video_thumbnail.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/volume_controller.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/wakelock_plus.framework", - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/workmanager.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Ass.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Avcodec.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Avfilter.framework", diff --git a/mobile/ios/Runner/AppDelegate.swift b/mobile/ios/Runner/AppDelegate.swift index d0d967667..982402268 100644 --- a/mobile/ios/Runner/AppDelegate.swift +++ b/mobile/ios/Runner/AppDelegate.swift @@ -1,6 +1,5 @@ import Flutter import UIKit -import workmanager @UIApplicationMain @objc class AppDelegate: FlutterAppDelegate { diff --git a/mobile/lib/app.dart b/mobile/lib/app.dart index 49e742f2f..d4b852540 100644 --- a/mobile/lib/app.dart +++ b/mobile/lib/app.dart @@ -7,17 +7,24 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_easyloading/flutter_easyloading.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:home_widget/home_widget.dart' as hw; import 'package:logging/logging.dart'; import 'package:media_extension/media_extension_action_types.dart'; import 'package:photos/ente_theme_data.dart'; import "package:photos/generated/l10n.dart"; import "package:photos/l10n/l10n.dart"; +import "package:photos/models/collection/collection_items.dart"; import 'package:photos/services/app_lifecycle_service.dart'; +import "package:photos/services/collections_service.dart"; +import "package:photos/services/favorites_service.dart"; +import "package:photos/services/home_widget_service.dart"; import "package:photos/services/machine_learning/machine_learning_controller.dart"; import 'package:photos/services/sync_service.dart'; import 'package:photos/ui/tabs/home_widget.dart'; import "package:photos/ui/viewer/actions/file_viewer.dart"; +import "package:photos/ui/viewer/gallery/collection_page.dart"; import "package:photos/utils/intent_util.dart"; +import "package:photos/utils/navigation_util.dart"; class EnteApp extends StatefulWidget { final Future Function(String) runBackgroundTask; @@ -55,6 +62,46 @@ class _EnteAppState extends State with WidgetsBindingObserver { WidgetsBinding.instance.addObserver(this); } + @override + void didChangeDependencies() { + super.didChangeDependencies(); + _checkForWidgetLaunch(); + hw.HomeWidget.widgetClicked.listen(_launchedFromWidget); + } + + void _checkForWidgetLaunch() { + hw.HomeWidget.initiallyLaunchedFromHomeWidget().then(_launchedFromWidget); + } + + Future _launchedFromWidget(Uri? uri) async { + if (uri == null) return; + final collectionID = + await FavoritesService.instance.getFavoriteCollectionID(); + if (collectionID == null) { + return; + } + final collection = CollectionsService.instance.getCollectionByID( + collectionID, + ); + if (collection == null) { + return; + } + unawaited(HomeWidgetService.instance.initHomeWidget()); + + final thumbnail = await CollectionsService.instance.getCover(collection); + unawaited( + routeToPage( + context, + CollectionPage( + CollectionWithThumbnail( + collection, + thumbnail, + ), + ), + ), + ); + } + setLocale(Locale newLocale) { setState(() { locale = newLocale; diff --git a/mobile/lib/core/configuration.dart b/mobile/lib/core/configuration.dart index 8579c6f99..cd6b5156e 100644 --- a/mobile/lib/core/configuration.dart +++ b/mobile/lib/core/configuration.dart @@ -14,7 +14,6 @@ import 'package:photos/db/collections_db.dart'; import "package:photos/db/embeddings_db.dart"; import 'package:photos/db/files_db.dart'; import 'package:photos/db/memories_db.dart'; -import 'package:photos/db/public_keys_db.dart'; import 'package:photos/db/trash_db.dart'; import 'package:photos/db/upload_locks_db.dart'; import 'package:photos/events/signed_in_event.dart'; @@ -25,6 +24,7 @@ import 'package:photos/models/private_key_attributes.dart'; import 'package:photos/services/billing_service.dart'; import 'package:photos/services/collections_service.dart'; import 'package:photos/services/favorites_service.dart'; +import "package:photos/services/home_widget_service.dart"; import 'package:photos/services/ignored_files_service.dart'; import 'package:photos/services/machine_learning/semantic_search/semantic_search_service.dart'; import 'package:photos/services/memories_service.dart'; @@ -32,7 +32,6 @@ import 'package:photos/services/search_service.dart'; import 'package:photos/services/sync_service.dart'; import 'package:photos/utils/crypto_util.dart'; import 'package:photos/utils/file_uploader.dart'; -import "package:photos/utils/home_widget_util.dart"; import 'package:photos/utils/validator_util.dart'; import 'package:shared_preferences/shared_preferences.dart'; import "package:tuple/tuple.dart"; @@ -165,7 +164,7 @@ class Configuration { : null; await CollectionsDB.instance.clearTable(); await MemoriesDB.instance.clearTable(); - await PublicKeysDB.instance.clearTable(); + await UploadLocksDB.instance.clearTable(); await IgnoredFilesService.instance.reset(); await TrashDB.instance.clearTable(); @@ -176,7 +175,7 @@ class Configuration { MemoriesService.instance.clearCache(); BillingService.instance.clearCache(); SearchService.instance.clearCache(); - unawaited(clearHomeWidget()); + unawaited(HomeWidgetService.instance.clearHomeWidget()); Bus.instance.fire(UserLoggedOutEvent()); } else { await _preferences.setBool("auto_logout", true); diff --git a/mobile/lib/core/constants.dart b/mobile/lib/core/constants.dart index fd94f259a..6f8f19115 100644 --- a/mobile/lib/core/constants.dart +++ b/mobile/lib/core/constants.dart @@ -67,7 +67,7 @@ const defaultCityRadius = 10.0; const galleryGridSpacing = 2.0; -const kSearchSectionLimit = 7; +const kSearchSectionLimit = 9; const iOSGroupID = "group.io.ente.frame.SlideshowWidget"; diff --git a/mobile/lib/db/public_keys_db.dart b/mobile/lib/db/public_keys_db.dart deleted file mode 100644 index 03ab61036..000000000 --- a/mobile/lib/db/public_keys_db.dart +++ /dev/null @@ -1,89 +0,0 @@ -import 'dart:async'; -import 'dart:io'; - -import 'package:path/path.dart'; -import 'package:path_provider/path_provider.dart'; -import 'package:photos/models/public_key.dart'; -import 'package:sqflite/sqflite.dart'; - -class PublicKeysDB { - static const _databaseName = "ente.public_keys.db"; - static const _databaseVersion = 1; - - static const table = 'public_keys'; - - static const columnEmail = 'email'; - static const columnPublicKey = 'public_key'; - - PublicKeysDB._privateConstructor(); - static final PublicKeysDB instance = PublicKeysDB._privateConstructor(); - - static Future? _dbFuture; - - Future get database async { - _dbFuture ??= _initDatabase(); - return _dbFuture!; - } - - Future _initDatabase() async { - final Directory documentsDirectory = - await getApplicationDocumentsDirectory(); - final String path = join(documentsDirectory.path, _databaseName); - return await openDatabase( - path, - version: _databaseVersion, - onCreate: _onCreate, - ); - } - - Future _onCreate(Database db, int version) async { - await db.execute( - ''' - CREATE TABLE $table ( - $columnEmail TEXT PRIMARY KEY NOT NULL, - $columnPublicKey TEXT NOT NULL - ) - ''', - ); - } - - Future clearTable() async { - final db = await instance.database; - await db.delete(table); - } - - Future setKey(PublicKey key) async { - final db = await instance.database; - return db.insert( - table, - _getRow(key), - conflictAlgorithm: ConflictAlgorithm.replace, - ); - } - - Future> searchByEmail(String email) async { - final db = await instance.database; - return _convertRows( - await db.query( - table, - where: '$columnEmail LIKE ?', - whereArgs: ['%$email%'], - ), - ); - } - - Map _getRow(PublicKey key) { - final row = {}; - row[columnEmail] = key.email; - row[columnPublicKey] = key.publicKey; - return row; - } - - List _convertRows(List> rows) { - final keys = []; - for (final row in rows) { - keys.add(PublicKey(row[columnEmail], row[columnPublicKey])); - } - return keys; - } -} diff --git a/mobile/lib/generated/intl/messages_cs.dart b/mobile/lib/generated/intl/messages_cs.dart index a824ca356..71aa6589d 100644 --- a/mobile/lib/generated/intl/messages_cs.dart +++ b/mobile/lib/generated/intl/messages_cs.dart @@ -29,12 +29,14 @@ class MessageLookup extends MessageLookupByLibrary { "contacts": MessageLookupByLibrary.simpleMessage("Contacts"), "deleteConfirmDialogBody": MessageLookupByLibrary.simpleMessage( "This account is linked to other ente apps, if you use any.\\n\\nYour uploaded data, across all ente apps, will be scheduled for deletion, and your account will be permanently deleted."), + "descriptions": MessageLookupByLibrary.simpleMessage("Descriptions"), "editLocation": MessageLookupByLibrary.simpleMessage("Edit location"), "editsToLocationWillOnlyBeSeenWithinEnte": MessageLookupByLibrary.simpleMessage( "Edits to location will only be seen within Ente"), "fileTypes": MessageLookupByLibrary.simpleMessage("File types"), "joinDiscord": MessageLookupByLibrary.simpleMessage("Join Discord"), + "locations": MessageLookupByLibrary.simpleMessage("Locations"), "modifyYourQueryOrTrySearchingFor": MessageLookupByLibrary.simpleMessage( "Modify your query, or try searching for"), diff --git a/mobile/lib/generated/intl/messages_de.dart b/mobile/lib/generated/intl/messages_de.dart index 2c766c0a9..5127b0ca1 100644 --- a/mobile/lib/generated/intl/messages_de.dart +++ b/mobile/lib/generated/intl/messages_de.dart @@ -562,6 +562,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Geteiltes Album löschen?"), "deleteSharedAlbumDialogBody": MessageLookupByLibrary.simpleMessage( "Dieses Album wird für alle gelöscht\n\nDu wirst den Zugriff auf geteilte Fotos in diesem Album, die anderen gehören, verlieren"), + "descriptions": MessageLookupByLibrary.simpleMessage("Descriptions"), "deselectAll": MessageLookupByLibrary.simpleMessage("Alle abwählen"), "designedToOutlive": MessageLookupByLibrary.simpleMessage("Entwickelt um zu bewahren"), @@ -872,6 +873,7 @@ class MessageLookup extends MessageLookupByLibrary { "locationName": MessageLookupByLibrary.simpleMessage("Standortname"), "locationTagFeatureDescription": MessageLookupByLibrary.simpleMessage( "Ein Standort-Tag gruppiert alle Fotos, die in einem Radius eines Fotos aufgenommen wurden"), + "locations": MessageLookupByLibrary.simpleMessage("Locations"), "lockButtonLabel": MessageLookupByLibrary.simpleMessage("Sperren"), "lockScreenEnablePreSteps": MessageLookupByLibrary.simpleMessage( "Um den Sperrbildschirm zu aktivieren, legen Sie bitte den Geräte-Passcode oder die Bildschirmsperre in den Systemeinstellungen fest."), @@ -1199,8 +1201,6 @@ class MessageLookup extends MessageLookupByLibrary { "searchHint4": MessageLookupByLibrary.simpleMessage("Ort"), "searchHint5": MessageLookupByLibrary.simpleMessage( "Demnächst: Gesichter & magische Suche ✨"), - "searchHintText": MessageLookupByLibrary.simpleMessage( - "Alben, Monate, Tage, Jahre, ..."), "searchLocationEmptySection": MessageLookupByLibrary.simpleMessage( "Gruppiere Fotos, die innerhalb des Radius eines bestimmten Fotos aufgenommen wurden"), "searchPeopleEmptySection": MessageLookupByLibrary.simpleMessage( diff --git a/mobile/lib/generated/intl/messages_en.dart b/mobile/lib/generated/intl/messages_en.dart index 8889b63c5..0ca7d3024 100644 --- a/mobile/lib/generated/intl/messages_en.dart +++ b/mobile/lib/generated/intl/messages_en.dart @@ -404,6 +404,8 @@ class MessageLookup extends MessageLookupByLibrary { "claimedStorageSoFar": m8, "cleanUncategorized": MessageLookupByLibrary.simpleMessage("Clean Uncategorized"), + "cleanUncategorizedDescription": MessageLookupByLibrary.simpleMessage( + "Remove all files from Uncategorized that are present in other albums"), "clearCaches": MessageLookupByLibrary.simpleMessage("Clear caches"), "clearIndexes": MessageLookupByLibrary.simpleMessage("Clear indexes"), "click": MessageLookupByLibrary.simpleMessage("• Click"), @@ -548,6 +550,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Delete shared album?"), "deleteSharedAlbumDialogBody": MessageLookupByLibrary.simpleMessage( "The album will be deleted for everyone\n\nYou will lose access to shared photos in this album that are owned by others"), + "descriptions": MessageLookupByLibrary.simpleMessage("Descriptions"), "deselectAll": MessageLookupByLibrary.simpleMessage("Deselect all"), "designedToOutlive": MessageLookupByLibrary.simpleMessage("Designed to outlive"), @@ -797,8 +800,6 @@ class MessageLookup extends MessageLookupByLibrary { "Kindly help us with this information"), "language": MessageLookupByLibrary.simpleMessage("Language"), "lastUpdated": MessageLookupByLibrary.simpleMessage("Last updated"), - "launchPasskeyUrlAgain": - MessageLookupByLibrary.simpleMessage("Launch passkey URL again"), "leave": MessageLookupByLibrary.simpleMessage("Leave"), "leaveAlbum": MessageLookupByLibrary.simpleMessage("Leave album"), "leaveFamily": MessageLookupByLibrary.simpleMessage("Leave family"), @@ -848,6 +849,7 @@ class MessageLookup extends MessageLookupByLibrary { "locationName": MessageLookupByLibrary.simpleMessage("Location name"), "locationTagFeatureDescription": MessageLookupByLibrary.simpleMessage( "A location tag groups all photos that were taken within some radius of a photo"), + "locations": MessageLookupByLibrary.simpleMessage("Locations"), "lockButtonLabel": MessageLookupByLibrary.simpleMessage("Lock"), "lockScreenEnablePreSteps": MessageLookupByLibrary.simpleMessage( "To enable lockscreen, please setup device passcode or screen lock in your system settings."), @@ -958,7 +960,7 @@ class MessageLookup extends MessageLookupByLibrary { "pair": MessageLookupByLibrary.simpleMessage("Pair"), "passkey": MessageLookupByLibrary.simpleMessage("Passkey"), "passkeyAuthTitle": - MessageLookupByLibrary.simpleMessage("Passkey authentication"), + MessageLookupByLibrary.simpleMessage("Passkey verification"), "password": MessageLookupByLibrary.simpleMessage("Password"), "passwordChangedSuccessfully": MessageLookupByLibrary.simpleMessage( "Password changed successfully"), @@ -1171,8 +1173,6 @@ class MessageLookup extends MessageLookupByLibrary { "searchHint4": MessageLookupByLibrary.simpleMessage("Location"), "searchHint5": MessageLookupByLibrary.simpleMessage( "Coming soon: Faces & magic search ✨"), - "searchHintText": MessageLookupByLibrary.simpleMessage( - "Albums, months, days, years, ..."), "searchLocationEmptySection": MessageLookupByLibrary.simpleMessage( "Group photos that are taken within some radius of a photo"), "searchPeopleEmptySection": MessageLookupByLibrary.simpleMessage( @@ -1446,6 +1446,7 @@ class MessageLookup extends MessageLookupByLibrary { "verifyEmail": MessageLookupByLibrary.simpleMessage("Verify email"), "verifyEmailID": m64, "verifyIDLabel": MessageLookupByLibrary.simpleMessage("Verify"), + "verifyPasskey": MessageLookupByLibrary.simpleMessage("Verify passkey"), "verifyPassword": MessageLookupByLibrary.simpleMessage("Verify password"), "verifying": MessageLookupByLibrary.simpleMessage("Verifying..."), @@ -1465,8 +1466,8 @@ class MessageLookup extends MessageLookupByLibrary { "viewer": MessageLookupByLibrary.simpleMessage("Viewer"), "visitWebToManage": MessageLookupByLibrary.simpleMessage( "Please visit web.ente.io to manage your subscription"), - "waitingForBrowserRequest": MessageLookupByLibrary.simpleMessage( - "Waiting for browser request..."), + "waitingForVerification": + MessageLookupByLibrary.simpleMessage("Waiting for verification..."), "waitingForWifi": MessageLookupByLibrary.simpleMessage("Waiting for WiFi..."), "weAreOpenSource": diff --git a/mobile/lib/generated/intl/messages_es.dart b/mobile/lib/generated/intl/messages_es.dart index a33d6e88a..7b2121bb5 100644 --- a/mobile/lib/generated/intl/messages_es.dart +++ b/mobile/lib/generated/intl/messages_es.dart @@ -487,6 +487,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("¿Borrar álbum compartido?"), "deleteSharedAlbumDialogBody": MessageLookupByLibrary.simpleMessage( "El álbum se eliminará para todos\n\nPerderás el acceso a las fotos compartidas en este álbum que son propiedad de otros"), + "descriptions": MessageLookupByLibrary.simpleMessage("Descriptions"), "deselectAll": MessageLookupByLibrary.simpleMessage("Deseleccionar todo"), "designedToOutlive": @@ -761,6 +762,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Nombre de la ubicación"), "locationTagFeatureDescription": MessageLookupByLibrary.simpleMessage( "Una etiqueta de ubicación agrupa todas las fotos que fueron tomadas dentro de un radio de una foto"), + "locations": MessageLookupByLibrary.simpleMessage("Locations"), "lockButtonLabel": MessageLookupByLibrary.simpleMessage("Bloquear"), "lockScreenEnablePreSteps": MessageLookupByLibrary.simpleMessage( "Para activar la pantalla de bloqueo, por favor configure el código de acceso del dispositivo o el bloqueo de pantalla en los ajustes de su sistema."), @@ -1036,8 +1038,6 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Nombre del álbum"), "searchByExamples": MessageLookupByLibrary.simpleMessage( "• Nombres de álbumes (por ejemplo, \"Cámara\")\n• Tipos de archivos (por ejemplo, \"Videos\", \".gif\")\n• Años y meses (por ejemplo, \"2022\", \"Enero\")\n• Vacaciones (por ejemplo, \"Navidad\")\n• Descripciones fotográficas (por ejemplo, \"#diversión\")"), - "searchHintText": MessageLookupByLibrary.simpleMessage( - "Álbunes, meses, días, años, ..."), "security": MessageLookupByLibrary.simpleMessage("Seguridad"), "selectALocation": MessageLookupByLibrary.simpleMessage("Select a location"), diff --git a/mobile/lib/generated/intl/messages_fr.dart b/mobile/lib/generated/intl/messages_fr.dart index 39287c79a..b2ac59426 100644 --- a/mobile/lib/generated/intl/messages_fr.dart +++ b/mobile/lib/generated/intl/messages_fr.dart @@ -562,6 +562,7 @@ class MessageLookup extends MessageLookupByLibrary { "Supprimer l\'album partagé ?"), "deleteSharedAlbumDialogBody": MessageLookupByLibrary.simpleMessage( "L\'album sera supprimé pour tout le monde\n\nVous perdrez l\'accès aux photos partagées dans cet album qui sont détenues par d\'autres personnes"), + "descriptions": MessageLookupByLibrary.simpleMessage("Descriptions"), "deselectAll": MessageLookupByLibrary.simpleMessage("Tout déselectionner"), "designedToOutlive": @@ -870,6 +871,7 @@ class MessageLookup extends MessageLookupByLibrary { "locationName": MessageLookupByLibrary.simpleMessage("Nom du lieu"), "locationTagFeatureDescription": MessageLookupByLibrary.simpleMessage( "Un tag d\'emplacement regroupe toutes les photos qui ont été prises dans un certain rayon d\'une photo"), + "locations": MessageLookupByLibrary.simpleMessage("Locations"), "lockButtonLabel": MessageLookupByLibrary.simpleMessage("Verrouiller"), "lockScreenEnablePreSteps": MessageLookupByLibrary.simpleMessage( "Pour activer l\'écran de verrouillage, veuillez configurer le code d\'accès de l\'appareil ou le verrouillage de l\'écran dans les paramètres de votre système."), @@ -1194,8 +1196,6 @@ class MessageLookup extends MessageLookupByLibrary { "searchHint4": MessageLookupByLibrary.simpleMessage("Emplacement"), "searchHint5": MessageLookupByLibrary.simpleMessage( "Bientôt: Visages & recherche magique ✨"), - "searchHintText": MessageLookupByLibrary.simpleMessage( - "Albums, mois, jours, années, ..."), "searchLocationEmptySection": MessageLookupByLibrary.simpleMessage( "Grouper les photos qui sont prises dans un certain angle d\'une photo"), "searchPeopleEmptySection": MessageLookupByLibrary.simpleMessage( diff --git a/mobile/lib/generated/intl/messages_it.dart b/mobile/lib/generated/intl/messages_it.dart index 29367dd33..d18386043 100644 --- a/mobile/lib/generated/intl/messages_it.dart +++ b/mobile/lib/generated/intl/messages_it.dart @@ -542,6 +542,7 @@ class MessageLookup extends MessageLookupByLibrary { "Eliminare l\'album condiviso?"), "deleteSharedAlbumDialogBody": MessageLookupByLibrary.simpleMessage( "L\'album verrà eliminato per tutti\n\nPerderai l\'accesso alle foto condivise in questo album che sono di proprietà di altri"), + "descriptions": MessageLookupByLibrary.simpleMessage("Descriptions"), "deselectAll": MessageLookupByLibrary.simpleMessage("Deseleziona tutti"), "designedToOutlive": @@ -838,6 +839,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Nome della località"), "locationTagFeatureDescription": MessageLookupByLibrary.simpleMessage( "Un tag di localizzazione raggruppa tutte le foto scattate entro il raggio di una foto"), + "locations": MessageLookupByLibrary.simpleMessage("Locations"), "lockButtonLabel": MessageLookupByLibrary.simpleMessage("Blocca"), "lockScreenEnablePreSteps": MessageLookupByLibrary.simpleMessage( "Per abilitare la schermata di blocco, configura il codice di accesso del dispositivo o il blocco schermo nelle impostazioni di sistema."), @@ -1130,8 +1132,6 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Nome album"), "searchByExamples": MessageLookupByLibrary.simpleMessage( "• Nomi degli album (es. \"Camera\")\n• Tipi di file (es. \"Video\", \".gif\")\n• Anni e mesi (e.. \"2022\", \"gennaio\")\n• Vacanze (ad es. \"Natale\")\n• Descrizioni delle foto (ad es. “#mare”)"), - "searchHintText": MessageLookupByLibrary.simpleMessage( - "Album, mesi, giorni, anni, ..."), "security": MessageLookupByLibrary.simpleMessage("Sicurezza"), "selectALocation": MessageLookupByLibrary.simpleMessage("Select a location"), diff --git a/mobile/lib/generated/intl/messages_ko.dart b/mobile/lib/generated/intl/messages_ko.dart index 903b11f1a..a97232feb 100644 --- a/mobile/lib/generated/intl/messages_ko.dart +++ b/mobile/lib/generated/intl/messages_ko.dart @@ -29,12 +29,14 @@ class MessageLookup extends MessageLookupByLibrary { "contacts": MessageLookupByLibrary.simpleMessage("Contacts"), "deleteConfirmDialogBody": MessageLookupByLibrary.simpleMessage( "This account is linked to other ente apps, if you use any.\\n\\nYour uploaded data, across all ente apps, will be scheduled for deletion, and your account will be permanently deleted."), + "descriptions": MessageLookupByLibrary.simpleMessage("Descriptions"), "editLocation": MessageLookupByLibrary.simpleMessage("Edit location"), "editsToLocationWillOnlyBeSeenWithinEnte": MessageLookupByLibrary.simpleMessage( "Edits to location will only be seen within Ente"), "fileTypes": MessageLookupByLibrary.simpleMessage("File types"), "joinDiscord": MessageLookupByLibrary.simpleMessage("Join Discord"), + "locations": MessageLookupByLibrary.simpleMessage("Locations"), "modifyYourQueryOrTrySearchingFor": MessageLookupByLibrary.simpleMessage( "Modify your query, or try searching for"), diff --git a/mobile/lib/generated/intl/messages_nl.dart b/mobile/lib/generated/intl/messages_nl.dart index 6b2b97ff8..1a5492224 100644 --- a/mobile/lib/generated/intl/messages_nl.dart +++ b/mobile/lib/generated/intl/messages_nl.dart @@ -564,6 +564,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Gedeeld album verwijderen?"), "deleteSharedAlbumDialogBody": MessageLookupByLibrary.simpleMessage( "Het album wordt verwijderd voor iedereen\n\nJe verliest de toegang tot gedeelde foto\'s in dit album die eigendom zijn van anderen"), + "descriptions": MessageLookupByLibrary.simpleMessage("Descriptions"), "deselectAll": MessageLookupByLibrary.simpleMessage("Alles deselecteren"), "designedToOutlive": MessageLookupByLibrary.simpleMessage( @@ -880,6 +881,7 @@ class MessageLookup extends MessageLookupByLibrary { "locationName": MessageLookupByLibrary.simpleMessage("Locatie naam"), "locationTagFeatureDescription": MessageLookupByLibrary.simpleMessage( "Een locatie tag groept alle foto\'s die binnen een bepaalde straal van een foto zijn genomen"), + "locations": MessageLookupByLibrary.simpleMessage("Locations"), "lockButtonLabel": MessageLookupByLibrary.simpleMessage("Vergrendel"), "lockScreenEnablePreSteps": MessageLookupByLibrary.simpleMessage( "Om vergrendelscherm in te schakelen, moet u een toegangscode of schermvergrendeling instellen in uw systeeminstellingen."), @@ -1218,8 +1220,6 @@ class MessageLookup extends MessageLookupByLibrary { "searchHint4": MessageLookupByLibrary.simpleMessage("Locatie"), "searchHint5": MessageLookupByLibrary.simpleMessage( "Binnenkort beschikbaar: Gezichten & magische zoekopdrachten ✨"), - "searchHintText": MessageLookupByLibrary.simpleMessage( - "Albums, maanden, dagen, jaren, ..."), "searchLocationEmptySection": MessageLookupByLibrary.simpleMessage( "Foto\'s groeperen die in een bepaalde straal van een foto worden genomen"), "searchPeopleEmptySection": MessageLookupByLibrary.simpleMessage( diff --git a/mobile/lib/generated/intl/messages_no.dart b/mobile/lib/generated/intl/messages_no.dart index f1e3324f3..795caf200 100644 --- a/mobile/lib/generated/intl/messages_no.dart +++ b/mobile/lib/generated/intl/messages_no.dart @@ -41,6 +41,7 @@ class MessageLookup extends MessageLookupByLibrary { "Vi er lei oss for at du forlater oss. Gi oss gjerne en tilbakemelding så vi kan forbedre oss."), "deleteConfirmDialogBody": MessageLookupByLibrary.simpleMessage( "This account is linked to other ente apps, if you use any.\\n\\nYour uploaded data, across all ente apps, will be scheduled for deletion, and your account will be permanently deleted."), + "descriptions": MessageLookupByLibrary.simpleMessage("Descriptions"), "editLocation": MessageLookupByLibrary.simpleMessage("Edit location"), "editsToLocationWillOnlyBeSeenWithinEnte": MessageLookupByLibrary.simpleMessage( @@ -57,6 +58,7 @@ class MessageLookup extends MessageLookupByLibrary { "joinDiscord": MessageLookupByLibrary.simpleMessage("Join Discord"), "kindlyHelpUsWithThisInformation": MessageLookupByLibrary.simpleMessage( "Vær vennlig og hjelp oss med denne informasjonen"), + "locations": MessageLookupByLibrary.simpleMessage("Locations"), "modifyYourQueryOrTrySearchingFor": MessageLookupByLibrary.simpleMessage( "Modify your query, or try searching for"), diff --git a/mobile/lib/generated/intl/messages_pl.dart b/mobile/lib/generated/intl/messages_pl.dart index debe5b6a8..2647e1191 100644 --- a/mobile/lib/generated/intl/messages_pl.dart +++ b/mobile/lib/generated/intl/messages_pl.dart @@ -78,6 +78,7 @@ class MessageLookup extends MessageLookupByLibrary { "Inna, niewymieniona wyżej przyczyna"), "deleteRequestSLAText": MessageLookupByLibrary.simpleMessage( "Twoje żądanie zostanie przetworzone w ciągu 72 godzin."), + "descriptions": MessageLookupByLibrary.simpleMessage("Descriptions"), "doThisLater": MessageLookupByLibrary.simpleMessage("Spróbuj później"), "editLocation": MessageLookupByLibrary.simpleMessage("Edit location"), "editsToLocationWillOnlyBeSeenWithinEnte": @@ -116,6 +117,7 @@ class MessageLookup extends MessageLookupByLibrary { "joinDiscord": MessageLookupByLibrary.simpleMessage("Join Discord"), "kindlyHelpUsWithThisInformation": MessageLookupByLibrary.simpleMessage("Pomóż nam z tą informacją"), + "locations": MessageLookupByLibrary.simpleMessage("Locations"), "logInLabel": MessageLookupByLibrary.simpleMessage("Zaloguj się"), "moderateStrength": MessageLookupByLibrary.simpleMessage("Umiarkowana"), "modifyYourQueryOrTrySearchingFor": diff --git a/mobile/lib/generated/intl/messages_pt.dart b/mobile/lib/generated/intl/messages_pt.dart index 6c3337ba9..4a2bfa26d 100644 --- a/mobile/lib/generated/intl/messages_pt.dart +++ b/mobile/lib/generated/intl/messages_pt.dart @@ -558,6 +558,7 @@ class MessageLookup extends MessageLookupByLibrary { "Excluir álbum compartilhado?"), "deleteSharedAlbumDialogBody": MessageLookupByLibrary.simpleMessage( "O álbum será apagado para todos\n\nVocê perderá o acesso a fotos compartilhadas neste álbum que pertencem aos outros"), + "descriptions": MessageLookupByLibrary.simpleMessage("Descriptions"), "deselectAll": MessageLookupByLibrary.simpleMessage("Desmarcar todos"), "designedToOutlive": MessageLookupByLibrary.simpleMessage("Feito para ter logenvidade"), @@ -871,6 +872,7 @@ class MessageLookup extends MessageLookupByLibrary { "locationName": MessageLookupByLibrary.simpleMessage("Nome do Local"), "locationTagFeatureDescription": MessageLookupByLibrary.simpleMessage( "Uma tag em grupo de todas as fotos que foram tiradas dentro de algum raio de uma foto"), + "locations": MessageLookupByLibrary.simpleMessage("Locations"), "lockButtonLabel": MessageLookupByLibrary.simpleMessage("Bloquear"), "lockScreenEnablePreSteps": MessageLookupByLibrary.simpleMessage( "Para ativar o bloqueio de tela, por favor ative um método de autenticação nas configurações do sistema do seu dispositivo."), @@ -984,6 +986,9 @@ class MessageLookup extends MessageLookupByLibrary { "orPickAnExistingOne": MessageLookupByLibrary.simpleMessage("Ou escolha um existente"), "pair": MessageLookupByLibrary.simpleMessage("Parear"), + "passkey": MessageLookupByLibrary.simpleMessage("Chave de acesso"), + "passkeyAuthTitle": MessageLookupByLibrary.simpleMessage( + "Autenticação via Chave de acesso"), "password": MessageLookupByLibrary.simpleMessage("Senha"), "passwordChangedSuccessfully": MessageLookupByLibrary.simpleMessage("Senha alterada com sucesso"), @@ -1208,8 +1213,6 @@ class MessageLookup extends MessageLookupByLibrary { "searchHint4": MessageLookupByLibrary.simpleMessage("Local"), "searchHint5": MessageLookupByLibrary.simpleMessage( "Em breve: Rostos e busca mágica ✨"), - "searchHintText": MessageLookupByLibrary.simpleMessage( - "Álbuns, meses, dias, anos, ..."), "searchLocationEmptySection": MessageLookupByLibrary.simpleMessage( "Fotos de grupo que estão sendo tiradas em algum raio da foto"), "searchPeopleEmptySection": MessageLookupByLibrary.simpleMessage( diff --git a/mobile/lib/generated/intl/messages_zh.dart b/mobile/lib/generated/intl/messages_zh.dart index 85b784799..4a056efe8 100644 --- a/mobile/lib/generated/intl/messages_zh.dart +++ b/mobile/lib/generated/intl/messages_zh.dart @@ -62,7 +62,7 @@ class MessageLookup extends MessageLookupByLibrary { static String m14(albumName) => "这将删除用于访问\"${albumName}\"的公共链接。"; - static String m15(supportEmail) => "请从您注册的电子邮件地址拖放一封邮件到 ${supportEmail}"; + static String m15(supportEmail) => "请从您注册的邮箱发送一封邮件到 ${supportEmail}"; static String m16(count, storageSaved) => "您已经清理了 ${Intl.plural(count, other: '${count} 个重复文件')}, 释放了 (${storageSaved}!)"; @@ -81,7 +81,7 @@ class MessageLookup extends MessageLookupByLibrary { "此相册中的 ${Intl.plural(count, one: '1 个文件', other: '${formattedNumber} 个文件')} 已安全备份"; static String m22(storageAmountInGB) => - "每当有人注册付费计划时${storageAmountInGB} GB 并应用了您的代码"; + "每当有人使用您的代码注册付费计划时您将获得${storageAmountInGB} GB"; static String m23(freeAmount, storageUnit) => "${freeAmount} ${storageUnit} 空闲"; @@ -126,7 +126,7 @@ class MessageLookup extends MessageLookupByLibrary { static String m40(storeName) => "在 ${storeName} 上给我们评分"; - static String m41(storageInGB) => "3. 你都可以免费获得 ${storageInGB} GB*"; + static String m41(storageInGB) => "3. 你和朋友都将免费获得 ${storageInGB} GB*"; static String m42(userEmail) => "${userEmail} 将从这个共享相册中删除\n\nTA们添加的任何照片也将从相册中删除"; @@ -143,10 +143,10 @@ class MessageLookup extends MessageLookupByLibrary { static String m47(verificationID) => "这是我的ente.io 的验证 ID: ${verificationID}。"; static String m48(verificationID) => - "嘿,你能确认这是你的 ente.io 验证 ID:${verificationID}"; + "嘿,你能确认这是你的 ente.io 验证 ID吗:${verificationID}"; static String m49(referralCode, referralStorageInGB) => - "ente转发码: ${referralCode} \n\n在设置 → 常规 → 推荐中应用它以在注册付费计划后可以免费获得 ${referralStorageInGB} GB\n\nhttps://ente.io"; + "ente推荐码: ${referralCode} \n\n注册付费计划后在设置 → 常规 → 推荐中应用它以免费获得 ${referralStorageInGB} GB空间\n\nhttps://ente.io"; static String m50(numberOfPeople) => "${Intl.plural(numberOfPeople, zero: '与特定人员共享', one: '与 1 人共享', other: '与 ${numberOfPeople} 人共享')}"; @@ -247,7 +247,7 @@ class MessageLookup extends MessageLookupByLibrary { "androidBiometricNotRecognized": MessageLookupByLibrary.simpleMessage("无法识别。请重试。"), "androidBiometricRequiredTitle": - MessageLookupByLibrary.simpleMessage("需要生物量"), + MessageLookupByLibrary.simpleMessage("需要生物识别认证"), "androidBiometricSuccess": MessageLookupByLibrary.simpleMessage("成功"), "androidCancelButton": MessageLookupByLibrary.simpleMessage("取消"), "androidDeviceCredentialsRequiredTitle": @@ -255,7 +255,7 @@ class MessageLookup extends MessageLookupByLibrary { "androidDeviceCredentialsSetupDescription": MessageLookupByLibrary.simpleMessage("需要设备凭据"), "androidGoToSettingsDescription": MessageLookupByLibrary.simpleMessage( - "未在您的设备上设置生物鉴别身份验证。前往“设置>安全”添加生物鉴别身份验证。"), + "您未在该设备上设置生物识别身份验证。前往“设置>安全”添加生物识别身份验证。"), "androidIosWebDesktop": MessageLookupByLibrary.simpleMessage("安卓, iOS, 网页端, 桌面端"), "androidSignInTitle": MessageLookupByLibrary.simpleMessage("需要身份验证"), @@ -286,7 +286,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("您删除账户的主要原因是什么?"), "askYourLovedOnesToShare": MessageLookupByLibrary.simpleMessage("请您的亲人分享"), - "atAFalloutShelter": MessageLookupByLibrary.simpleMessage("在一个保护所中"), + "atAFalloutShelter": MessageLookupByLibrary.simpleMessage("在一个庇护所中"), "authToChangeEmailVerificationSetting": MessageLookupByLibrary.simpleMessage("请进行身份验证以更改电子邮件验证"), "authToChangeLockscreenSetting": @@ -360,14 +360,14 @@ class MessageLookup extends MessageLookupByLibrary { "clickOnTheOverflowMenu": MessageLookupByLibrary.simpleMessage("• 点击溢出菜单"), "close": MessageLookupByLibrary.simpleMessage("关闭"), - "clubByCaptureTime": MessageLookupByLibrary.simpleMessage("按抓取时间断开"), + "clubByCaptureTime": MessageLookupByLibrary.simpleMessage("按拍摄时间分组"), "clubByFileName": MessageLookupByLibrary.simpleMessage("按文件名排序"), "codeAppliedPageTitle": MessageLookupByLibrary.simpleMessage("代码已应用"), "codeCopiedToClipboard": MessageLookupByLibrary.simpleMessage("代码已复制到剪贴板"), "codeUsedByYou": MessageLookupByLibrary.simpleMessage("您所使用的代码"), "collabLinkSectionDescription": MessageLookupByLibrary.simpleMessage( - "创建一个链接以允许人们在您的共享相册中添加和查看照片,而无需应用程序或账户。 非常适合收集活动照片。"), + "创建一个链接以允许其他人在您的共享相册中添加和查看照片,而无需应用程序或ente账户。 非常适合收集活动照片。"), "collaborativeLink": MessageLookupByLibrary.simpleMessage("协作链接"), "collaborativeLinkCreatedFor": m9, "collaborator": MessageLookupByLibrary.simpleMessage("协作者"), @@ -408,7 +408,7 @@ class MessageLookup extends MessageLookupByLibrary { "couldNotUpdateSubscription": MessageLookupByLibrary.simpleMessage("无法升级订阅"), "count": MessageLookupByLibrary.simpleMessage("计数"), - "crashReporting": MessageLookupByLibrary.simpleMessage("崩溃报告"), + "crashReporting": MessageLookupByLibrary.simpleMessage("上报崩溃"), "create": MessageLookupByLibrary.simpleMessage("创建"), "createAccount": MessageLookupByLibrary.simpleMessage("创建账户"), "createAlbumActionHint": @@ -427,7 +427,7 @@ class MessageLookup extends MessageLookupByLibrary { "dayYesterday": MessageLookupByLibrary.simpleMessage("昨天"), "decrypting": MessageLookupByLibrary.simpleMessage("解密中..."), "decryptingVideo": MessageLookupByLibrary.simpleMessage("正在解密视频..."), - "deduplicateFiles": MessageLookupByLibrary.simpleMessage("重复文件"), + "deduplicateFiles": MessageLookupByLibrary.simpleMessage("文件去重"), "delete": MessageLookupByLibrary.simpleMessage("删除"), "deleteAccount": MessageLookupByLibrary.simpleMessage("删除账户"), "deleteAccountFeedbackPrompt": @@ -438,7 +438,7 @@ class MessageLookup extends MessageLookupByLibrary { "deleteAlbumDialog": MessageLookupByLibrary.simpleMessage( "也删除此相册中存在的照片(和视频),从 他们所加入的所有 其他相册?"), "deleteAlbumsDialogBody": MessageLookupByLibrary.simpleMessage( - "这将删除所有空相册。 当您想减少相册列表中的混乱时,这很有用。"), + "这将删除所有空相册。 当您想减少相册列表的混乱时,这很有用。"), "deleteAll": MessageLookupByLibrary.simpleMessage("全部删除"), "deleteConfirmDialogBody": MessageLookupByLibrary.simpleMessage( "此账户已链接到其他 ente 旗下的应用程序(如果您使用任何 ente 旗下的应用程序)。\\n\\n您在所有 ente 旗下的应用程序中上传的数据将被安排删除,并且您的账户将被永久删除。"), @@ -456,7 +456,7 @@ class MessageLookup extends MessageLookupByLibrary { "deleteProgress": m13, "deleteReason1": MessageLookupByLibrary.simpleMessage("找不到我想要的功能"), "deleteReason2": - MessageLookupByLibrary.simpleMessage("应用或某个功能不会有 行为。我认为它应该有的"), + MessageLookupByLibrary.simpleMessage("应用或某个功能没有按我的预期运行"), "deleteReason3": MessageLookupByLibrary.simpleMessage("我找到了另一个我喜欢更好的服务"), "deleteReason4": MessageLookupByLibrary.simpleMessage("我的原因未被列出"), @@ -465,6 +465,7 @@ class MessageLookup extends MessageLookupByLibrary { "deleteSharedAlbum": MessageLookupByLibrary.simpleMessage("要删除共享相册吗?"), "deleteSharedAlbumDialogBody": MessageLookupByLibrary.simpleMessage( "将为所有人删除相册\n\n您将无法访问此相册中他人拥有的共享照片"), + "descriptions": MessageLookupByLibrary.simpleMessage("Descriptions"), "deselectAll": MessageLookupByLibrary.simpleMessage("取消全选"), "designedToOutlive": MessageLookupByLibrary.simpleMessage("经久耐用"), "details": MessageLookupByLibrary.simpleMessage("详情"), @@ -489,8 +490,8 @@ class MessageLookup extends MessageLookupByLibrary { "discord": MessageLookupByLibrary.simpleMessage("Discord"), "dismiss": MessageLookupByLibrary.simpleMessage("忽略"), "distanceInKMUnit": MessageLookupByLibrary.simpleMessage("公里"), - "doNotSignOut": MessageLookupByLibrary.simpleMessage("不要退登"), - "doThisLater": MessageLookupByLibrary.simpleMessage("稍后再做"), + "doNotSignOut": MessageLookupByLibrary.simpleMessage("不要登出"), + "doThisLater": MessageLookupByLibrary.simpleMessage("稍后再说"), "doYouWantToDiscardTheEditsYouHaveMade": MessageLookupByLibrary.simpleMessage("您想要放弃您所做的编辑吗?"), "done": MessageLookupByLibrary.simpleMessage("已完成"), @@ -559,11 +560,11 @@ class MessageLookup extends MessageLookupByLibrary { "exif": MessageLookupByLibrary.simpleMessage("EXIF"), "existingUser": MessageLookupByLibrary.simpleMessage("现有用户"), "expiredLinkInfo": - MessageLookupByLibrary.simpleMessage("此链接已过期。请选择新的过期时间或禁用链接过期。"), + MessageLookupByLibrary.simpleMessage("此链接已过期。请选择新的过期时间或禁用链接有效期。"), "exportLogs": MessageLookupByLibrary.simpleMessage("导出日志"), "exportYourData": MessageLookupByLibrary.simpleMessage("导出您的数据"), "faces": MessageLookupByLibrary.simpleMessage("人脸"), - "failedToApplyCode": MessageLookupByLibrary.simpleMessage("无法应用代码"), + "failedToApplyCode": MessageLookupByLibrary.simpleMessage("无法使用此代码"), "failedToCancel": MessageLookupByLibrary.simpleMessage("取消失败"), "failedToDownloadVideo": MessageLookupByLibrary.simpleMessage("视频下载失败"), "failedToFetchOriginalForEdit": @@ -575,7 +576,7 @@ class MessageLookup extends MessageLookupByLibrary { "failedToVerifyPaymentStatus": MessageLookupByLibrary.simpleMessage("验证支付状态失败"), "familyPlanOverview": MessageLookupByLibrary.simpleMessage( - "在您现有的计划中添加 5 名家庭成员,无需支付额外费用。\n\n每个成员都有自己的私人空间,除非共享,否则无法看到彼此的文件。\n\n家庭计划适用于付费订阅的客户。\n\n立即订阅以开始使用!"), + "在您现有的计划中添加 5 名家庭成员而无需支付额外费用。\n\n每个成员都有自己的私人空间,除非共享,否则无法看到彼此的文件。\n\n家庭计划适用于已有付费订阅的客户。\n\n立即订阅以开始使用!"), "familyPlanPortalTitle": MessageLookupByLibrary.simpleMessage("家庭"), "familyPlans": MessageLookupByLibrary.simpleMessage("家庭计划"), "faq": MessageLookupByLibrary.simpleMessage("常见问题"), @@ -614,7 +615,7 @@ class MessageLookup extends MessageLookupByLibrary { "goToSettings": MessageLookupByLibrary.simpleMessage("前往设置"), "googlePlayId": MessageLookupByLibrary.simpleMessage("Google Play ID"), "grantFullAccessPrompt": - MessageLookupByLibrary.simpleMessage("请在“设置”应用中将权限更改为允许访问所有所有照片"), + MessageLookupByLibrary.simpleMessage("请在手机“设置”中授权软件访问所有照片"), "grantPermission": MessageLookupByLibrary.simpleMessage("授予权限"), "groupNearbyPhotos": MessageLookupByLibrary.simpleMessage("将附近的照片分组"), "hearUsExplanation": MessageLookupByLibrary.simpleMessage( @@ -629,9 +630,9 @@ class MessageLookup extends MessageLookupByLibrary { "howToViewShareeVerificationID": MessageLookupByLibrary.simpleMessage( "请让他们在设置屏幕上长按他们的电子邮件地址,并验证两台设备上的 ID 是否匹配。"), "iOSGoToSettingsDescription": MessageLookupByLibrary.simpleMessage( - "未在您的设备上设置生物鉴别身份验证。请在您的手机上启用 Touch ID或Face ID。"), + "您未在该设备上设置生物识别身份验证。请在您的手机上启用 Touch ID或Face ID。"), "iOSLockOut": - MessageLookupByLibrary.simpleMessage("生物鉴别认证已禁用。请锁定并解锁您的屏幕以启用它。"), + MessageLookupByLibrary.simpleMessage("生物识别认证已禁用。请锁定并解锁您的屏幕以启用它。"), "iOSOkButton": MessageLookupByLibrary.simpleMessage("好的"), "ignoreUpdate": MessageLookupByLibrary.simpleMessage("忽略"), "ignoredFolderUploadReason": MessageLookupByLibrary.simpleMessage( @@ -691,9 +692,9 @@ class MessageLookup extends MessageLookupByLibrary { "livePhotos": MessageLookupByLibrary.simpleMessage("实况照片"), "loadMessage1": MessageLookupByLibrary.simpleMessage("您可以与家庭分享您的订阅"), "loadMessage2": - MessageLookupByLibrary.simpleMessage("到目前为止,我们已经保存了1 000多万个回忆"), + MessageLookupByLibrary.simpleMessage("到目前为止,我们已经保存了超过3 000万个回忆"), "loadMessage3": - MessageLookupByLibrary.simpleMessage("我们保存你的3个数据副本,一个在地下安全屋中"), + MessageLookupByLibrary.simpleMessage("我们保存你的3个数据副本,其中一个在地下安全屋中"), "loadMessage4": MessageLookupByLibrary.simpleMessage("我们所有的应用程序都是开源的"), "loadMessage5": MessageLookupByLibrary.simpleMessage("我们的源代码和加密技术已经由外部审计"), @@ -715,6 +716,7 @@ class MessageLookup extends MessageLookupByLibrary { "locationName": MessageLookupByLibrary.simpleMessage("地点名称"), "locationTagFeatureDescription": MessageLookupByLibrary.simpleMessage("位置标签将在照片的某个半径范围内拍摄的所有照片进行分组"), + "locations": MessageLookupByLibrary.simpleMessage("Locations"), "lockButtonLabel": MessageLookupByLibrary.simpleMessage("锁定"), "lockScreenEnablePreSteps": MessageLookupByLibrary.simpleMessage("要启用锁屏,请在系统设置中设置设备密码或屏幕锁定。"), @@ -722,7 +724,7 @@ class MessageLookup extends MessageLookupByLibrary { "logInLabel": MessageLookupByLibrary.simpleMessage("登录"), "loggingOut": MessageLookupByLibrary.simpleMessage("正在退出登录..."), "loginTerms": MessageLookupByLibrary.simpleMessage( - "点击登录后,我同意 服务条款隐私政策"), + "点击登录时,默认我同意 服务条款隐私政策"), "logout": MessageLookupByLibrary.simpleMessage("退出登录"), "logsDialogBody": MessageLookupByLibrary.simpleMessage( "这将跨日志发送以帮助我们调试您的问题。 请注意,将包含文件名以帮助跟踪特定文件的问题。"), @@ -732,7 +734,7 @@ class MessageLookup extends MessageLookupByLibrary { "machineLearning": MessageLookupByLibrary.simpleMessage("机器学习"), "magicSearch": MessageLookupByLibrary.simpleMessage("魔法搜索"), "magicSearchDescription": MessageLookupByLibrary.simpleMessage( - "请使用我们的桌面应用程序来为您库中的待处理项目建立索引。"), + "请注意,在所有项目完成索引之前,这将使用更高的带宽和电量。"), "manage": MessageLookupByLibrary.simpleMessage("管理"), "manageDeviceStorage": MessageLookupByLibrary.simpleMessage("管理设备存储"), "manageFamily": MessageLookupByLibrary.simpleMessage("管理家庭计划"), @@ -811,13 +813,15 @@ class MessageLookup extends MessageLookupByLibrary { "orPickAnExistingOne": MessageLookupByLibrary.simpleMessage("或者选择一个现有的"), "pair": MessageLookupByLibrary.simpleMessage("配对"), + "passkey": MessageLookupByLibrary.simpleMessage("通行密钥"), + "passkeyAuthTitle": MessageLookupByLibrary.simpleMessage("通行密钥认证"), "password": MessageLookupByLibrary.simpleMessage("密码"), "passwordChangedSuccessfully": MessageLookupByLibrary.simpleMessage("密码修改成功"), "passwordLock": MessageLookupByLibrary.simpleMessage("密码锁"), "passwordStrength": m34, "passwordWarning": MessageLookupByLibrary.simpleMessage( - "我们不储存这个密码,所以如果忘记, 我们不能解密您的数据"), + "我们不储存这个密码,所以如果忘记, 我们将无法解密您的数据"), "paymentDetails": MessageLookupByLibrary.simpleMessage("付款明细"), "paymentFailed": MessageLookupByLibrary.simpleMessage("支付失败"), "paymentFailedTalkToProvider": m35, @@ -896,11 +900,11 @@ class MessageLookup extends MessageLookupByLibrary { "如果您忘记了您的密码,您的恢复密钥是恢复您的照片的唯一途径。 您可以在“设置 > 账户”中找到您的恢复密钥。\n\n请在此输入您的恢复密钥以确认您已经正确地保存了它。"), "recoverySuccessful": MessageLookupByLibrary.simpleMessage("恢复成功!"), "recreatePasswordBody": MessageLookupByLibrary.simpleMessage( - "当前设备的功能不足以验证您的密码,但我们可以以适用于所有设备的方式重新生成。\n\n请使用您的恢复密钥登录并重新生成您的密码(如果您愿意,可以再次使用相同的密码)。"), + "当前设备的功能不足以验证您的密码,但我们可以以适用于所有设备的方式重新生成。\n\n请使用您的恢复密钥登录并重新生成您的密码(如果您希望,可以再次使用相同的密码)。"), "recreatePasswordTitle": MessageLookupByLibrary.simpleMessage("重新创建密码"), "reddit": MessageLookupByLibrary.simpleMessage("Reddit"), "referFriendsAnd2xYourPlan": - MessageLookupByLibrary.simpleMessage("推荐朋友和 2 倍您的计划"), + MessageLookupByLibrary.simpleMessage("把我们推荐给你的朋友然后获得延长一倍的订阅计划"), "referralStep1": MessageLookupByLibrary.simpleMessage("1. 将此代码提供给您的朋友"), "referralStep2": MessageLookupByLibrary.simpleMessage("2. 他们注册一个付费计划"), "referralStep3": m41, @@ -911,9 +915,9 @@ class MessageLookup extends MessageLookupByLibrary { "同时从“设置”->“存储”中清空“最近删除”以领取释放的空间"), "remindToEmptyEnteTrash": MessageLookupByLibrary.simpleMessage("同时清空您的“回收站”以领取释放的空间"), - "remoteImages": MessageLookupByLibrary.simpleMessage("远程图像"), - "remoteThumbnails": MessageLookupByLibrary.simpleMessage("远程缩略图"), - "remoteVideos": MessageLookupByLibrary.simpleMessage("远程视频"), + "remoteImages": MessageLookupByLibrary.simpleMessage("云端图像"), + "remoteThumbnails": MessageLookupByLibrary.simpleMessage("云端缩略图"), + "remoteVideos": MessageLookupByLibrary.simpleMessage("云端视频"), "remove": MessageLookupByLibrary.simpleMessage("移除"), "removeDuplicates": MessageLookupByLibrary.simpleMessage("移除重复内容"), "removeFromAlbum": MessageLookupByLibrary.simpleMessage("从相册中移除"), @@ -954,9 +958,9 @@ class MessageLookup extends MessageLookupByLibrary { "saveCopy": MessageLookupByLibrary.simpleMessage("保存副本"), "saveKey": MessageLookupByLibrary.simpleMessage("保存密钥"), "saveYourRecoveryKeyIfYouHaventAlready": - MessageLookupByLibrary.simpleMessage("如果你还没有就请保存你的恢复密钥"), + MessageLookupByLibrary.simpleMessage("若您尚未保存,请妥善保存此恢复密钥"), "saving": MessageLookupByLibrary.simpleMessage("正在保存..."), - "scanCode": MessageLookupByLibrary.simpleMessage("扫描代码"), + "scanCode": MessageLookupByLibrary.simpleMessage("扫描二维码/条码"), "scanThisBarcodeWithnyourAuthenticatorApp": MessageLookupByLibrary.simpleMessage("用您的身份验证器应用\n扫描此条码"), "searchAlbumsEmptySection": MessageLookupByLibrary.simpleMessage("相册"), @@ -976,7 +980,6 @@ class MessageLookup extends MessageLookupByLibrary { "searchHint3": MessageLookupByLibrary.simpleMessage("相册、文件名和类型"), "searchHint4": MessageLookupByLibrary.simpleMessage("位置"), "searchHint5": MessageLookupByLibrary.simpleMessage("即将到来:面部和魔法搜索✨"), - "searchHintText": MessageLookupByLibrary.simpleMessage("相册,月,日,年,..."), "searchLocationEmptySection": MessageLookupByLibrary.simpleMessage("在照片的一定半径内拍摄的几组照片"), "searchPeopleEmptySection": @@ -1058,7 +1061,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("它将从所有相册中删除。"), "singleFileInBothLocalAndRemote": m53, "singleFileInRemoteOnly": m54, - "skip": MessageLookupByLibrary.simpleMessage("略过"), + "skip": MessageLookupByLibrary.simpleMessage("跳过"), "social": MessageLookupByLibrary.simpleMessage("社交"), "someItemsAreInBothEnteAndYourDevice": MessageLookupByLibrary.simpleMessage("有些项目既在ente 也在您的设备中。"), @@ -1109,7 +1112,7 @@ class MessageLookup extends MessageLookupByLibrary { "syncProgress": m59, "syncStopped": MessageLookupByLibrary.simpleMessage("同步已停止"), "syncing": MessageLookupByLibrary.simpleMessage("正在同步···"), - "systemTheme": MessageLookupByLibrary.simpleMessage("系统"), + "systemTheme": MessageLookupByLibrary.simpleMessage("适应系统"), "tapToCopy": MessageLookupByLibrary.simpleMessage("点击以复制"), "tapToEnterCode": MessageLookupByLibrary.simpleMessage("点击以输入代码"), "tempErrorContactSupportIfPersists": @@ -1137,7 +1140,7 @@ class MessageLookup extends MessageLookupByLibrary { "thisAlbumAlreadyHDACollaborativeLink": MessageLookupByLibrary.simpleMessage("此相册已经有一个协作链接"), "thisCanBeUsedToRecoverYourAccountIfYou": - MessageLookupByLibrary.simpleMessage("如果您丢失了双因素,这可以用来恢复您的账户"), + MessageLookupByLibrary.simpleMessage("如果您丢失了双因素验证方式,这可以用来恢复您的账户"), "thisDevice": MessageLookupByLibrary.simpleMessage("此设备"), "thisEmailIsAlreadyInUse": MessageLookupByLibrary.simpleMessage("这个邮箱地址已经被使用"), diff --git a/mobile/lib/generated/l10n.dart b/mobile/lib/generated/l10n.dart index b19a95551..03b753399 100644 --- a/mobile/lib/generated/l10n.dart +++ b/mobile/lib/generated/l10n.dart @@ -5785,16 +5785,6 @@ class S { ); } - /// `Albums, months, days, years, ...` - String get searchHintText { - return Intl.message( - 'Albums, months, days, years, ...', - name: 'searchHintText', - desc: '', - args: [], - ); - } - /// `• Album names (e.g. "Camera")\n• Types of files (e.g. "Videos", ".gif")\n• Years and months (e.g. "2022", "January")\n• Holidays (e.g. "Christmas")\n• Photo descriptions (e.g. “#fun”)` String get searchByExamples { return Intl.message( @@ -8308,21 +8298,21 @@ class S { ); } - /// `Waiting for browser request...` - String get waitingForBrowserRequest { + /// `Remove all files from Uncategorized that are present in other albums` + String get cleanUncategorizedDescription { return Intl.message( - 'Waiting for browser request...', - name: 'waitingForBrowserRequest', + 'Remove all files from Uncategorized that are present in other albums', + name: 'cleanUncategorizedDescription', desc: '', args: [], ); } - /// `Launch passkey URL again` - String get launchPasskeyUrlAgain { + /// `Waiting for verification...` + String get waitingForVerification { return Intl.message( - 'Launch passkey URL again', - name: 'launchPasskeyUrlAgain', + 'Waiting for verification...', + name: 'waitingForVerification', desc: '', args: [], ); @@ -8338,16 +8328,26 @@ class S { ); } - /// `Passkey authentication` + /// `Passkey verification` String get passkeyAuthTitle { return Intl.message( - 'Passkey authentication', + 'Passkey verification', name: 'passkeyAuthTitle', desc: '', args: [], ); } + /// `Verify passkey` + String get verifyPasskey { + return Intl.message( + 'Verify passkey', + name: 'verifyPasskey', + desc: '', + args: [], + ); + } + /// `Play album on TV` String get playOnTv { return Intl.message( @@ -8407,6 +8407,26 @@ class S { args: [], ); } + + /// `Locations` + String get locations { + return Intl.message( + 'Locations', + name: 'locations', + desc: '', + args: [], + ); + } + + /// `Descriptions` + String get descriptions { + return Intl.message( + 'Descriptions', + name: 'descriptions', + desc: '', + args: [], + ); + } } class AppLocalizationDelegate extends LocalizationsDelegate { diff --git a/mobile/lib/l10n/intl_cs.arb b/mobile/lib/l10n/intl_cs.arb index 519dc2871..d9ce7ebeb 100644 --- a/mobile/lib/l10n/intl_cs.arb +++ b/mobile/lib/l10n/intl_cs.arb @@ -11,5 +11,7 @@ "selectALocationFirst": "Select a location first", "changeLocationOfSelectedItems": "Change location of selected items?", "editsToLocationWillOnlyBeSeenWithinEnte": "Edits to location will only be seen within Ente", - "joinDiscord": "Join Discord" + "joinDiscord": "Join Discord", + "locations": "Locations", + "descriptions": "Descriptions" } \ No newline at end of file diff --git a/mobile/lib/l10n/intl_de.arb b/mobile/lib/l10n/intl_de.arb index 0e99c86f8..50a4a1b21 100644 --- a/mobile/lib/l10n/intl_de.arb +++ b/mobile/lib/l10n/intl_de.arb @@ -406,6 +406,15 @@ }, "photoGridSize": "Fotorastergröße", "manageDeviceStorage": "Gerätespeicher verwalten", + "machineLearning": "Maschinelles Lernen", + "magicSearch": "Magische Suche", + "magicSearchDescription": "Bitte beachten Sie, dass dies mehr Bandbreite nutzt und zu einem höheren Akkuverbrauch führt, bis alle Elemente indiziert sind.", + "loadingModel": "Lade Modelle herunter...", + "waitingForWifi": "Warte auf WLAN...", + "status": "Status", + "indexedItems": "Indizierte Elemente", + "pendingItems": "Ausstehende Elemente", + "clearIndexes": "Indexe löschen", "selectFoldersForBackup": "Ordner für Sicherung auswählen", "selectedFoldersWillBeEncryptedAndBackedUp": "Ausgewählte Ordner werden verschlüsselt und gesichert", "unselectAll": "Alle demarkieren", @@ -810,7 +819,6 @@ "archiveAlbum": "Album archivieren", "calculating": "Wird berechnet...", "pleaseWaitDeletingAlbum": "Bitte warten, Album wird gelöscht", - "searchHintText": "Alben, Monate, Tage, Jahre, ...", "searchByExamples": "• Albumnamen (z.B. \"Kamera\")\n• Dateitypen (z.B. \"Videos\", \".gif\")\n• Jahre und Monate (z.B. \"2022\", \"Januar\")\n• Feiertage (z.B. \"Weihnachten\")\n• Fotobeschreibungen (z.B. \"#fun\")", "youCanTrySearchingForADifferentQuery": "Sie können versuchen, nach einer anderen Abfrage suchen.", "noResultsFound": "Keine Ergebnisse gefunden", @@ -1179,5 +1187,17 @@ "changeLocationOfSelectedItems": "Standort der gewählten Elemente ändern?", "editsToLocationWillOnlyBeSeenWithinEnte": "Änderungen des Standorts werden nur in ente sichtbar sein", "cleanUncategorized": "Unkategorisiert leeren", - "joinDiscord": "Join Discord" + "cleanUncategorizedDescription": "Entferne alle Dateien von \"Unkategorisiert\" die in anderen Alben vorhanden sind", + "waitingForVerification": "Warte auf Bestätigung...", + "passkey": "Passkey", + "passkeyAuthTitle": "Passkey-Verifizierung", + "verifyPasskey": "Passkey verifizieren", + "playOnTv": "Album auf dem Fernseher wiedergeben", + "pair": "Koppeln", + "deviceNotFound": "Gerät nicht gefunden", + "castInstruction": "Besuche cast.ente.io auf dem Gerät, das du verbinden möchtest.\n\nGib den unten angegebenen Code ein, um das Album auf deinem Fernseher abzuspielen.", + "deviceCodeHint": "Code eingeben", + "joinDiscord": "Discord beitreten", + "locations": "Orte", + "descriptions": "Beschreibungen" } \ No newline at end of file diff --git a/mobile/lib/l10n/intl_en.arb b/mobile/lib/l10n/intl_en.arb index e4ad661aa..16b203128 100644 --- a/mobile/lib/l10n/intl_en.arb +++ b/mobile/lib/l10n/intl_en.arb @@ -819,7 +819,6 @@ "archiveAlbum": "Archive album", "calculating": "Calculating...", "pleaseWaitDeletingAlbum": "Please wait, deleting album", - "searchHintText": "Albums, months, days, years, ...", "searchByExamples": "• Album names (e.g. \"Camera\")\n• Types of files (e.g. \"Videos\", \".gif\")\n• Years and months (e.g. \"2022\", \"January\")\n• Holidays (e.g. \"Christmas\")\n• Photo descriptions (e.g. “#fun”)", "youCanTrySearchingForADifferentQuery": "You can try searching for a different query.", "noResultsFound": "No results found", @@ -1188,14 +1187,17 @@ "changeLocationOfSelectedItems": "Change location of selected items?", "editsToLocationWillOnlyBeSeenWithinEnte": "Edits to location will only be seen within Ente", "cleanUncategorized": "Clean Uncategorized", - "waitingForBrowserRequest": "Waiting for browser request...", - "launchPasskeyUrlAgain": "Launch passkey URL again", + "cleanUncategorizedDescription": "Remove all files from Uncategorized that are present in other albums", + "waitingForVerification": "Waiting for verification...", "passkey": "Passkey", - "passkeyAuthTitle": "Passkey authentication", + "passkeyAuthTitle": "Passkey verification", + "verifyPasskey": "Verify passkey", "playOnTv": "Play album on TV", "pair": "Pair", "deviceNotFound": "Device not found", "castInstruction": "Visit cast.ente.io on the device you want to pair.\n\nEnter the code below to play the album on your TV.", "deviceCodeHint": "Enter the code", - "joinDiscord": "Join Discord" + "joinDiscord": "Join Discord", + "locations": "Locations", + "descriptions": "Descriptions" } \ No newline at end of file diff --git a/mobile/lib/l10n/intl_es.arb b/mobile/lib/l10n/intl_es.arb index d9f69970f..cbbb60a45 100644 --- a/mobile/lib/l10n/intl_es.arb +++ b/mobile/lib/l10n/intl_es.arb @@ -746,7 +746,6 @@ "archiveAlbum": "Archivar álbum", "calculating": "Calculando...", "pleaseWaitDeletingAlbum": "Por favor espere, borrando álbum", - "searchHintText": "Álbunes, meses, días, años, ...", "searchByExamples": "• Nombres de álbumes (por ejemplo, \"Cámara\")\n• Tipos de archivos (por ejemplo, \"Videos\", \".gif\")\n• Años y meses (por ejemplo, \"2022\", \"Enero\")\n• Vacaciones (por ejemplo, \"Navidad\")\n• Descripciones fotográficas (por ejemplo, \"#diversión\")", "youCanTrySearchingForADifferentQuery": "Puedes intentar buscar una consulta diferente.", "noResultsFound": "No se han encontrado resultados", @@ -974,5 +973,7 @@ "selectALocationFirst": "Select a location first", "changeLocationOfSelectedItems": "Change location of selected items?", "editsToLocationWillOnlyBeSeenWithinEnte": "Edits to location will only be seen within Ente", - "joinDiscord": "Join Discord" + "joinDiscord": "Join Discord", + "locations": "Locations", + "descriptions": "Descriptions" } \ No newline at end of file diff --git a/mobile/lib/l10n/intl_fr.arb b/mobile/lib/l10n/intl_fr.arb index 87d792c7b..f0afcaa98 100644 --- a/mobile/lib/l10n/intl_fr.arb +++ b/mobile/lib/l10n/intl_fr.arb @@ -794,7 +794,6 @@ "archiveAlbum": "Archiver l'album", "calculating": "Calcul en cours...", "pleaseWaitDeletingAlbum": "Veuillez patienter, suppression de l'album", - "searchHintText": "Albums, mois, jours, années, ...", "searchByExamples": "• Noms d'albums (par exemple \"Caméra\")\n• Types de fichiers (par exemple \"Vidéos\", \".gif\")\n• Années et mois (par exemple \"2022\", \"Janvier\")\n• Vacances (par exemple \"Noël\")\n• Descriptions de photos (par exemple \"#fun\")", "youCanTrySearchingForADifferentQuery": "Vous pouvez essayer de rechercher une autre requête.", "noResultsFound": "Aucun résultat trouvé", @@ -1155,5 +1154,7 @@ "selectALocationFirst": "Select a location first", "changeLocationOfSelectedItems": "Change location of selected items?", "editsToLocationWillOnlyBeSeenWithinEnte": "Edits to location will only be seen within Ente", - "joinDiscord": "Join Discord" + "joinDiscord": "Join Discord", + "locations": "Locations", + "descriptions": "Descriptions" } \ No newline at end of file diff --git a/mobile/lib/l10n/intl_it.arb b/mobile/lib/l10n/intl_it.arb index 83fa1d054..be0dc1e2c 100644 --- a/mobile/lib/l10n/intl_it.arb +++ b/mobile/lib/l10n/intl_it.arb @@ -794,7 +794,6 @@ "archiveAlbum": "Archivia album", "calculating": "Calcolando...", "pleaseWaitDeletingAlbum": "Attendere, sto eliminando l'album", - "searchHintText": "Album, mesi, giorni, anni, ...", "searchByExamples": "• Nomi degli album (es. \"Camera\")\n• Tipi di file (es. \"Video\", \".gif\")\n• Anni e mesi (e.. \"2022\", \"gennaio\")\n• Vacanze (ad es. \"Natale\")\n• Descrizioni delle foto (ad es. “#mare”)", "youCanTrySearchingForADifferentQuery": "Prova con una ricerca differente.", "noResultsFound": "Nessun risultato trovato", @@ -1117,5 +1116,7 @@ "selectALocationFirst": "Select a location first", "changeLocationOfSelectedItems": "Change location of selected items?", "editsToLocationWillOnlyBeSeenWithinEnte": "Edits to location will only be seen within Ente", - "joinDiscord": "Join Discord" + "joinDiscord": "Join Discord", + "locations": "Locations", + "descriptions": "Descriptions" } \ No newline at end of file diff --git a/mobile/lib/l10n/intl_ko.arb b/mobile/lib/l10n/intl_ko.arb index 519dc2871..d9ce7ebeb 100644 --- a/mobile/lib/l10n/intl_ko.arb +++ b/mobile/lib/l10n/intl_ko.arb @@ -11,5 +11,7 @@ "selectALocationFirst": "Select a location first", "changeLocationOfSelectedItems": "Change location of selected items?", "editsToLocationWillOnlyBeSeenWithinEnte": "Edits to location will only be seen within Ente", - "joinDiscord": "Join Discord" + "joinDiscord": "Join Discord", + "locations": "Locations", + "descriptions": "Descriptions" } \ No newline at end of file diff --git a/mobile/lib/l10n/intl_nl.arb b/mobile/lib/l10n/intl_nl.arb index ef9d9e433..0f2ab0ec2 100644 --- a/mobile/lib/l10n/intl_nl.arb +++ b/mobile/lib/l10n/intl_nl.arb @@ -819,7 +819,6 @@ "archiveAlbum": "Album archiveren", "calculating": "Berekenen...", "pleaseWaitDeletingAlbum": "Een ogenblik geduld, album wordt verwijderd", - "searchHintText": "Albums, maanden, dagen, jaren, ...", "searchByExamples": "• Albumnamen (bijv. \"Camera\")\n• Types van bestanden (bijv. \"Video's\", \".gif\")\n• Jaren en maanden (bijv. \"2022\", \"januari\")\n• Feestdagen (bijv. \"Kerstmis\")\n• Fotobeschrijvingen (bijv. \"#fun\")", "youCanTrySearchingForADifferentQuery": "U kunt proberen een andere zoekopdracht te vinden.", "noResultsFound": "Geen resultaten gevonden", @@ -1193,5 +1192,7 @@ "deviceNotFound": "Apparaat niet gevonden", "castInstruction": "Bezoek cast.ente.io op het apparaat dat u wilt koppelen.\n\nVoer de code hieronder in om het album op uw TV af te spelen.", "deviceCodeHint": "Voer de code in", - "joinDiscord": "Join Discord" + "joinDiscord": "Join Discord", + "locations": "Locations", + "descriptions": "Descriptions" } \ No newline at end of file diff --git a/mobile/lib/l10n/intl_no.arb b/mobile/lib/l10n/intl_no.arb index ec336dba8..8d40561fc 100644 --- a/mobile/lib/l10n/intl_no.arb +++ b/mobile/lib/l10n/intl_no.arb @@ -25,5 +25,7 @@ "selectALocationFirst": "Select a location first", "changeLocationOfSelectedItems": "Change location of selected items?", "editsToLocationWillOnlyBeSeenWithinEnte": "Edits to location will only be seen within Ente", - "joinDiscord": "Join Discord" + "joinDiscord": "Join Discord", + "locations": "Locations", + "descriptions": "Descriptions" } \ No newline at end of file diff --git a/mobile/lib/l10n/intl_pl.arb b/mobile/lib/l10n/intl_pl.arb index 183e2b5bd..6bd5a30aa 100644 --- a/mobile/lib/l10n/intl_pl.arb +++ b/mobile/lib/l10n/intl_pl.arb @@ -112,5 +112,7 @@ "selectALocationFirst": "Select a location first", "changeLocationOfSelectedItems": "Change location of selected items?", "editsToLocationWillOnlyBeSeenWithinEnte": "Edits to location will only be seen within Ente", - "joinDiscord": "Join Discord" + "joinDiscord": "Join Discord", + "locations": "Locations", + "descriptions": "Descriptions" } \ No newline at end of file diff --git a/mobile/lib/l10n/intl_pt.arb b/mobile/lib/l10n/intl_pt.arb index 486129fa7..bf0e53409 100644 --- a/mobile/lib/l10n/intl_pt.arb +++ b/mobile/lib/l10n/intl_pt.arb @@ -819,7 +819,6 @@ "archiveAlbum": "Arquivar álbum", "calculating": "Calculando...", "pleaseWaitDeletingAlbum": "Por favor, aguarde, excluindo álbum", - "searchHintText": "Álbuns, meses, dias, anos, ...", "searchByExamples": "• Nomes de álbuns (ex: \"Câmera\")\n• Tipos de arquivos (ex.: \"Vídeos\", \".gif\")\n• Anos e meses (e.. \"2022\", \"Janeiro\")\n• Feriados (por exemplo, \"Natal\")\n• Descrições de fotos (por exemplo, \"#divertido\")", "youCanTrySearchingForADifferentQuery": "Você pode tentar procurar uma consulta diferente.", "noResultsFound": "Nenhum resultado encontrado", @@ -1188,10 +1187,17 @@ "changeLocationOfSelectedItems": "Alterar o local dos itens selecionados?", "editsToLocationWillOnlyBeSeenWithinEnte": "Edições para local só serão vistas dentro do Ente", "cleanUncategorized": "Limpar Sem Categoria", + "cleanUncategorizedDescription": "Remover todos os arquivos de Não Categorizados que estão presentes em outros álbuns", + "waitingForVerification": "Esperando por verificação...", + "passkey": "Chave de acesso", + "passkeyAuthTitle": "Autenticação via Chave de acesso", + "verifyPasskey": "Verificar chave de acesso", "playOnTv": "Reproduzir álbum na TV", "pair": "Parear", "deviceNotFound": "Dispositivo não encontrado", "castInstruction": "Visite cast.ente.io no dispositivo que você deseja parear.\n\ndigite o código abaixo para reproduzir o álbum em sua TV.", "deviceCodeHint": "Insira o código", - "joinDiscord": "Junte-se ao Discord" + "joinDiscord": "Junte-se ao Discord", + "locations": "Locais", + "descriptions": "Descrições" } \ No newline at end of file diff --git a/mobile/lib/l10n/intl_zh.arb b/mobile/lib/l10n/intl_zh.arb index e3420317a..4e9e9e4eb 100644 --- a/mobile/lib/l10n/intl_zh.arb +++ b/mobile/lib/l10n/intl_zh.arb @@ -17,7 +17,7 @@ "yourAccountHasBeenDeleted": "您的账户已删除", "selectReason": "选择原因", "deleteReason1": "找不到我想要的功能", - "deleteReason2": "应用或某个功能不会有 行为。我认为它应该有的", + "deleteReason2": "应用或某个功能没有按我的预期运行", "deleteReason3": "我找到了另一个我喜欢更好的服务", "deleteReason4": "我的原因未被列出", "sendEmail": "发送电子邮件", @@ -67,7 +67,7 @@ "changePasswordTitle": "修改密码", "resetPasswordTitle": "重置密码", "encryptionKeys": "加密密钥", - "passwordWarning": "我们不储存这个密码,所以如果忘记, 我们不能解密您的数据", + "passwordWarning": "我们不储存这个密码,所以如果忘记, 我们将无法解密您的数据", "enterPasswordToEncrypt": "输入我们可以用来加密您的数据的密码", "enterNewPasswordToEncrypt": "输入我们可以用来加密您的数据的新密码", "weakStrength": "弱", @@ -98,7 +98,7 @@ "termsOfServicesTitle": "使用条款", "signUpTerms": "我同意 服务条款隐私政策", "logInLabel": "登录", - "loginTerms": "点击登录后,我同意 服务条款隐私政策", + "loginTerms": "点击登录时,默认我同意 服务条款隐私政策", "changeEmail": "修改邮箱", "enterYourPassword": "输入您的密码", "welcomeBack": "欢迎回来!", @@ -107,17 +107,17 @@ "pleaseTryAgain": "请重试", "recreatePasswordTitle": "重新创建密码", "useRecoveryKey": "使用恢复密钥", - "recreatePasswordBody": "当前设备的功能不足以验证您的密码,但我们可以以适用于所有设备的方式重新生成。\n\n请使用您的恢复密钥登录并重新生成您的密码(如果您愿意,可以再次使用相同的密码)。", + "recreatePasswordBody": "当前设备的功能不足以验证您的密码,但我们可以以适用于所有设备的方式重新生成。\n\n请使用您的恢复密钥登录并重新生成您的密码(如果您希望,可以再次使用相同的密码)。", "verifyPassword": "验证密码", "recoveryKey": "恢复密钥", "recoveryKeyOnForgotPassword": "如果您忘记了密码,恢复数据的唯一方法就是使用此密钥。", "recoveryKeySaveDescription": "我们不会存储此密钥,请将此24个单词密钥保存在一个安全的地方。", - "doThisLater": "稍后再做", + "doThisLater": "稍后再说", "saveKey": "保存密钥", "recoveryKeyCopiedToClipboard": "恢复密钥已复制到剪贴板", "recoverAccount": "恢复账户", "recover": "恢复", - "dropSupportEmail": "请从您注册的电子邮件地址拖放一封邮件到 {supportEmail}", + "dropSupportEmail": "请从您注册的邮箱发送一封邮件到 {supportEmail}", "@dropSupportEmail": { "placeholders": { "supportEmail": { @@ -129,7 +129,7 @@ }, "twofactorSetup": "双因素认证设置", "enterCode": "输入代码", - "scanCode": "扫描代码", + "scanCode": "扫描二维码/条码", "codeCopiedToClipboard": "代码已复制到剪贴板", "copypasteThisCodentoYourAuthenticatorApp": "请复制粘贴此代码\n到您的身份验证器应用程序上", "tapToCopy": "点击以复制", @@ -137,8 +137,8 @@ "enterThe6digitCodeFromnyourAuthenticatorApp": "从你的身份验证器应用中\n输入6位数字代码", "confirm": "确认", "setupComplete": "设置完成", - "saveYourRecoveryKeyIfYouHaventAlready": "如果你还没有就请保存你的恢复密钥", - "thisCanBeUsedToRecoverYourAccountIfYou": "如果您丢失了双因素,这可以用来恢复您的账户", + "saveYourRecoveryKeyIfYouHaventAlready": "若您尚未保存,请妥善保存此恢复密钥", + "thisCanBeUsedToRecoverYourAccountIfYou": "如果您丢失了双因素验证方式,这可以用来恢复您的账户", "twofactorAuthenticationPageTitle": "双因素认证", "lostDevice": "丢失了设备吗?", "verifyingRecoveryKey": "正在验证恢复密钥...", @@ -196,7 +196,7 @@ "linkExpired": "已过期", "linkEnabled": "已启用", "linkNeverExpires": "永不", - "expiredLinkInfo": "此链接已过期。请选择新的过期时间或禁用链接过期。", + "expiredLinkInfo": "此链接已过期。请选择新的过期时间或禁用链接有效期。", "setAPassword": "设置密码", "lockButtonLabel": "锁定", "enterPassword": "输入密码", @@ -225,7 +225,7 @@ }, "description": "Number of participants in an album, including the album owner." }, - "collabLinkSectionDescription": "创建一个链接以允许人们在您的共享相册中添加和查看照片,而无需应用程序或账户。 非常适合收集活动照片。", + "collabLinkSectionDescription": "创建一个链接以允许其他人在您的共享相册中添加和查看照片,而无需应用程序或ente账户。 非常适合收集活动照片。", "collectPhotos": "收集照片", "collaborativeLink": "协作链接", "shareWithNonenteUsers": "与非ente 用户分享", @@ -261,7 +261,7 @@ "verifyEmailID": "验证 {email}", "emailNoEnteAccount": "{email} 没有 ente 账户。\n\n向他们发送分享照片的邀请。", "shareMyVerificationID": "这是我的ente.io 的验证 ID: {verificationID}。", - "shareTextConfirmOthersVerificationID": "嘿,你能确认这是你的 ente.io 验证 ID:{verificationID}", + "shareTextConfirmOthersVerificationID": "嘿,你能确认这是你的 ente.io 验证 ID吗:{verificationID}", "somethingWentWrong": "出了些问题", "sendInvite": "发送邀请", "shareTextRecommendUsingEnte": "下载 ente,以便我们轻松分享原始质量的照片和视频\n\nhttps://ente.io", @@ -269,7 +269,7 @@ "applyCodeTitle": "应用代码", "enterCodeDescription": "输入您的朋友提供的代码来为您申请免费存储", "apply": "应用", - "failedToApplyCode": "无法应用代码", + "failedToApplyCode": "无法使用此代码", "enterReferralCode": "输入推荐代码", "codeAppliedPageTitle": "代码已应用", "storageInGB": "{storageAmountInGB} GB", @@ -280,14 +280,14 @@ "details": "详情", "claimMore": "领取更多!", "theyAlsoGetXGb": "他们也会获得 {storageAmountInGB} GB", - "freeStorageOnReferralSuccess": "每当有人注册付费计划时{storageAmountInGB} GB 并应用了您的代码", - "shareTextReferralCode": "ente转发码: {referralCode} \n\n在设置 → 常规 → 推荐中应用它以在注册付费计划后可以免费获得 {referralStorageInGB} GB\n\nhttps://ente.io", + "freeStorageOnReferralSuccess": "每当有人使用您的代码注册付费计划时您将获得{storageAmountInGB} GB", + "shareTextReferralCode": "ente推荐码: {referralCode} \n\n注册付费计划后在设置 → 常规 → 推荐中应用它以免费获得 {referralStorageInGB} GB空间\n\nhttps://ente.io", "claimFreeStorage": "领取免费存储", "inviteYourFriends": "邀请您的朋友", "failedToFetchReferralDetails": "无法获取引荐详细信息。 请稍后再试。", "referralStep1": "1. 将此代码提供给您的朋友", "referralStep2": "2. 他们注册一个付费计划", - "referralStep3": "3. 你都可以免费获得 {storageInGB} GB*", + "referralStep3": "3. 你和朋友都将免费获得 {storageInGB} GB*", "referralsAreCurrentlyPaused": "推荐已暂停", "youCanAtMaxDoubleYourStorage": "* 您最多可以将您的存储空间增加一倍", "claimedStorageSoFar": "{isFamilyMember, select, true {到目前为止,您的家庭已经领取了 {storageAmountInGb} GB} false {到目前为止,您已经领取了 {storageAmountInGb} GB} other {到目前为止,您已经领取了{storageAmountInGb} GB}}", @@ -408,7 +408,7 @@ "manageDeviceStorage": "管理设备存储", "machineLearning": "机器学习", "magicSearch": "魔法搜索", - "magicSearchDescription": "请使用我们的桌面应用程序来为您库中的待处理项目建立索引。", + "magicSearchDescription": "请注意,在所有项目完成索引之前,这将使用更高的带宽和电量。", "loadingModel": "正在下载模型...", "waitingForWifi": "正在等待 WiFi...", "status": "状态", @@ -419,7 +419,7 @@ "selectedFoldersWillBeEncryptedAndBackedUp": "所选文件夹将被加密和备份", "unselectAll": "取消全部选择", "selectAll": "全选", - "skip": "略过", + "skip": "跳过", "updatingFolderSelection": "正在更新文件夹选择...", "itemCount": "{count, plural, one{{count} 个项目} other{{count} 个项目}}", "deleteItemCount": "{count, plural, =1 {删除 {count} 个项目} other {删除 {count} 个项目}}", @@ -550,7 +550,7 @@ "theme": "主题", "lightTheme": "浅色", "darkTheme": "深色", - "systemTheme": "系统", + "systemTheme": "适应系统", "freeTrial": "免费试用", "selectYourPlan": "选择您的计划", "enteSubscriptionPitch": "ente 会保留您的回忆,因此即使您丢失了设备,它们也始终可供您使用。", @@ -650,7 +650,7 @@ "startBackup": "开始备份", "noPhotosAreBeingBackedUpRightNow": "目前没有照片正在备份", "preserveMore": "保留更多", - "grantFullAccessPrompt": "请在“设置”应用中将权限更改为允许访问所有所有照片", + "grantFullAccessPrompt": "请在手机“设置”中授权软件访问所有照片", "openSettings": "打开“设置”", "selectMorePhotos": "选择更多照片", "existingUser": "现有用户", @@ -658,7 +658,7 @@ "forYourMemories": "为您的回忆", "endtoendEncryptedByDefault": "默认端到端加密", "safelyStored": "安全存储", - "atAFalloutShelter": "在一个保护所中", + "atAFalloutShelter": "在一个庇护所中", "designedToOutlive": "经久耐用", "available": "可用", "everywhere": "随时随地", @@ -703,7 +703,7 @@ "lastUpdated": "最后更新", "deleteEmptyAlbums": "删除空相册", "deleteEmptyAlbumsWithQuestionMark": "要删除空相册吗?", - "deleteAlbumsDialogBody": "这将删除所有空相册。 当您想减少相册列表中的混乱时,这很有用。", + "deleteAlbumsDialogBody": "这将删除所有空相册。 当您想减少相册列表的混乱时,这很有用。", "deleteProgress": "正在删除 {currentlyDeleting} /共 {totalCount}", "genericProgress": "正在处理 {currentlyProcessing} / {totalCount}", "@genericProgress": { @@ -776,7 +776,7 @@ "sharedWithMe": "与我共享", "sharedByMe": "由我共享的", "doubleYourStorage": "将您的存储空间增加一倍", - "referFriendsAnd2xYourPlan": "推荐朋友和 2 倍您的计划", + "referFriendsAnd2xYourPlan": "把我们推荐给你的朋友然后获得延长一倍的订阅计划", "shareAlbumHint": "打开相册并点击右上角的分享按钮进行分享", "itemsShowTheNumberOfDaysRemainingBeforePermanentDeletion": "项目显示永久删除前剩余的天数", "trashDaysLeft": "{count, plural, =0 {} =1 {1天} other {{count} 天}}", @@ -819,7 +819,6 @@ "archiveAlbum": "存档相册", "calculating": "正在计算...", "pleaseWaitDeletingAlbum": "请稍候,正在删除相册", - "searchHintText": "相册,月,日,年,...", "searchByExamples": "• 相册名称(例如“相机”)\n• 文件类型(例如“视频”、“.gif”)\n• 年份和月份(例如“2022”、“一月”)\n• 假期(例如“圣诞节”)\n• 照片说明(例如“#和女儿独居,好开心啊”)", "youCanTrySearchingForADifferentQuery": "您可以尝试搜索不同的查询。", "noResultsFound": "未找到任何结果", @@ -840,10 +839,10 @@ "pressAndHoldToPlayVideo": "按住以播放视频", "pressAndHoldToPlayVideoDetailed": "长按图像以播放视频", "downloadFailed": "下載失敗", - "deduplicateFiles": "重复文件", + "deduplicateFiles": "文件去重", "deselectAll": "取消全选", "reviewDeduplicateItems": "请检查并删除您认为重复的项目。", - "clubByCaptureTime": "按抓取时间断开", + "clubByCaptureTime": "按拍摄时间分组", "clubByFileName": "按文件名排序", "count": "计数", "totalSize": "总大小", @@ -956,9 +955,9 @@ "networkConnectionRefusedErr": "无法连接到 Ente,请稍后重试。如果错误仍然存在,请联系支持人员。", "cachedData": "缓存数据", "clearCaches": "清除缓存", - "remoteImages": "远程图像", - "remoteVideos": "远程视频", - "remoteThumbnails": "远程缩略图", + "remoteImages": "云端图像", + "remoteVideos": "云端视频", + "remoteThumbnails": "云端缩略图", "pendingSync": "正在等待同步", "localGallery": "本地相册", "todaysLogs": "当天日志", @@ -974,8 +973,8 @@ "didYouKnow": "您知道吗?", "loadingMessage": "正在加载您的照片...", "loadMessage1": "您可以与家庭分享您的订阅", - "loadMessage2": "到目前为止,我们已经保存了1 000多万个回忆", - "loadMessage3": "我们保存你的3个数据副本,一个在地下安全屋中", + "loadMessage2": "到目前为止,我们已经保存了超过3 000万个回忆", + "loadMessage3": "我们保存你的3个数据副本,其中一个在地下安全屋中", "loadMessage4": "我们所有的应用程序都是开源的", "loadMessage5": "我们的源代码和加密技术已经由外部审计", "loadMessage6": "您可以与您所爱的人分享您相册的链接", @@ -1052,7 +1051,7 @@ }, "setRadius": "设定半径", "familyPlanPortalTitle": "家庭", - "familyPlanOverview": "在您现有的计划中添加 5 名家庭成员,无需支付额外费用。\n\n每个成员都有自己的私人空间,除非共享,否则无法看到彼此的文件。\n\n家庭计划适用于付费订阅的客户。\n\n立即订阅以开始使用!", + "familyPlanOverview": "在您现有的计划中添加 5 名家庭成员而无需支付额外费用。\n\n每个成员都有自己的私人空间,除非共享,否则无法看到彼此的文件。\n\n家庭计划适用于已有付费订阅的客户。\n\n立即订阅以开始使用!", "androidBiometricHint": "验证身份", "@androidBiometricHint": { "description": "Hint message advising the user how to authenticate with biometrics. It is used on Android side. Maximum 60 characters." @@ -1073,7 +1072,7 @@ "@androidSignInTitle": { "description": "Message showed as a title in a dialog which indicates the user that they need to scan biometric to continue. It is used on Android side. Maximum 60 characters." }, - "androidBiometricRequiredTitle": "需要生物量", + "androidBiometricRequiredTitle": "需要生物识别认证", "@androidBiometricRequiredTitle": { "description": "Message showed as a title in a dialog which indicates the user has not set up biometric authentication on their device. It is used on Android side. Maximum 60 characters." }, @@ -1089,15 +1088,15 @@ "@goToSettings": { "description": "Message showed on a button that the user can click to go to settings pages from the current dialog. It is used on both Android and iOS side. Maximum 30 characters." }, - "androidGoToSettingsDescription": "未在您的设备上设置生物鉴别身份验证。前往“设置>安全”添加生物鉴别身份验证。", + "androidGoToSettingsDescription": "您未在该设备上设置生物识别身份验证。前往“设置>安全”添加生物识别身份验证。", "@androidGoToSettingsDescription": { "description": "Message advising the user to go to the settings and configure biometric on their device. It shows in a dialog on Android side." }, - "iOSLockOut": "生物鉴别认证已禁用。请锁定并解锁您的屏幕以启用它。", + "iOSLockOut": "生物识别认证已禁用。请锁定并解锁您的屏幕以启用它。", "@iOSLockOut": { "description": "Message advising the user to re-enable biometrics on their device. It shows in a dialog on iOS side." }, - "iOSGoToSettingsDescription": "未在您的设备上设置生物鉴别身份验证。请在您的手机上启用 Touch ID或Face ID。", + "iOSGoToSettingsDescription": "您未在该设备上设置生物识别身份验证。请在您的手机上启用 Touch ID或Face ID。", "@iOSGoToSettingsDescription": { "description": "Message advising the user to go to the settings and configure Biometrics for their device. It shows in a dialog on iOS side." }, @@ -1136,7 +1135,7 @@ "unhiding": "正在取消隐藏...", "successfullyHid": "已成功隐藏", "successfullyUnhid": "已成功取消隐藏", - "crashReporting": "崩溃报告", + "crashReporting": "上报崩溃", "addToHiddenAlbum": "添加到隐藏相册", "moveToHiddenAlbum": "移至隐藏相册", "fileTypes": "文件类型", @@ -1181,17 +1180,24 @@ "signOutFromOtherDevices": "从其他设备退出登录", "signOutOtherBody": "如果你认为有人可能知道你的密码,你可以强制所有使用你账户的其他设备退出登录。", "signOutOtherDevices": "登出其他设备", - "doNotSignOut": "不要退登", + "doNotSignOut": "不要登出", "editLocation": "编辑位置", "selectALocation": "选择一个位置", "selectALocationFirst": "首先选择一个位置", "changeLocationOfSelectedItems": "确定要更改所选项目的位置吗?", "editsToLocationWillOnlyBeSeenWithinEnte": "对位置的编辑只能在 Ente 内看到", "cleanUncategorized": "清除未分类的", + "cleanUncategorizedDescription": "从“未分类”中删除其他相册中存在的所有文件", + "waitingForVerification": "等待验证...", + "passkey": "通行密钥", + "passkeyAuthTitle": "通行密钥认证", + "verifyPasskey": "验证通行密钥", "playOnTv": "在电视上播放相册", "pair": "配对", "deviceNotFound": "未发现设备", "castInstruction": "在您要配对的设备上访问 cast.ente.io。\n输入下面的代码即可在电视上播放相册。", "deviceCodeHint": "输入代码", - "joinDiscord": "加入 Discord" + "joinDiscord": "加入 Discord", + "locations": "位置", + "descriptions": "描述" } \ No newline at end of file diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart index 7e639891f..49dbc01b1 100644 --- a/mobile/lib/main.dart +++ b/mobile/lib/main.dart @@ -27,6 +27,7 @@ import 'package:photos/services/collections_service.dart'; import "package:photos/services/entity_service.dart"; import 'package:photos/services/favorites_service.dart'; import 'package:photos/services/feature_flag_service.dart'; +import 'package:photos/services/home_widget_service.dart'; import 'package:photos/services/local_file_update_service.dart'; import 'package:photos/services/local_sync_service.dart'; import "package:photos/services/location_service.dart"; @@ -46,10 +47,8 @@ import 'package:photos/ui/tools/app_lock.dart'; import 'package:photos/ui/tools/lock_screen.dart'; import 'package:photos/utils/crypto_util.dart'; import 'package:photos/utils/file_uploader.dart'; -import "package:photos/utils/home_widget_util.dart"; import 'package:photos/utils/local_settings.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import "package:workmanager/workmanager.dart"; final _logger = Logger("main"); @@ -58,55 +57,17 @@ const kLastBGTaskHeartBeatTime = "bg_task_hb_time"; const kLastFGTaskHeartBeatTime = "fg_task_hb_time"; const kHeartBeatFrequency = Duration(seconds: 1); const kFGSyncFrequency = Duration(minutes: 5); +const kFGHomeWidgetSyncFrequency = Duration(minutes: 15); const kBGTaskTimeout = Duration(seconds: 25); const kBGPushTimeout = Duration(seconds: 28); const kFGTaskDeathTimeoutInMicroseconds = 5000000; const kBackgroundLockLatency = Duration(seconds: 3); -@pragma("vm:entry-point") -void initSlideshowWidget() { - Workmanager().executeTask( - (taskName, inputData) async { - try { - if (await countHomeWidgets() != 0) { - await _init(true, via: 'runViaSlideshowWidget'); - await initHomeWidget(); - } - return true; - } catch (e, s) { - _logger.severe("Error in initSlideshowWidget", e, s); - return false; - } - }, - ); -} - -Future initWorkmanager() async { - await Workmanager() - .initialize(initSlideshowWidget, isInDebugMode: kDebugMode); - await Workmanager().registerPeriodicTask( - "slideshow-widget", - "updateSlideshowWidget", - initialDelay: const Duration(seconds: 10), - frequency: const Duration( - minutes: 15, - ), - ); -} - void main() async { debugRepaintRainbowEnabled = false; WidgetsFlutterBinding.ensureInitialized(); MediaKit.ensureInitialized(); - if (Platform.isAndroid) { - unawaited( - initWorkmanager().catchError((e, s) { - _logger.severe("Error in initWorkmanager", e, s); - }), - ); - } - final savedThemeMode = await AdaptiveTheme.getThemeMode(); await _runInForeground(savedThemeMode); unawaited(BackgroundFetch.registerHeadlessTask(_headlessTaskHandler)); @@ -118,6 +79,9 @@ Future _runInForeground(AdaptiveThemeMode? savedThemeMode) async { _logger.info("Starting app in foreground"); await _init(false, via: 'mainMethod'); final Locale locale = await getLocale(); + if (Platform.isAndroid) { + unawaited(_scheduleFGHomeWidgetSync()); + } unawaited(_scheduleFGSync('appStart in FG')); runApp( @@ -143,6 +107,17 @@ ThemeMode _themeMode(AdaptiveThemeMode? savedThemeMode) { return ThemeMode.system; } +Future _homeWidgetSync() async { + if (!Platform.isAndroid) return; + try { + if (await HomeWidgetService.instance.countHomeWidgets() != 0) { + await HomeWidgetService.instance.initHomeWidget(); + } + } catch (e, s) { + _logger.severe("Error in initSlideshowWidget", e, s); + } +} + Future _runBackgroundTask(String taskId, {String mode = 'normal'}) async { if (_isProcessRunning) { _logger.info("Background task triggered when process was already running"); @@ -176,8 +151,15 @@ Future _runInBackground(String taskId) async { _scheduleSuicide(kBGTaskTimeout, taskId); // To prevent OS from punishing us } await _init(true, via: 'runViaBackgroundTask'); - UpdateService.instance.showUpdateNotification().ignore(); - await _sync('bgSync'); + await Future.wait( + [ + _homeWidgetSync(), + () async { + UpdateService.instance.showUpdateNotification().ignore(); + await _sync('bgSync'); + }(), + ], + ); BackgroundFetch.finish(taskId); } @@ -228,6 +210,12 @@ Future _init(bool isBackground, {String via = ''}) async { LocalFileUpdateService.instance.init(preferences); SearchService.instance.init(); StorageBonusService.instance.init(preferences); + if (!isBackground && + Platform.isAndroid && + await HomeWidgetService.instance.countHomeWidgets() == 0) { + unawaited(HomeWidgetService.instance.initHomeWidget()); + } + if (Platform.isIOS) { // ignore: unawaited_futures PushService.instance.init().then((_) { @@ -292,6 +280,19 @@ Future _scheduleHeartBeat( }); } +Future _scheduleFGHomeWidgetSync() async { + Future.delayed(kFGHomeWidgetSyncFrequency, () async { + unawaited(_homeWidgetSyncPeriodic()); + }); +} + +Future _homeWidgetSyncPeriodic() async { + await _homeWidgetSync(); + Future.delayed(kFGHomeWidgetSyncFrequency, () async { + unawaited(_homeWidgetSyncPeriodic()); + }); +} + Future _scheduleFGSync(String caller) async { await _sync(caller); Future.delayed(kFGSyncFrequency, () async { diff --git a/mobile/lib/models/account/two_factor.dart b/mobile/lib/models/account/two_factor.dart new file mode 100644 index 000000000..6a18f4277 --- /dev/null +++ b/mobile/lib/models/account/two_factor.dart @@ -0,0 +1,13 @@ +enum TwoFactorType { totp, passkey } + +// ToString for TwoFactorType +String twoFactorTypeToString(TwoFactorType type) { + switch (type) { + case TwoFactorType.totp: + return "totp"; + case TwoFactorType.passkey: + return "passkey"; + default: + return type.name; + } +} diff --git a/mobile/lib/models/public_key.dart b/mobile/lib/models/public_key.dart deleted file mode 100644 index 0d14a4a55..000000000 --- a/mobile/lib/models/public_key.dart +++ /dev/null @@ -1,6 +0,0 @@ -class PublicKey { - final String email; - final String publicKey; - - PublicKey(this.email, this.publicKey); -} diff --git a/mobile/lib/models/search/search_types.dart b/mobile/lib/models/search/search_types.dart index 9f5ee9b1d..1ec197c7e 100644 --- a/mobile/lib/models/search/search_types.dart +++ b/mobile/lib/models/search/search_types.dart @@ -61,7 +61,7 @@ extension SectionTypeExtensions on SectionType { case SectionType.moment: return S.of(context).moments; case SectionType.location: - return S.of(context).location; + return S.of(context).locations; case SectionType.contacts: return S.of(context).contacts; case SectionType.album: @@ -69,7 +69,7 @@ extension SectionTypeExtensions on SectionType { case SectionType.fileTypesAndExtension: return S.of(context).fileTypes; case SectionType.fileCaption: - return S.of(context).photoDescriptions; + return S.of(context).descriptions; } } diff --git a/mobile/lib/services/home_widget_service.dart b/mobile/lib/services/home_widget_service.dart new file mode 100644 index 000000000..c8082b448 --- /dev/null +++ b/mobile/lib/services/home_widget_service.dart @@ -0,0 +1,180 @@ +import "dart:math"; + +import "package:figma_squircle/figma_squircle.dart"; +import "package:flutter/material.dart"; +import "package:flutter/scheduler.dart"; +import 'package:home_widget/home_widget.dart' as hw; +import "package:logging/logging.dart"; +import "package:photos/core/configuration.dart"; +import "package:photos/core/constants.dart"; +import "package:photos/db/files_db.dart"; +import "package:photos/models/file/file_type.dart"; +import "package:photos/services/favorites_service.dart"; +import "package:photos/utils/file_util.dart"; +import "package:photos/utils/preload_util.dart"; + +class HomeWidgetService { + final Logger _logger = Logger((HomeWidgetService).toString()); + + HomeWidgetService._privateConstructor(); + + static final HomeWidgetService instance = + HomeWidgetService._privateConstructor(); + + Future initHomeWidget() async { + final isLoggedIn = Configuration.instance.isLoggedIn(); + + if (!isLoggedIn) { + await clearHomeWidget(); + _logger.info("user not logged in"); + return; + } + + final collectionID = + await FavoritesService.instance.getFavoriteCollectionID(); + if (collectionID == null) { + await clearHomeWidget(); + _logger.info("Favorite collection not found"); + return; + } + + try { + await hw.HomeWidget.setAppGroupId(iOSGroupID); + final res = await FilesDB.instance.getFilesInCollection( + collectionID, + galleryLoadStartTime, + galleryLoadEndTime, + ); + + final previousGeneratedId = + await hw.HomeWidget.getWidgetData("home_widget_last_img"); + + if (res.files.length == 1 && + res.files[0].generatedID == previousGeneratedId) { + _logger + .info("Only one image found and it's the same as the previous one"); + return; + } + if (res.files.isEmpty) { + await clearHomeWidget(); + _logger.info("No images found"); + return; + } + final files = res.files.where( + (element) => + element.generatedID != previousGeneratedId && + element.fileType == FileType.image, + ); + + final randomNumber = Random().nextInt(files.length); + final randomFile = files.elementAt(randomNumber); + final fullImage = await getFileFromServer(randomFile); + if (fullImage == null) throw Exception("File not found"); + + final image = await decodeImageFromList(await fullImage.readAsBytes()); + final width = image.width.toDouble(); + final height = image.height.toDouble(); + final size = min(min(width, height), 1024.0); + final aspectRatio = width / height; + late final int cacheWidth; + late final int cacheHeight; + if (aspectRatio > 1) { + cacheWidth = 1024; + cacheHeight = (1024 / aspectRatio).round(); + } else if (aspectRatio < 1) { + cacheHeight = 1024; + cacheWidth = (1024 * aspectRatio).round(); + } else { + cacheWidth = 1024; + cacheHeight = 1024; + } + final Image img = Image.file( + fullImage, + fit: BoxFit.cover, + cacheWidth: cacheWidth, + cacheHeight: cacheHeight, + ); + + await PreloadImage.loadImage(img.image); + + final platformBrightness = + SchedulerBinding.instance.platformDispatcher.platformBrightness; + + final widget = ClipSmoothRect( + radius: SmoothBorderRadius(cornerRadius: 32, cornerSmoothing: 1), + child: Container( + width: size, + height: size, + decoration: BoxDecoration( + color: platformBrightness == Brightness.light + ? const Color.fromRGBO(251, 251, 251, 1) + : const Color.fromRGBO(27, 27, 27, 1), + image: DecorationImage(image: img.image, fit: BoxFit.cover), + ), + ), + ); + + await hw.HomeWidget.renderFlutterWidget( + widget, + logicalSize: Size(size, size), + key: "slideshow", + ); + + if (randomFile.generatedID != null) { + await hw.HomeWidget.saveWidgetData( + "home_widget_last_img", + randomFile.generatedID!, + ); + } + + await hw.HomeWidget.updateWidget( + name: 'SlideshowWidgetProvider', + androidName: 'SlideshowWidgetProvider', + qualifiedAndroidName: 'io.ente.photos.SlideshowWidgetProvider', + iOSName: 'SlideshowWidget', + ); + _logger.info( + ">>> OG size of SlideshowWidget image: ${width} x $height", + ); + _logger.info( + ">>> SlideshowWidget image rendered with size ${cacheWidth} x $cacheHeight", + ); + } catch (e) { + _logger.severe("Error rendering widget", e); + } + } + + Future countHomeWidgets() async { + return await hw.HomeWidget.getWidgetCount( + name: 'SlideshowWidgetProvider', + androidName: 'SlideshowWidgetProvider', + qualifiedAndroidName: 'io.ente.photos.SlideshowWidgetProvider', + iOSName: 'SlideshowWidget', + ) ?? + 0; + } + + Future clearHomeWidget() async { + final previousGeneratedId = + await hw.HomeWidget.getWidgetData("home_widget_last_img"); + if (previousGeneratedId == null) return; + + _logger.info("Clearing SlideshowWidget"); + await hw.HomeWidget.saveWidgetData( + "slideshow", + null, + ); + + await hw.HomeWidget.updateWidget( + name: 'SlideshowWidgetProvider', + androidName: 'SlideshowWidgetProvider', + qualifiedAndroidName: 'io.ente.photos.SlideshowWidgetProvider', + iOSName: 'SlideshowWidget', + ); + await hw.HomeWidget.saveWidgetData( + "home_widget_last_img", + null, + ); + _logger.info(">>> SlideshowWidget cleared"); + } +} diff --git a/mobile/lib/services/passkey_service.dart b/mobile/lib/services/passkey_service.dart index e704edccf..07a22f5a6 100644 --- a/mobile/lib/services/passkey_service.dart +++ b/mobile/lib/services/passkey_service.dart @@ -17,6 +17,28 @@ class PasskeyService { return response.data!["accountsToken"] as String; } + Future isPasskeyRecoveryEnabled() async { + final response = await _enteDio.get( + "/users/two-factor/recovery-status", + ); + return response.data!["isPasskeyRecoveryEnabled"] as bool; + } + + Future configurePasskeyRecovery( + String secret, + String userEncryptedSecret, + String userSecretNonce, + ) async { + await _enteDio.post( + "/users/two-factor/passkeys/configure-recovery", + data: { + "secret": secret, + "userSecretCipher": userEncryptedSecret, + "userSecretNonce": userSecretNonce, + }, + ); + } + Future openPasskeyPage(BuildContext context) async { try { final jwtToken = await getJwtToken(); diff --git a/mobile/lib/services/update_service.dart b/mobile/lib/services/update_service.dart index 10b066390..759adaf42 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 = 14; + static const currentChangeLogVersion = 15; LatestVersionInfo? _latestVersion; final _logger = Logger("UpdateService"); diff --git a/mobile/lib/services/user_service.dart b/mobile/lib/services/user_service.dart index f8612049a..44e098567 100644 --- a/mobile/lib/services/user_service.dart +++ b/mobile/lib/services/user_service.dart @@ -12,16 +12,15 @@ import 'package:photos/core/constants.dart'; import "package:photos/core/errors.dart"; import 'package:photos/core/event_bus.dart'; import 'package:photos/core/network/network.dart'; -import 'package:photos/db/public_keys_db.dart'; import "package:photos/events/account_configured_event.dart"; import 'package:photos/events/two_factor_status_change_event.dart'; import 'package:photos/events/user_details_changed_event.dart'; import "package:photos/generated/l10n.dart"; +import "package:photos/models/account/two_factor.dart"; import "package:photos/models/api/user/srp.dart"; import 'package:photos/models/delete_account.dart'; import 'package:photos/models/key_attributes.dart'; import 'package:photos/models/key_gen_result.dart'; -import 'package:photos/models/public_key.dart' as public_key; import 'package:photos/models/sessions.dart'; import 'package:photos/models/set_keys_request.dart'; import 'package:photos/models/set_recovery_key_request.dart'; @@ -160,12 +159,6 @@ class UserService { queryParameters: {"email": email}, ); final publicKey = response.data["publicKey"]; - await PublicKeysDB.instance.setKey( - public_key.PublicKey( - email, - publicKey, - ), - ); return publicKey; } on DioError catch (e) { if (e.response != null && e.response?.statusCode == 404) { @@ -815,7 +808,11 @@ class UserService { } } - Future recoverTwoFactor(BuildContext context, String sessionID) async { + Future recoverTwoFactor( + BuildContext context, + String sessionID, + TwoFactorType type, + ) async { final dialog = createProgressDialog(context, S.of(context).pleaseWait); await dialog.show(); try { @@ -823,6 +820,7 @@ class UserService { _config.getHttpEndpoint() + "/users/two-factor/recover", queryParameters: { "sessionID": sessionID, + "twoFactorType": twoFactorTypeToString(type), }, ); if (response.statusCode == 200) { @@ -831,6 +829,7 @@ class UserService { MaterialPageRoute( builder: (BuildContext context) { return TwoFactorRecoveryPage( + type, sessionID, response.data["encryptedSecret"], response.data["secretDecryptionNonce"], @@ -841,6 +840,7 @@ class UserService { ); } } on DioError catch (e) { + await dialog.hide(); _logger.severe(e); if (e.response != null && e.response!.statusCode == 404) { showToast(context, S.of(context).sessionExpired); @@ -862,6 +862,7 @@ class UserService { ); } } catch (e) { + await dialog.hide(); _logger.severe(e); // ignore: unawaited_futures showErrorDialog( @@ -876,6 +877,7 @@ class UserService { Future removeTwoFactor( BuildContext context, + TwoFactorType type, String sessionID, String recoveryKey, String encryptedSecret, @@ -915,6 +917,7 @@ class UserService { data: { "sessionID": sessionID, "secret": secret, + "twoFactorType": twoFactorTypeToString(type), }, ); if (response.statusCode == 200) { @@ -934,7 +937,8 @@ class UserService { ); } } on DioError catch (e) { - _logger.severe(e); + await dialog.hide(); + _logger.severe("error during recovery", e); if (e.response != null && e.response!.statusCode == 404) { showToast(context, "Session expired"); // ignore: unawaited_futures @@ -955,7 +959,9 @@ class UserService { ); } } catch (e) { - _logger.severe(e); + await dialog.hide(); + _logger.severe('unexpcted error during recovery', e); + // ignore: unawaited_futures showErrorDialog( context, diff --git a/mobile/lib/ui/account/passkey_page.dart b/mobile/lib/ui/account/passkey_page.dart index 25c2c7605..08b2472b3 100644 --- a/mobile/lib/ui/account/passkey_page.dart +++ b/mobile/lib/ui/account/passkey_page.dart @@ -3,9 +3,12 @@ import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:logging/logging.dart'; import 'package:photos/core/configuration.dart'; -import 'package:photos/ente_theme_data.dart'; import "package:photos/generated/l10n.dart"; +import "package:photos/models/account/two_factor.dart"; import 'package:photos/services/user_service.dart'; +import "package:photos/ui/components/buttons/button_widget.dart"; +import "package:photos/ui/components/models/button_type.dart"; +import "package:photos/utils/dialog_util.dart"; import 'package:uni_links/uni_links.dart'; import 'package:url_launcher/url_launcher_string.dart'; @@ -49,20 +52,27 @@ class _PasskeyPageState extends State { if (!context.mounted || Configuration.instance.hasConfiguredAccount() || link == null) { + _logger.warning( + 'ignored deeplink: contextMounted ${context.mounted} hasConfiguredAccount ${Configuration.instance.hasConfiguredAccount()}', + ); return; } - if (mounted && link.toLowerCase().startsWith("ente://passkey")) { - final uri = Uri.parse(link).queryParameters['response']; - - // response to json - final res = utf8.decode(base64.decode(uri!)); - final json = jsonDecode(res) as Map; - - try { + try { + if (mounted && link.toLowerCase().startsWith("ente://passkey")) { + final String? uri = Uri.parse(link).queryParameters['response']; + String base64String = uri!.toString(); + while (base64String.length % 4 != 0) { + base64String += '='; + } + final res = utf8.decode(base64.decode(base64String)); + final json = jsonDecode(res) as Map; await UserService.instance.onPassKeyVerified(context, json); - } catch (e) { - _logger.severe(e); + } else { + _logger.info('ignored deeplink: $link mounted $mounted'); } + } catch (e, s) { + _logger.severe('passKey: failed to handle deeplink', e, s); + showGenericErrorDialog(context: context, error: e).ignore(); } } @@ -91,27 +101,49 @@ class _PasskeyPageState extends State { Widget _getBody() { return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - S.of(context).waitingForBrowserRequest, - style: const TextStyle( - height: 1.4, - fontSize: 16, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 32), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + S.of(context).waitingForVerification, + style: const TextStyle( + height: 1.4, + fontSize: 16, + ), ), - ), - const SizedBox(height: 16), - Container( - width: double.infinity, - padding: const EdgeInsets.symmetric(horizontal: 32), - child: ElevatedButton( - style: Theme.of(context).colorScheme.optionalActionButtonStyle, - onPressed: launchPasskey, - child: Text(S.of(context).launchPasskeyUrlAgain), + const SizedBox(height: 16), + ButtonWidget( + buttonType: ButtonType.primary, + labelText: S.of(context).verifyPasskey, + onTap: () => launchPasskey(), ), - ), - ], + const Padding(padding: EdgeInsets.all(30)), + GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + UserService.instance.recoverTwoFactor( + context, + widget.sessionID, + TwoFactorType.passkey, + ); + }, + child: Container( + padding: const EdgeInsets.all(10), + child: Center( + child: Text( + S.of(context).recoverAccount, + style: const TextStyle( + decoration: TextDecoration.underline, + fontSize: 12, + ), + ), + ), + ), + ), + ], + ), ), ); } diff --git a/mobile/lib/ui/account/request_pwd_verification_page.dart b/mobile/lib/ui/account/request_pwd_verification_page.dart index 5b9109d83..67908f734 100644 --- a/mobile/lib/ui/account/request_pwd_verification_page.dart +++ b/mobile/lib/ui/account/request_pwd_verification_page.dart @@ -16,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() => diff --git a/mobile/lib/ui/account/two_factor_authentication_page.dart b/mobile/lib/ui/account/two_factor_authentication_page.dart index 2e88e7277..8dd71d044 100644 --- a/mobile/lib/ui/account/two_factor_authentication_page.dart +++ b/mobile/lib/ui/account/two_factor_authentication_page.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import "package:photos/generated/l10n.dart"; +import "package:photos/models/account/two_factor.dart"; import 'package:photos/services/user_service.dart'; import 'package:photos/ui/lifecycle_event_handler.dart'; import 'package:pinput/pin_put/pin_put.dart'; @@ -124,7 +125,11 @@ class _TwoFactorAuthenticationPageState GestureDetector( behavior: HitTestBehavior.opaque, onTap: () { - UserService.instance.recoverTwoFactor(context, widget.sessionID); + UserService.instance.recoverTwoFactor( + context, + widget.sessionID, + TwoFactorType.totp, + ); }, child: Container( padding: const EdgeInsets.all(10), diff --git a/mobile/lib/ui/account/two_factor_recovery_page.dart b/mobile/lib/ui/account/two_factor_recovery_page.dart index 7cab3d343..1b9a4fd62 100644 --- a/mobile/lib/ui/account/two_factor_recovery_page.dart +++ b/mobile/lib/ui/account/two_factor_recovery_page.dart @@ -2,6 +2,7 @@ import 'dart:ui'; import 'package:flutter/material.dart'; import "package:photos/generated/l10n.dart"; +import "package:photos/models/account/two_factor.dart"; import 'package:photos/services/user_service.dart'; import 'package:photos/utils/dialog_util.dart'; @@ -9,8 +10,10 @@ class TwoFactorRecoveryPage extends StatefulWidget { final String sessionID; final String encryptedSecret; final String secretDecryptionNonce; + final TwoFactorType type; const TwoFactorRecoveryPage( + this.type, this.sessionID, this.encryptedSecret, this.secretDecryptionNonce, { @@ -71,6 +74,7 @@ class _TwoFactorRecoveryPageState extends State { ? () async { await UserService.instance.removeTwoFactor( context, + widget.type, widget.sessionID, _recoveryKey.text, widget.encryptedSecret, diff --git a/mobile/lib/ui/notification/update/change_log_page.dart b/mobile/lib/ui/notification/update/change_log_page.dart index ba7f24f3e..6ea2510f2 100644 --- a/mobile/lib/ui/notification/update/change_log_page.dart +++ b/mobile/lib/ui/notification/update/change_log_page.dart @@ -1,4 +1,5 @@ import "dart:async"; +import "dart:io"; import 'package:flutter/material.dart'; import "package:photos/generated/l10n.dart"; @@ -84,14 +85,22 @@ class _ChangeLogPageState extends State { ButtonWidget( buttonType: ButtonType.trailingIconSecondary, buttonSize: ButtonSize.large, - labelText: S.of(context).joinDiscord, - icon: Icons.discord_outlined, + // labelText: S.of(context).joinDiscord, + labelText: "Why we open sourced", + // icon: Icons.discord_outlined, + icon: Icons.rocket_rounded, iconColor: enteColorScheme.primary500, onTap: () async { + // unawaited( + // launchUrlString( + // "https://discord.com/invite/z2YVKkycX3", + // mode: LaunchMode.externalApplication, + // ), + // ); unawaited( launchUrlString( - "https://discord.com/invite/z2YVKkycX3", - mode: LaunchMode.externalApplication, + "https://ente.io/blog/open-sourcing-our-server/", + mode: LaunchMode.inAppBrowserView, ), ); }, @@ -120,11 +129,27 @@ 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( - "Map View ✨", - 'You can now view the location where a photo was clicked.\n' - '\nOpen a photo and tap the Info button to view its place on the map!', + "Redesigned Discovery Tab", + 'We\'ve given it a fresh new look for improved design and better visual separation between each section.', + ), + 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", diff --git a/mobile/lib/ui/settings/app_update_dialog.dart b/mobile/lib/ui/settings/app_update_dialog.dart index 124b87927..8038b7fa5 100644 --- a/mobile/lib/ui/settings/app_update_dialog.dart +++ b/mobile/lib/ui/settings/app_update_dialog.dart @@ -1,8 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:logging/logging.dart'; -import 'package:photos/core/configuration.dart'; -import 'package:photos/core/network/network.dart'; -import 'package:photos/ente_theme_data.dart'; import "package:photos/generated/l10n.dart"; import 'package:photos/services/update_service.dart'; import 'package:photos/theme/ente_theme.dart'; @@ -66,16 +62,12 @@ class _AppUpdateDialogState extends State { const Padding(padding: EdgeInsets.all(8)), ButtonWidget( buttonType: ButtonType.primary, - labelText: S.of(context).update, + labelText: S.of(context).download, onTap: () async { - Navigator.pop(context); // ignore: unawaited_futures - showDialog( - context: context, - builder: (BuildContext context) { - return ApkDownloaderDialog(widget.latestVersionInfo); - }, - barrierDismissible: false, + launchUrlString( + widget.latestVersionInfo!.url, + mode: LaunchMode.externalApplication, ); }, ), @@ -87,22 +79,6 @@ class _AppUpdateDialogState extends State { Navigator.of(context).pop(); }, ), - const Padding(padding: EdgeInsets.all(8)), - Center( - child: InkWell( - child: Text( - S.of(context).installManually, - style: Theme.of(context) - .textTheme - .bodySmall! - .copyWith(decoration: TextDecoration.underline), - ), - onTap: () => launchUrlString( - widget.latestVersionInfo!.url, - mode: LaunchMode.externalApplication, - ), - ), - ), ], ); final shouldForceUpdate = @@ -135,114 +111,3 @@ class _AppUpdateDialogState extends State { ); } } - -class ApkDownloaderDialog extends StatefulWidget { - final LatestVersionInfo? versionInfo; - - const ApkDownloaderDialog(this.versionInfo, {Key? key}) : super(key: key); - - @override - State createState() => _ApkDownloaderDialogState(); -} - -class _ApkDownloaderDialogState extends State { - String? _saveUrl; - double? _downloadProgress; - - @override - void initState() { - super.initState(); - _saveUrl = Configuration.instance.getTempDirectory() + - "ente-" + - widget.versionInfo!.name + - ".apk"; - _downloadApk(); - } - - @override - Widget build(BuildContext context) { - return WillPopScope( - onWillPop: () async => false, - child: AlertDialog( - title: Text( - S.of(context).downloading, - style: const TextStyle( - fontSize: 16, - ), - textAlign: TextAlign.center, - ), - content: LinearProgressIndicator( - value: _downloadProgress, - valueColor: AlwaysStoppedAnimation( - Theme.of(context).colorScheme.greenAlternative, - ), - ), - ), - ); - } - - Future _downloadApk() async { - try { - await NetworkClient.instance.getDio().download( - widget.versionInfo!.url, - _saveUrl, - onReceiveProgress: (count, _) { - setState(() { - _downloadProgress = count / widget.versionInfo!.size; - }); - }, - ); - Navigator.of(context, rootNavigator: true).pop('dialog'); - // OpenFile.open(_saveUrl); - } catch (e) { - Logger("ApkDownloader").severe(e); - final AlertDialog alert = AlertDialog( - title: Text(S.of(context).sorry), - content: Text(S.of(context).theDownloadCouldNotBeCompleted), - actions: [ - TextButton( - child: Text( - S.of(context).ignoreUpdate, - style: const TextStyle( - color: Colors.white, - ), - ), - onPressed: () { - Navigator.of(context, rootNavigator: true).pop('dialog'); - Navigator.of(context, rootNavigator: true).pop('dialog'); - }, - ), - TextButton( - child: Text( - S.of(context).retry, - style: TextStyle( - color: Theme.of(context).colorScheme.greenAlternative, - ), - ), - onPressed: () { - Navigator.of(context, rootNavigator: true).pop('dialog'); - Navigator.of(context, rootNavigator: true).pop('dialog'); - showDialog( - context: context, - builder: (BuildContext context) { - return ApkDownloaderDialog(widget.versionInfo); - }, - barrierDismissible: false, - ); - }, - ), - ], - ); - - // ignore: unawaited_futures - showDialog( - context: context, - builder: (BuildContext context) { - return alert; - }, - barrierColor: Colors.black87, - ); - return; - } - } -} diff --git a/mobile/lib/ui/settings/security_section_widget.dart b/mobile/lib/ui/settings/security_section_widget.dart index 3d10bf6c4..dce7e97ec 100644 --- a/mobile/lib/ui/settings/security_section_widget.dart +++ b/mobile/lib/ui/settings/security_section_widget.dart @@ -2,6 +2,7 @@ import 'dart:async'; import "dart:typed_data"; import 'package:flutter/material.dart'; +import "package:logging/logging.dart"; import 'package:photos/core/configuration.dart'; import 'package:photos/core/event_bus.dart'; import 'package:photos/ente_theme_data.dart'; @@ -22,6 +23,7 @@ import 'package:photos/ui/components/menu_item_widget/menu_item_widget.dart'; import 'package:photos/ui/components/toggle_switch_widget.dart'; import 'package:photos/ui/settings/common_settings.dart'; import "package:photos/utils/crypto_util.dart"; +import "package:photos/utils/dialog_util.dart"; import "package:photos/utils/navigation_util.dart"; import "package:photos/utils/toast_util.dart"; @@ -37,7 +39,7 @@ class _SecuritySectionWidgetState extends State { late StreamSubscription _twoFactorStatusChangeEvent; - + final Logger _logger = Logger('SecuritySectionWidget'); @override void initState() { super.initState(); @@ -110,7 +112,7 @@ class _SecuritySectionWidgetState extends State { pressedColor: getEnteColorScheme(context).fillFaint, trailingIcon: Icons.chevron_right_outlined, trailingIconIsMuted: true, - onTap: () => PasskeyService.instance.openPasskeyPage(context), + onTap: () async => await onPasskeyClick(context), ), sectionOptionSpacing, MenuItemWidget( @@ -232,6 +234,32 @@ class _SecuritySectionWidgetState extends State { ); } + Future onPasskeyClick(BuildContext buildContext) async { + try { + final isPassKeyResetEnabled = + await PasskeyService.instance.isPasskeyRecoveryEnabled(); + if (!isPassKeyResetEnabled) { + final Uint8List recoveryKey = + await UserService.instance.getOrCreateRecoveryKey(context); + final resetKey = CryptoUtil.generateKey(); + final resetKeyBase64 = CryptoUtil.bin2base64(resetKey); + final encryptionResult = CryptoUtil.encryptSync( + resetKey, + recoveryKey, + ); + await PasskeyService.instance.configurePasskeyRecovery( + resetKeyBase64, + CryptoUtil.bin2base64(encryptionResult.encryptedData!), + CryptoUtil.bin2base64(encryptionResult.nonce!), + ); + } + PasskeyService.instance.openPasskeyPage(buildContext).ignore(); + } catch (e, s) { + _logger.severe("failed to open passkey page", e, s); + await showGenericErrorDialog(context: context, error: e); + } + } + Future updateEmailMFA(bool isEnabled) async { try { final UserDetails details = diff --git a/mobile/lib/ui/tools/debug/log_file_viewer.dart b/mobile/lib/ui/tools/debug/log_file_viewer.dart index 9e3e00789..e41561c0e 100644 --- a/mobile/lib/ui/tools/debug/log_file_viewer.dart +++ b/mobile/lib/ui/tools/debug/log_file_viewer.dart @@ -43,6 +43,9 @@ class _LogFileViewerState extends State { return Container( padding: const EdgeInsets.only(left: 12, top: 8, right: 12), child: Scrollbar( + interactive: true, + thickness: 4, + radius: const Radius.circular(2), child: SingleChildScrollView( child: Text( _logs!, diff --git a/mobile/lib/ui/viewer/file/zoomable_image.dart b/mobile/lib/ui/viewer/file/zoomable_image.dart index 3329e6955..933ff748a 100644 --- a/mobile/lib/ui/viewer/file/zoomable_image.dart +++ b/mobile/lib/ui/viewer/file/zoomable_image.dart @@ -41,8 +41,7 @@ class ZoomableImage extends StatefulWidget { State createState() => _ZoomableImageState(); } -class _ZoomableImageState extends State - with SingleTickerProviderStateMixin { +class _ZoomableImageState extends State { late Logger _logger; late EnteFile _photo; ImageProvider? _imageProvider; @@ -54,6 +53,7 @@ class _ZoomableImageState extends State ValueChanged? _scaleStateChangedCallback; bool _isZooming = false; PhotoViewController _photoViewController = PhotoViewController(); + final _scaleStateController = PhotoViewScaleStateController(); @override void initState() { @@ -74,6 +74,7 @@ class _ZoomableImageState extends State @override void dispose() { _photoViewController.dispose(); + _scaleStateController.dispose(); super.dispose(); } @@ -90,8 +91,10 @@ class _ZoomableImageState extends State content = PhotoViewGestureDetectorScope( axis: Axis.vertical, child: PhotoView( + key: ValueKey(_loadedFinalImage), imageProvider: _imageProvider, controller: _photoViewController, + scaleStateController: _scaleStateController, scaleStateChangedCallback: _scaleStateChangedCallback, minScale: widget.shouldCover ? PhotoViewComputedScale.covered @@ -101,6 +104,40 @@ class _ZoomableImageState extends State tag: widget.tagPrefix! + _photo.tag, ), backgroundDecoration: widget.backgroundDecoration as BoxDecoration?, + loadingBuilder: (context, event) { + // This is to make sure the hero anitmation animates and fits in the + //dimensions of the image on screen. + final screenDimensions = MediaQuery.sizeOf(context); + late final double screenRelativeImageWidth; + late final double screenRelativeImageHeight; + final screenWidth = screenDimensions.width; + final screenHeight = screenDimensions.height; + + final aspectRatioOfScreen = screenWidth / screenHeight; + final aspectRatioOfImage = _photo.width / _photo.height; + + if (aspectRatioOfImage > aspectRatioOfScreen) { + screenRelativeImageWidth = screenWidth; + screenRelativeImageHeight = screenWidth / aspectRatioOfImage; + } else if (aspectRatioOfImage < aspectRatioOfScreen) { + screenRelativeImageHeight = screenHeight; + screenRelativeImageWidth = screenHeight * aspectRatioOfImage; + } else { + screenRelativeImageWidth = screenWidth; + screenRelativeImageHeight = screenHeight; + } + + return Center( + child: SizedBox( + width: screenRelativeImageWidth, + height: screenRelativeImageHeight, + child: Hero( + tag: widget.tagPrefix! + _photo.tag, + child: const EnteLoadingWidget(), + ), + ), + ); + }, ), ); } else { @@ -272,15 +309,13 @@ class _ZoomableImageState extends State final scale = _photoViewController.scale! / (finalImageInfo.image.width / prevImageInfo.image.width); final currentPosition = _photoViewController.value.position; - final positionScaleFactor = 1 / scale; - final newPosition = currentPosition.scale( - positionScaleFactor, - positionScaleFactor, - ); _photoViewController = PhotoViewController( - initialPosition: newPosition, + initialPosition: currentPosition, initialScale: scale, ); + // Fix for auto-zooming when final image is loaded after double tapping + //twice. + _scaleStateController.scaleState = PhotoViewScaleState.zoomedIn; } final bool canUpdateMetadata = _photo.canEditMetaInfo; // forcefully get finalImageInfo is dimensions are not available in metadata diff --git a/mobile/lib/ui/viewer/gallery/component/gallery_file_widget.dart b/mobile/lib/ui/viewer/gallery/component/gallery_file_widget.dart index 142f4427c..5f1ce7cbf 100644 --- a/mobile/lib/ui/viewer/gallery/component/gallery_file_widget.dart +++ b/mobile/lib/ui/viewer/gallery/component/gallery_file_widget.dart @@ -68,11 +68,20 @@ class GalleryFileWidget extends StatelessWidget { : _onLongPressNoSelectionLimit(context, file); }, child: Stack( + clipBehavior: Clip.none, children: [ ClipRRect( borderRadius: BorderRadius.circular(1), child: Hero( tag: heroTag, + flightShuttleBuilder: ( + flightContext, + animation, + flightDirection, + fromHeroContext, + toHeroContext, + ) => + thumbnailWidget, transitionOnUserGestures: true, child: isFileSelected ? ColorFiltered( diff --git a/mobile/lib/ui/viewer/gallery/gallery_app_bar_widget.dart b/mobile/lib/ui/viewer/gallery/gallery_app_bar_widget.dart index 9a72e4ca9..539cbc182 100644 --- a/mobile/lib/ui/viewer/gallery/gallery_app_bar_widget.dart +++ b/mobile/lib/ui/viewer/gallery/gallery_app_bar_widget.dart @@ -631,10 +631,7 @@ class _GalleryAppBarWidgetState extends State { } else if (value == AlbumPopupAction.map) { await showOnMap(); } else if (value == AlbumPopupAction.cleanUncategorized) { - await collectionActions.removeFromUncatIfPresentInOtherAlbum( - widget.collection!, - context, - ); + await onCleanUncategorizedClick(context); } else { showToast(context, S.of(context).somethingWentWrong); } @@ -646,6 +643,24 @@ class _GalleryAppBarWidgetState extends State { return actions; } + Future onCleanUncategorizedClick(BuildContext buildContext) async { + final actionResult = await showChoiceActionSheet( + context, + isCritical: true, + title: S.of(context).cleanUncategorized, + firstButtonLabel: S.of(context).confirm, + body: S.of(context).cleanUncategorizedDescription, + ); + if (actionResult?.action != null && mounted) { + if (actionResult!.action == ButtonAction.first) { + await collectionActions.removeFromUncatIfPresentInOtherAlbum( + widget.collection!, + buildContext, + ); + } + } + } + Future setCoverPhoto(BuildContext context) async { final int? coverPhotoID = await showPickCoverPhotoSheet( context, diff --git a/mobile/lib/ui/viewer/gallery/uncategorized_page.dart b/mobile/lib/ui/viewer/gallery/uncategorized_page.dart index c38a804a7..265a614a8 100644 --- a/mobile/lib/ui/viewer/gallery/uncategorized_page.dart +++ b/mobile/lib/ui/viewer/gallery/uncategorized_page.dart @@ -68,6 +68,7 @@ class UnCategorizedPage extends StatelessWidget { ], tagPrefix: tagPrefix, selectedFiles: _selectedFiles, + sortAsyncFn: () => collection.pubMagicMetadata.asc ?? false, initialFiles: null, albumName: S.of(context).uncategorized, ); diff --git a/mobile/lib/ui/viewer/search/search_widget.dart b/mobile/lib/ui/viewer/search/search_widget.dart index 2d9132875..c624a78b3 100644 --- a/mobile/lib/ui/viewer/search/search_widget.dart +++ b/mobile/lib/ui/viewer/search/search_widget.dart @@ -139,12 +139,9 @@ class SearchWidgetState extends State { autocorrect: false, // Above parameters are to disable auto-suggestion decoration: InputDecoration( - // hintText: S.of(context).searchHintText, + //TODO: Extract string hintText: "Search", filled: true, - contentPadding: const EdgeInsets.symmetric( - vertical: 10, - ), border: const UnderlineInputBorder( borderSide: BorderSide.none, ), diff --git a/mobile/lib/utils/home_widget_util.dart b/mobile/lib/utils/home_widget_util.dart deleted file mode 100644 index 62da4b7ea..000000000 --- a/mobile/lib/utils/home_widget_util.dart +++ /dev/null @@ -1,136 +0,0 @@ -import "dart:math"; - -import "package:flutter/material.dart"; -import 'package:home_widget/home_widget.dart' as hw; -import "package:logging/logging.dart"; -import "package:photos/core/configuration.dart"; -import "package:photos/core/constants.dart"; -import "package:photos/db/files_db.dart"; -import "package:photos/models/file/file_type.dart"; -import "package:photos/services/favorites_service.dart"; -import "package:photos/utils/file_util.dart"; -import "package:photos/utils/preload_util.dart"; - -Future countHomeWidgets() async { - return await hw.HomeWidget.getWidgetCount( - name: 'SlideshowWidgetProvider', - androidName: 'SlideshowWidgetProvider', - qualifiedAndroidName: 'io.ente.photos.SlideshowWidgetProvider', - iOSName: 'SlideshowWidget', - ) ?? - 0; -} - -Future initHomeWidget() async { - final Logger logger = Logger("initHomeWidget"); - final user = Configuration.instance.getUserID(); - - if (user == null) { - await clearHomeWidget(); - throw Exception("User not found"); - } - - final collectionID = - await FavoritesService.instance.getFavoriteCollectionID(); - if (collectionID == null) { - await clearHomeWidget(); - throw Exception("Collection not found"); - } - - try { - await hw.HomeWidget.setAppGroupId(iOSGroupID); - final res = await FilesDB.instance.getFilesInCollection( - collectionID, - galleryLoadStartTime, - galleryLoadEndTime, - ); - - final previousGeneratedId = - await hw.HomeWidget.getWidgetData("home_widget_last_img"); - final files = res.files.where( - (element) => - element.generatedID != previousGeneratedId && - element.fileType == FileType.image, - ); - final randomNumber = Random().nextInt(files.length); - final randomFile = files.elementAt(randomNumber); - final fullImage = await getFileFromServer(randomFile); - if (fullImage == null) throw Exception("File not found"); - - Image img = Image.file(fullImage); - var imgProvider = img.image; - await PreloadImage.loadImage(imgProvider); - - img = Image.file(fullImage); - imgProvider = img.image; - - final image = await decodeImageFromList(await fullImage.readAsBytes()); - final width = image.width.toDouble(); - final height = image.height.toDouble(); - final size = min(min(width, height), 1024.0); - - final widget = ClipRRect( - borderRadius: BorderRadius.circular(32), - child: Container( - width: size, - height: size, - decoration: BoxDecoration( - color: Colors.black, - image: DecorationImage(image: imgProvider, fit: BoxFit.cover), - ), - ), - ); - - await hw.HomeWidget.renderFlutterWidget( - widget, - logicalSize: Size(size, size), - key: "slideshow", - ); - - await hw.HomeWidget.updateWidget( - name: 'SlideshowWidgetProvider', - androidName: 'SlideshowWidgetProvider', - qualifiedAndroidName: 'io.ente.photos.SlideshowWidgetProvider', - iOSName: 'SlideshowWidget', - ); - - if (randomFile.generatedID != null) { - await hw.HomeWidget.saveWidgetData( - "home_widget_last_img", - randomFile.generatedID!, - ); - } - - logger.info( - ">>> SlideshowWidget rendered with size ${width}x$height", - ); - } catch (_) { - throw Exception("Error rendering widget"); - } -} - -Future clearHomeWidget() async { - final previousGeneratedId = - await hw.HomeWidget.getWidgetData("home_widget_last_img"); - if (previousGeneratedId == null) return; - - final Logger logger = Logger("clearHomeWidget"); - - logger.info("Clearing SlideshowWidget"); - await hw.HomeWidget.saveWidgetData( - "slideshow", - null, - ); - - await hw.HomeWidget.updateWidget( - name: 'SlideshowWidgetProvider', - androidName: 'SlideshowWidgetProvider', - qualifiedAndroidName: 'io.ente.photos.SlideshowWidgetProvider', - iOSName: 'SlideshowWidget', - ); - await hw.HomeWidget.saveWidgetData( - "home_widget_last_img", - null, - ); - logger.info(">>> SlideshowWidget cleared"); -} diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index 7298e7134..5c3f8e2f2 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -2449,14 +2449,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" - workmanager: - dependency: "direct main" - description: - name: workmanager - sha256: ed13530cccd28c5c9959ad42d657cd0666274ca74c56dea0ca183ddd527d3a00 - url: "https://pub.dev" - source: hosted - version: "0.5.2" xdg_directories: dependency: transitive description: @@ -2498,5 +2490,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.1.2 <4.0.0" + dart: ">=3.1.0 <4.0.0" flutter: ">=3.13.0" diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 578331c8d..3adcf3ca3 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.65+585 +version: 0.8.71+591 publish_to: none environment: @@ -122,8 +122,6 @@ dependencies: motionphoto: git: "https://github.com/ente-io/motionphoto.git" move_to_background: ^1.0.2 - - # open_file: ^3.2.1 onnxruntime: git: url: https://github.com/ente-io/onnxruntime.git @@ -175,7 +173,6 @@ dependencies: wallpaper_manager_flutter: ^0.0.2 wechat_assets_picker: ^8.6.3 widgets_to_image: ^0.0.2 - workmanager: ^0.5.2 dependency_overrides: # current fork of tfite_flutter_helper depends on ffi: ^1.x.x diff --git a/mobile/thirdparty/flutter b/mobile/thirdparty/flutter deleted file mode 160000 index 367f9ea16..000000000 --- a/mobile/thirdparty/flutter +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 367f9ea16bfae1ca451b9cc27c1366870b187ae2 diff --git a/mobile/thirdparty/isar b/mobile/thirdparty/isar deleted file mode 160000 index 6643d064a..000000000 --- a/mobile/thirdparty/isar +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 6643d064abf22606b6c6a741ea873e4781115ef4 diff --git a/mobile/thirdparty/transistor-background-fetch/.gitignore b/mobile/thirdparty/transistor-background-fetch/.gitignore deleted file mode 100644 index 296d68a8f..000000000 --- a/mobile/thirdparty/transistor-background-fetch/.gitignore +++ /dev/null @@ -1,63 +0,0 @@ -# Eclipse -.metadata - -# Xcode -# -.DS_Store -build/ -*.pbxuser -!default.pbxuser -*.mode1v3 -!default.mode1v3 -*.mode2v3 -!default.mode2v3 -*.perspectivev3 -!default.perspectivev3 -xcuserdata -*.xccheckout -*.moved-aside -DerivedData -*.hmap -*.ipa -*.xcuserstate - -# CocoaPods -# -# We recommend against adding the Pods directory to your .gitignore. However -# you should judge for yourself, the pros and cons are mentioned at: -# http://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control -# -#Pods/ - -# Eclipse - -# built application files -*.apk -*.ap_ - -# files for the dex VM -*.dex - -# Java class files -*.class - -# generated files -bin/ -gen/ - -# Local configuration file (sdk path, etc) -local.properties - -# Eclipse project files -.classpath -.project - -# Proguard folder generated by Eclipse -proguard/ - -# Intellij project files -*.iml -*.ipr -*.iws -.idea/ - diff --git a/mobile/thirdparty/transistor-background-fetch/LICENSE b/mobile/thirdparty/transistor-background-fetch/LICENSE deleted file mode 100644 index 526bbac98..000000000 --- a/mobile/thirdparty/transistor-background-fetch/LICENSE +++ /dev/null @@ -1,19 +0,0 @@ -Copyright (c) 2017 Transistor Software - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. diff --git a/mobile/thirdparty/transistor-background-fetch/README.md b/mobile/thirdparty/transistor-background-fetch/README.md deleted file mode 100644 index 418370978..000000000 --- a/mobile/thirdparty/transistor-background-fetch/README.md +++ /dev/null @@ -1,22 +0,0 @@ -Transistor Background Fetch -=========================================================================== - -Copyright (c) 2017 Transistor Software - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. diff --git a/mobile/thirdparty/transistor-background-fetch/TSBackgroundFetch.podspec b/mobile/thirdparty/transistor-background-fetch/TSBackgroundFetch.podspec deleted file mode 100644 index 1967bd2e9..000000000 --- a/mobile/thirdparty/transistor-background-fetch/TSBackgroundFetch.podspec +++ /dev/null @@ -1,28 +0,0 @@ -# -# Be sure to run `pod lib lint TSBackgroundFetch.podspec' to ensure this is a -# valid spec before submitting. -# -# Any lines starting with a # are optional, but their use is encouraged -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html -# - -Pod::Spec.new do |s| - s.name = 'TSBackgroundFetch' - s.version = '0.0.1' - s.summary = 'iOS Background Fetch API Manager' - - s.description = <<-DESC -iOS Background Fetch API Manager with ability to handle multiple listeners. - DESC - - s.homepage = 'http://www.transistorsoft.com' - s.license = { :type => 'MIT', :file => 'LICENSE' } - s.author = { 'christocracy' => 'christocracy@gmail.com' } - s.source = { :git => 'https://github.com/transistorsoft/transistor-background-fetch.git', :tag => s.version.to_s } - s.social_media_url = 'https://twitter.com/christocracy' - - s.ios.deployment_target = '8.0' - - s.source_files = 'ios/TSBackgroundFetch/TSBackgroundFetch/*.{h,m}' - s.vendored_frameworks = 'ios/TSBackgroundFetch/TSBackgroundFetch.framework' -end diff --git a/mobile/thirdparty/transistor-background-fetch/android/.gitignore b/mobile/thirdparty/transistor-background-fetch/android/.gitignore deleted file mode 100644 index 39fb081a4..000000000 --- a/mobile/thirdparty/transistor-background-fetch/android/.gitignore +++ /dev/null @@ -1,9 +0,0 @@ -*.iml -.gradle -/local.properties -/.idea/workspace.xml -/.idea/libraries -.DS_Store -/build -/captures -.externalNativeBuild diff --git a/mobile/thirdparty/transistor-background-fetch/android/app/.gitignore b/mobile/thirdparty/transistor-background-fetch/android/app/.gitignore deleted file mode 100644 index 796b96d1c..000000000 --- a/mobile/thirdparty/transistor-background-fetch/android/app/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build diff --git a/mobile/thirdparty/transistor-background-fetch/android/app/build.gradle b/mobile/thirdparty/transistor-background-fetch/android/app/build.gradle deleted file mode 100644 index 93a4040d2..000000000 --- a/mobile/thirdparty/transistor-background-fetch/android/app/build.gradle +++ /dev/null @@ -1,29 +0,0 @@ -apply plugin: 'com.android.application' - -android { - compileSdkVersion 26 - defaultConfig { - applicationId "com.transistorsoft.backgroundfetch" - minSdkVersion 16 - targetSdkVersion 26 - versionCode 1 - versionName "1.0" - testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" - } - buildTypes { - release { - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' - } - } -} - -dependencies { - testImplementation 'junit:junit:4.12' - androidTestImplementation 'androidx.test:runner:1.2.0' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' - - implementation fileTree(dir: 'libs', include: ['*.jar']) - implementation 'com.android.support:appcompat-v7:26.1.0' - -} diff --git a/mobile/thirdparty/transistor-background-fetch/android/app/proguard-rules.pro b/mobile/thirdparty/transistor-background-fetch/android/app/proguard-rules.pro deleted file mode 100644 index f1b424510..000000000 --- a/mobile/thirdparty/transistor-background-fetch/android/app/proguard-rules.pro +++ /dev/null @@ -1,21 +0,0 @@ -# Add project specific ProGuard rules here. -# You can control the set of applied configuration files using the -# proguardFiles setting in build.gradle. -# -# For more details, see -# http://developer.android.com/guide/developing/tools/proguard.html - -# If your project uses WebView with JS, uncomment the following -# and specify the fully qualified class name to the JavaScript interface -# class: -#-keepclassmembers class fqcn.of.javascript.interface.for.webview { -# public *; -#} - -# Uncomment this to preserve the line number information for -# debugging stack traces. -#-keepattributes SourceFile,LineNumberTable - -# If you keep the line number information, uncomment this to -# hide the original source file name. -#-renamesourcefileattribute SourceFile diff --git a/mobile/thirdparty/transistor-background-fetch/android/app/src/androidTest/java/com/transistorsoft/backgroundfetch/ExampleInstrumentedTest.java b/mobile/thirdparty/transistor-background-fetch/android/app/src/androidTest/java/com/transistorsoft/backgroundfetch/ExampleInstrumentedTest.java deleted file mode 100644 index 983348763..000000000 --- a/mobile/thirdparty/transistor-background-fetch/android/app/src/androidTest/java/com/transistorsoft/backgroundfetch/ExampleInstrumentedTest.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.transistorsoft.backgroundfetch; - -import android.content.Context; - -import androidx.test.InstrumentationRegistry; -import androidx.test.runner.AndroidJUnit4; - -import org.junit.Test; -import org.junit.runner.RunWith; - -import static org.junit.Assert.*; - -/** - * Instrumented test, which will execute on an Android device. - * - * @see Testing documentation - */ -@RunWith(AndroidJUnit4.class) -public class ExampleInstrumentedTest { - @Test - public void useAppContext() throws Exception { - // Context of the app under test. - Context appContext = InstrumentationRegistry.getTargetContext(); - - assertEquals("com.transistorsoft.backgroundfetch", appContext.getPackageName()); - } -} diff --git a/mobile/thirdparty/transistor-background-fetch/android/app/src/main/AndroidManifest.xml b/mobile/thirdparty/transistor-background-fetch/android/app/src/main/AndroidManifest.xml deleted file mode 100644 index b22ee9b63..000000000 --- a/mobile/thirdparty/transistor-background-fetch/android/app/src/main/AndroidManifest.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - diff --git a/mobile/thirdparty/transistor-background-fetch/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/mobile/thirdparty/transistor-background-fetch/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml deleted file mode 100644 index c7bd21dbd..000000000 --- a/mobile/thirdparty/transistor-background-fetch/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - - - - diff --git a/mobile/thirdparty/transistor-background-fetch/android/app/src/main/res/drawable/ic_launcher_background.xml b/mobile/thirdparty/transistor-background-fetch/android/app/src/main/res/drawable/ic_launcher_background.xml deleted file mode 100644 index d5fccc538..000000000 --- a/mobile/thirdparty/transistor-background-fetch/android/app/src/main/res/drawable/ic_launcher_background.xml +++ /dev/null @@ -1,170 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/mobile/thirdparty/transistor-background-fetch/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/mobile/thirdparty/transistor-background-fetch/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml deleted file mode 100644 index eca70cfe5..000000000 --- a/mobile/thirdparty/transistor-background-fetch/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/mobile/thirdparty/transistor-background-fetch/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/mobile/thirdparty/transistor-background-fetch/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml deleted file mode 100644 index eca70cfe5..000000000 --- a/mobile/thirdparty/transistor-background-fetch/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/mobile/thirdparty/transistor-background-fetch/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/mobile/thirdparty/transistor-background-fetch/android/app/src/main/res/mipmap-hdpi/ic_launcher.png deleted file mode 100644 index a2f590828..000000000 Binary files a/mobile/thirdparty/transistor-background-fetch/android/app/src/main/res/mipmap-hdpi/ic_launcher.png and /dev/null differ diff --git a/mobile/thirdparty/transistor-background-fetch/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/mobile/thirdparty/transistor-background-fetch/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png deleted file mode 100644 index 1b5239980..000000000 Binary files a/mobile/thirdparty/transistor-background-fetch/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png and /dev/null differ diff --git a/mobile/thirdparty/transistor-background-fetch/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/mobile/thirdparty/transistor-background-fetch/android/app/src/main/res/mipmap-mdpi/ic_launcher.png deleted file mode 100644 index ff10afd6e..000000000 Binary files a/mobile/thirdparty/transistor-background-fetch/android/app/src/main/res/mipmap-mdpi/ic_launcher.png and /dev/null differ diff --git a/mobile/thirdparty/transistor-background-fetch/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/mobile/thirdparty/transistor-background-fetch/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png deleted file mode 100644 index 115a4c768..000000000 Binary files a/mobile/thirdparty/transistor-background-fetch/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png and /dev/null differ diff --git a/mobile/thirdparty/transistor-background-fetch/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/mobile/thirdparty/transistor-background-fetch/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png deleted file mode 100644 index dcd3cd808..000000000 Binary files a/mobile/thirdparty/transistor-background-fetch/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png and /dev/null differ diff --git a/mobile/thirdparty/transistor-background-fetch/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/mobile/thirdparty/transistor-background-fetch/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png deleted file mode 100644 index 459ca609d..000000000 Binary files a/mobile/thirdparty/transistor-background-fetch/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png and /dev/null differ diff --git a/mobile/thirdparty/transistor-background-fetch/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/mobile/thirdparty/transistor-background-fetch/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png deleted file mode 100644 index 8ca12fe02..000000000 Binary files a/mobile/thirdparty/transistor-background-fetch/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and /dev/null differ diff --git a/mobile/thirdparty/transistor-background-fetch/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/mobile/thirdparty/transistor-background-fetch/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png deleted file mode 100644 index 8e19b410a..000000000 Binary files a/mobile/thirdparty/transistor-background-fetch/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png and /dev/null differ diff --git a/mobile/thirdparty/transistor-background-fetch/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/mobile/thirdparty/transistor-background-fetch/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png deleted file mode 100644 index b824ebdd4..000000000 Binary files a/mobile/thirdparty/transistor-background-fetch/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and /dev/null differ diff --git a/mobile/thirdparty/transistor-background-fetch/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/mobile/thirdparty/transistor-background-fetch/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png deleted file mode 100644 index 4c19a13c2..000000000 Binary files a/mobile/thirdparty/transistor-background-fetch/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png and /dev/null differ diff --git a/mobile/thirdparty/transistor-background-fetch/android/app/src/main/res/values/colors.xml b/mobile/thirdparty/transistor-background-fetch/android/app/src/main/res/values/colors.xml deleted file mode 100644 index 3ab3e9cbc..000000000 --- a/mobile/thirdparty/transistor-background-fetch/android/app/src/main/res/values/colors.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - #3F51B5 - #303F9F - #FF4081 - diff --git a/mobile/thirdparty/transistor-background-fetch/android/app/src/main/res/values/strings.xml b/mobile/thirdparty/transistor-background-fetch/android/app/src/main/res/values/strings.xml deleted file mode 100644 index 89ece41f6..000000000 --- a/mobile/thirdparty/transistor-background-fetch/android/app/src/main/res/values/strings.xml +++ /dev/null @@ -1,3 +0,0 @@ - - BackgroundFetch - diff --git a/mobile/thirdparty/transistor-background-fetch/android/app/src/main/res/values/styles.xml b/mobile/thirdparty/transistor-background-fetch/android/app/src/main/res/values/styles.xml deleted file mode 100644 index 5885930df..000000000 --- a/mobile/thirdparty/transistor-background-fetch/android/app/src/main/res/values/styles.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - diff --git a/mobile/thirdparty/transistor-background-fetch/android/app/src/test/java/com/transistorsoft/backgroundfetch/ExampleUnitTest.java b/mobile/thirdparty/transistor-background-fetch/android/app/src/test/java/com/transistorsoft/backgroundfetch/ExampleUnitTest.java deleted file mode 100644 index 6536d1cd3..000000000 --- a/mobile/thirdparty/transistor-background-fetch/android/app/src/test/java/com/transistorsoft/backgroundfetch/ExampleUnitTest.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.transistorsoft.backgroundfetch; - -import org.junit.Test; - -import static org.junit.Assert.*; - -/** - * Example local unit test, which will execute on the development machine (host). - * - * @see Testing documentation - */ -public class ExampleUnitTest { - @Test - public void addition_isCorrect() throws Exception { - assertEquals(4, 2 + 2); - } -} \ No newline at end of file diff --git a/mobile/thirdparty/transistor-background-fetch/android/build.gradle b/mobile/thirdparty/transistor-background-fetch/android/build.gradle deleted file mode 100644 index c46c347b1..000000000 --- a/mobile/thirdparty/transistor-background-fetch/android/build.gradle +++ /dev/null @@ -1,34 +0,0 @@ -// Top-level build file where you can add configuration options common to all sub-projects/modules. - -buildscript { - repositories { - google() - jcenter() - } - - dependencies { - classpath 'com.android.tools.build:gradle:3.5.0' - - - // NOTE: Do not place your application dependencies here; they belong - // in the individual module build.gradle files - } -} - -allprojects { - repositories { - google() - jcenter() - } -} - -task clean(type: Delete) { - delete rootProject.buildDir -} - -ext { - compileSdkVersion = 29 - targetSdkVersion = 29 - buildToolsVersion = "29.0.6" - appCompatVersion = "1.1.0" -} diff --git a/mobile/thirdparty/transistor-background-fetch/android/gradle.properties b/mobile/thirdparty/transistor-background-fetch/android/gradle.properties deleted file mode 100644 index 85104bf95..000000000 --- a/mobile/thirdparty/transistor-background-fetch/android/gradle.properties +++ /dev/null @@ -1,23 +0,0 @@ -# Project-wide Gradle settings. - -# IDE (e.g. Android Studio) users: -# Gradle settings configured through the IDE *will override* -# any settings specified in this file. - -# For more details on how to configure your build environment visit -# http://www.gradle.org/docs/current/userguide/build_environment.html - -# Specifies the JVM arguments used for the daemon process. -# The setting is particularly useful for tweaking memory settings. -org.gradle.jvmargs=-Xmx1536m - -# When configured, Gradle will run in incubating parallel mode. -# This option should only be used with decoupled projects. More details, visit -# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects -# org.gradle.parallel=true - -VERSION_NAME=0.5.0 -VERSION_CODE=15 - -android.useAndroidX=true -android.enableJetifier=true diff --git a/mobile/thirdparty/transistor-background-fetch/android/gradle/wrapper/gradle-wrapper.jar b/mobile/thirdparty/transistor-background-fetch/android/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index 13372aef5..000000000 Binary files a/mobile/thirdparty/transistor-background-fetch/android/gradle/wrapper/gradle-wrapper.jar and /dev/null differ diff --git a/mobile/thirdparty/transistor-background-fetch/android/gradle/wrapper/gradle-wrapper.properties b/mobile/thirdparty/transistor-background-fetch/android/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index 7a8c57bad..000000000 --- a/mobile/thirdparty/transistor-background-fetch/android/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,7 +0,0 @@ -#Thu Feb 09 18:40:48 IST 2023 -distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip -distributionPath=wrapper/dists -zipStorePath=wrapper/dists -zipStoreBase=GRADLE_USER_HOME -distributionSha256Sum=10065868c78f1207afb3a92176f99a37d753a513dff453abb6b5cceda4058cda diff --git a/mobile/thirdparty/transistor-background-fetch/android/gradlew b/mobile/thirdparty/transistor-background-fetch/android/gradlew deleted file mode 100755 index 9d82f7891..000000000 --- a/mobile/thirdparty/transistor-background-fetch/android/gradlew +++ /dev/null @@ -1,160 +0,0 @@ -#!/usr/bin/env bash - -############################################################################## -## -## Gradle start up script for UN*X -## -############################################################################## - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" - -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" - -warn ( ) { - echo "$*" -} - -die ( ) { - echo - echo "$*" - echo - exit 1 -} - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; -esac - -# Attempt to set APP_HOME -# Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null - -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar - -# Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - else - JAVACMD="$JAVA_HOME/bin/java" - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -else - JAVACMD="java" - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." -fi - -# Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi -fi - -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi - -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi - # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" - fi - i=$((i+1)) - done - case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac -fi - -# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules -function splitJvmOpts() { - JVM_OPTS=("$@") -} -eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS -JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" - -exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/mobile/thirdparty/transistor-background-fetch/android/gradlew.bat b/mobile/thirdparty/transistor-background-fetch/android/gradlew.bat deleted file mode 100644 index 8a0b282aa..000000000 --- a/mobile/thirdparty/transistor-background-fetch/android/gradlew.bat +++ /dev/null @@ -1,90 +0,0 @@ -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto init - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:init -@rem Get command-line arguments, handling Windowz variants - -if not "%OS%" == "Windows_NT" goto win9xME_args -if "%@eval[2+2]" == "4" goto 4NT_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* -goto execute - -:4NT_args -@rem Get arguments from the 4NT Shell from JP Software -set CMD_LINE_ARGS=%$ - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega diff --git a/mobile/thirdparty/transistor-background-fetch/android/settings.gradle b/mobile/thirdparty/transistor-background-fetch/android/settings.gradle deleted file mode 100644 index 8b053e2cc..000000000 --- a/mobile/thirdparty/transistor-background-fetch/android/settings.gradle +++ /dev/null @@ -1 +0,0 @@ -include ':app', ':tsbackgroundfetch' diff --git a/mobile/thirdparty/transistor-background-fetch/android/tsbackgroundfetch/.gitignore b/mobile/thirdparty/transistor-background-fetch/android/tsbackgroundfetch/.gitignore deleted file mode 100644 index 796b96d1c..000000000 --- a/mobile/thirdparty/transistor-background-fetch/android/tsbackgroundfetch/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build diff --git a/mobile/thirdparty/transistor-background-fetch/android/tsbackgroundfetch/build.gradle b/mobile/thirdparty/transistor-background-fetch/android/tsbackgroundfetch/build.gradle deleted file mode 100644 index d64e6cfdd..000000000 --- a/mobile/thirdparty/transistor-background-fetch/android/tsbackgroundfetch/build.gradle +++ /dev/null @@ -1,136 +0,0 @@ -apply plugin: 'com.android.library' -apply plugin: 'maven' -apply plugin: 'maven-publish' - -android { - compileSdkVersion rootProject.compileSdkVersion - defaultConfig { - minSdkVersion 16 - targetSdkVersion rootProject.targetSdkVersion - versionCode 1 - versionName "1.0" - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - } - - buildTypes { - release { - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' - } - } - publishing { - publications { - tslocationmanager(MavenPublication) { - groupId 'com.transistorsoft' - artifactId 'tsbackgroundfetch' - version VERSION_NAME - artifact("$buildDir/outputs/aar/tsbackgroundfetch-release.aar") - - } - } - repositories { - mavenLocal() - } - } -} - -dependencies { - testImplementation 'junit:junit:4.12' - androidTestImplementation 'androidx.test:runner:1.2.0' - - implementation fileTree(dir: 'libs', include: ['*.jar']) - - //implementation "androidx.appcompat:appcompat:$rootProject.appCompatVersion" - -} - -// Build Release -task buildRelease { task -> - task.dependsOn 'cordovaRelease' - task.dependsOn 'reactNativeRelease' - task.dependsOn 'nativeScriptRelease' - task.dependsOn 'flutterRelease' -} - -// Publish Release. -task publishRelease { task -> - task.dependsOn 'assembleRelease' -} -tasks["publishRelease"].mustRunAfter("assembleRelease") -tasks["publishRelease"].finalizedBy("publish") - -def WORKSPACE_PATH = "/Volumes/Glyph2TB/Users/chris/workspace" - -// Build local maven repo. -def LIBRARY_PATH = "com/transistorsoft/tsbackgroundfetch" -task buildLocalRepository { task -> - task.dependsOn 'publishRelease' - doLast { - delete "$buildDir/repo-local" - copy { - from "$buildDir/repo/$LIBRARY_PATH/$VERSION_NAME" - into "$buildDir/repo-local/$LIBRARY_PATH/$VERSION_NAME" - } - copy { - from("$buildDir/repo/$LIBRARY_PATH/maven-metadata.xml") - into("$buildDir/repo-local/$LIBRARY_PATH") - } - } -} - -def cordovaDir = "$WORKSPACE_PATH/cordova/background-geolocation/cordova-plugin-background-fetch" -task cordovaRelease { task -> - task.dependsOn 'buildLocalRepository' - doLast { - delete "$cordovaDir/src/android/libs" - copy { - // Maven repo format. - from("$buildDir/repo-local") - into("$cordovaDir/src/android/libs") - // OLD FORMAT - //from("$buildDir/outputs/aar/tsbackgroundfetch-release.aar") - //into("$cordovaDir/src/android/libs/tsbackgroundfetch") - //rename(/(.*)-release/, '$1-' + VERSION_NAME) - } - } -} - -def reactNativeDir = "$WORKSPACE_PATH/react/background-geolocation/react-native-background-fetch" -task reactNativeRelease { task -> - task.dependsOn 'buildLocalRepository' - doLast { - delete "$reactNativeDir/android/libs" - copy { - // Maven repo format. - from("$buildDir/repo-local") - into("$reactNativeDir/android/libs") - // OLD format. - //from("$buildDir/outputs/aar/tsbackgroundfetch-release.aar") - //into("$reactNativeDir/android/libs") - //rename(/(.*)-release/, '$1-' + VERSION_NAME) - } - } -} - -def flutterDir = "$WORKSPACE_PATH/background-geolocation/flutter/flutter_background_fetch" -task flutterRelease { task -> - task.dependsOn 'buildLocalRepository' - doLast { - delete "$flutterDir/android/libs" - copy { - // Maven repo format. - from("$buildDir/repo-local") - into("$flutterDir/android/libs") - // OLD format. - //from("$buildDir/outputs/aar/tsbackgroundfetch-release.aar") - //into("$flutterDir/android/libs") - //rename(/(.*)-release/, '$1-' + VERSION_NAME) - } - } -} - -task nativeScriptRelease(type: Copy) { - from('./build/outputs/aar/tsbackgroundfetch-release.aar') - into("$WORKSPACE_PATH/NativeScript/background-geolocation/nativescript-background-fetch/src/platforms/android/libs") - rename('tsbackgroundfetch-release.aar', 'tsbackgroundfetch.aar') -} diff --git a/mobile/thirdparty/transistor-background-fetch/android/tsbackgroundfetch/proguard-rules.pro b/mobile/thirdparty/transistor-background-fetch/android/tsbackgroundfetch/proguard-rules.pro deleted file mode 100644 index f1b424510..000000000 --- a/mobile/thirdparty/transistor-background-fetch/android/tsbackgroundfetch/proguard-rules.pro +++ /dev/null @@ -1,21 +0,0 @@ -# Add project specific ProGuard rules here. -# You can control the set of applied configuration files using the -# proguardFiles setting in build.gradle. -# -# For more details, see -# http://developer.android.com/guide/developing/tools/proguard.html - -# If your project uses WebView with JS, uncomment the following -# and specify the fully qualified class name to the JavaScript interface -# class: -#-keepclassmembers class fqcn.of.javascript.interface.for.webview { -# public *; -#} - -# Uncomment this to preserve the line number information for -# debugging stack traces. -#-keepattributes SourceFile,LineNumberTable - -# If you keep the line number information, uncomment this to -# hide the original source file name. -#-renamesourcefileattribute SourceFile diff --git a/mobile/thirdparty/transistor-background-fetch/android/tsbackgroundfetch/src/androidTest/java/com/transistorsoft/tsbackgroundfetch/ExampleInstrumentedTest.java b/mobile/thirdparty/transistor-background-fetch/android/tsbackgroundfetch/src/androidTest/java/com/transistorsoft/tsbackgroundfetch/ExampleInstrumentedTest.java deleted file mode 100644 index 2bb391be3..000000000 --- a/mobile/thirdparty/transistor-background-fetch/android/tsbackgroundfetch/src/androidTest/java/com/transistorsoft/tsbackgroundfetch/ExampleInstrumentedTest.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.transistorsoft.tsbackgroundfetch; - -import android.content.Context; -import android.support.test.InstrumentationRegistry; -import android.support.test.runner.AndroidJUnit4; - -import org.junit.Test; -import org.junit.runner.RunWith; - -import static org.junit.Assert.*; - -/** - * Instrumented test, which will execute on an Android device. - * - * @see Testing documentation - */ -@RunWith(AndroidJUnit4.class) -public class ExampleInstrumentedTest { - @Test - public void useAppContext() throws Exception { - // Context of the app under test. - Context appContext = InstrumentationRegistry.getTargetContext(); - - assertEquals("com.transistorsoft.tsbackgroundfetch.test", appContext.getPackageName()); - } -} diff --git a/mobile/thirdparty/transistor-background-fetch/android/tsbackgroundfetch/src/main/AndroidManifest.xml b/mobile/thirdparty/transistor-background-fetch/android/tsbackgroundfetch/src/main/AndroidManifest.xml deleted file mode 100644 index 1cd70cd57..000000000 --- a/mobile/thirdparty/transistor-background-fetch/android/tsbackgroundfetch/src/main/AndroidManifest.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/mobile/thirdparty/transistor-background-fetch/android/tsbackgroundfetch/src/main/java/com/transistorsoft/tsbackgroundfetch/BGTask.java b/mobile/thirdparty/transistor-background-fetch/android/tsbackgroundfetch/src/main/java/com/transistorsoft/tsbackgroundfetch/BGTask.java deleted file mode 100644 index a858dcf59..000000000 --- a/mobile/thirdparty/transistor-background-fetch/android/tsbackgroundfetch/src/main/java/com/transistorsoft/tsbackgroundfetch/BGTask.java +++ /dev/null @@ -1,279 +0,0 @@ -package com.transistorsoft.tsbackgroundfetch; - -import android.app.AlarmManager; -import android.app.PendingIntent; -import android.app.job.JobInfo; -import android.app.job.JobScheduler; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.os.Build; -import android.os.PersistableBundle; -import android.util.Log; - -import org.json.JSONException; -import org.json.JSONObject; - -import java.lang.reflect.Constructor; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.concurrent.TimeUnit; - -public class BGTask { - static int MAX_TIME = 60000; - - private static final List mTasks = new ArrayList<>(); - - static BGTask getTask(String taskId) { - synchronized (mTasks) { - for (BGTask task : mTasks) { - if (task.hasTaskId(taskId)) return task; - } - } - return null; - } - - static void addTask(BGTask task) { - synchronized (mTasks) { - mTasks.add(task); - } - } - - static void removeTask(String taskId) { - synchronized (mTasks) { - BGTask found = null; - for (BGTask task : mTasks) { - if (task.hasTaskId(taskId)) { - found = task; - break; - } - } - if (found != null) { - mTasks.remove(found); - } - } - } - - static void clear() { - synchronized (mTasks) { - mTasks.clear(); - } - } - - private FetchJobService.CompletionHandler mCompletionHandler; - private String mTaskId; - private int mJobId; - private Runnable mTimeoutTask; - private boolean mTimedout = false; - - BGTask(final Context context, String taskId, FetchJobService.CompletionHandler handler, int jobId) { - mTaskId = taskId; - mCompletionHandler = handler; - mJobId = jobId; - - mTimeoutTask = new Runnable() { - @Override public void run() { - onTimeout(context); - } - }; - BackgroundFetch.getUiHandler().postDelayed(mTimeoutTask, MAX_TIME); - } - - public boolean getTimedOut() { - return mTimedout; - } - - public String getTaskId() { return mTaskId; } - - int getJobId() { return mJobId; } - - boolean hasTaskId(String taskId) { - return ((mTaskId != null) && mTaskId.equalsIgnoreCase(taskId)); - } - - void setCompletionHandler(FetchJobService.CompletionHandler handler) { - mCompletionHandler = handler; - } - - void finish() { - if (mCompletionHandler != null) { - mCompletionHandler.finish(); - } - if (mTimeoutTask != null) { - BackgroundFetch.getUiHandler().removeCallbacks(mTimeoutTask); - } - mCompletionHandler = null; - removeTask(mTaskId); - } - - static void schedule(Context context, BackgroundFetchConfig config) { - Log.d(BackgroundFetch.TAG, config.toString()); - - long interval = (config.isFetchTask()) ? (TimeUnit.MINUTES.toMillis(config.getMinimumFetchInterval())) : config.getDelay(); - - if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && !config.getForceAlarmManager()) { - // API 21+ uses new JobScheduler API - - JobScheduler jobScheduler = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE); - JobInfo.Builder builder = new JobInfo.Builder(config.getJobId(), new ComponentName(context, FetchJobService.class)) - .setRequiredNetworkType(config.getRequiredNetworkType()) - .setRequiresDeviceIdle(config.getRequiresDeviceIdle()) - .setRequiresCharging(config.getRequiresCharging()) - .setPersisted(config.getStartOnBoot() && !config.getStopOnTerminate()); - - if (config.getPeriodic()) { - if (android.os.Build.VERSION.SDK_INT >= 24) { - builder.setPeriodic(interval, interval); - } else { - builder.setPeriodic(interval); - } - } else { - builder.setMinimumLatency(interval); - } - PersistableBundle extras = new PersistableBundle(); - extras.putString(BackgroundFetchConfig.FIELD_TASK_ID, config.getTaskId()); - builder.setExtras(extras); - - if (android.os.Build.VERSION.SDK_INT >= 26) { - builder.setRequiresStorageNotLow(config.getRequiresStorageNotLow()); - builder.setRequiresBatteryNotLow(config.getRequiresBatteryNotLow()); - } - if (jobScheduler != null) { - jobScheduler.schedule(builder.build()); - } - } else { - // Everyone else get AlarmManager - AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); - if (alarmManager != null) { - PendingIntent pi = getAlarmPI(context, config.getTaskId()); - long delay = System.currentTimeMillis() + interval; - if (config.getPeriodic()) { - alarmManager.setRepeating(AlarmManager.RTC_WAKEUP, delay, interval, pi); - } else { - if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, delay, pi); - } else if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { - alarmManager.setExact(AlarmManager.RTC_WAKEUP, delay, pi); - } else { - alarmManager.set(AlarmManager.RTC_WAKEUP, delay, pi); - } - } - } - } - } - - void onTimeout(Context context) { - mTimedout = true; - Log.d(BackgroundFetch.TAG, "[BGTask] timeout: " + mTaskId); - - BackgroundFetch adapter = BackgroundFetch.getInstance(context); - - if (adapter.isMainActivityActive()) { - BackgroundFetch.Callback callback = adapter.getFetchCallback(); - if (callback != null) { - callback.onTimeout(mTaskId); - } - } else { - BackgroundFetchConfig config = adapter.getConfig(mTaskId); - if (config != null) { - if (config.getJobService() != null) { - fireHeadlessEvent(context, config); - } else { - adapter.finish(mTaskId); - } - } else { - Log.e(BackgroundFetch.TAG, "[BGTask] failed to load config for taskId: " + mTaskId); - adapter.finish(mTaskId); - } - } - } - - // Fire a headless background-fetch event by reflecting an instance of Config.jobServiceClass. - // Will attempt to reflect upon two different forms of Headless class: - // 1: new HeadlessTask(context, taskId) - // or - // 2: new HeadlessTask().onFetch(context, taskId); - // - void fireHeadlessEvent(Context context, BackgroundFetchConfig config) throws Error { - try { - // Get class via reflection. - Class HeadlessClass = Class.forName(config.getJobService()); - Class[] types = { Context.class, BGTask.class }; - Object[] params = { context, this}; - try { - // 1: new HeadlessTask(context, taskId); - Constructor constructor = HeadlessClass.getDeclaredConstructor(types); - constructor.newInstance(params); - } catch (NoSuchMethodException e) { - // 2: new HeadlessTask().onFetch(context, taskId); - Constructor constructor = HeadlessClass.getConstructor(); - Object instance = constructor.newInstance(); - Method onFetch = instance.getClass().getDeclaredMethod("onFetch", types); - onFetch.invoke(instance, params); - } - } catch (ClassNotFoundException e) { - throw new Error(e.getMessage()); - } catch (NoSuchMethodException e) { - throw new Error(e.getMessage()); - } catch (IllegalAccessException e) { - throw new Error(e.getMessage()); - } catch (InstantiationException e) { - throw new Error(e.getMessage()); - } catch (InvocationTargetException e) { - throw new Error(e.getMessage()); - } - } - - static void cancel(Context context, String taskId, int jobId) { - Log.i(BackgroundFetch.TAG, "- cancel taskId=" + taskId + ", jobId=" + jobId); - if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && (jobId != 0)) { - JobScheduler jobScheduler = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE); - if (jobScheduler != null) { - jobScheduler.cancel(jobId); - } - } else { - AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); - if (alarmManager != null) { - alarmManager.cancel(BGTask.getAlarmPI(context, taskId)); - } - } - } - - static PendingIntent getAlarmPI(Context context, String taskId) { - Intent intent = new Intent(context, FetchAlarmReceiver.class); - intent.setAction(taskId); - return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT); - } - - public String toString() { - return "[BGTask taskId=" + mTaskId + "]"; - } - - public Map toMap() { - Map map = new HashMap<>(); - map.put("taskId", mTaskId); - map.put("timeout", mTimedout); - return map; - } - - public JSONObject toJson() { - JSONObject json = new JSONObject(); - try { - json.put("taskId", mTaskId); - json.put("timeout", mTimedout); - } catch (JSONException e) { - e.printStackTrace(); - } - return json; - } - - static class Error extends RuntimeException { - public Error(String msg) { - super(msg); - } - } -} diff --git a/mobile/thirdparty/transistor-background-fetch/android/tsbackgroundfetch/src/main/java/com/transistorsoft/tsbackgroundfetch/BackgroundFetch.java b/mobile/thirdparty/transistor-background-fetch/android/tsbackgroundfetch/src/main/java/com/transistorsoft/tsbackgroundfetch/BackgroundFetch.java deleted file mode 100644 index 8c374c65c..000000000 --- a/mobile/thirdparty/transistor-background-fetch/android/tsbackgroundfetch/src/main/java/com/transistorsoft/tsbackgroundfetch/BackgroundFetch.java +++ /dev/null @@ -1,306 +0,0 @@ -package com.transistorsoft.tsbackgroundfetch; - -import android.annotation.TargetApi; -import android.app.ActivityManager; - -import android.content.Context; -import android.os.Build; -import android.os.Handler; -import android.os.Looper; - -import android.util.Log; - -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; - -/** - * Created by chris on 2018-01-11. - */ - -public class BackgroundFetch { - public static final String TAG = "TSBackgroundFetch"; - - public static final String ACTION_CONFIGURE = "configure"; - public static final String ACTION_START = "start"; - public static final String ACTION_STOP = "stop"; - public static final String ACTION_FINISH = "finish"; - public static final String ACTION_STATUS = "status"; - public static final String ACTION_FORCE_RELOAD = TAG + "-forceReload"; - - public static final String EVENT_FETCH = ".event.BACKGROUND_FETCH"; - - public static final int STATUS_AVAILABLE = 2; - - private static BackgroundFetch mInstance = null; - - private static ExecutorService sThreadPool; - - private static Handler uiHandler; - - @SuppressWarnings({"WeakerAccess"}) - public static Handler getUiHandler() { - if (uiHandler == null) { - uiHandler = new Handler(Looper.getMainLooper()); - } - return uiHandler; - } - - @SuppressWarnings({"WeakerAccess"}) - public static ExecutorService getThreadPool() { - if (sThreadPool == null) { - sThreadPool = Executors.newCachedThreadPool(); - } - return sThreadPool; - } - - @SuppressWarnings({"WeakerAccess"}) - public static BackgroundFetch getInstance(Context context) { - if (mInstance == null) { - mInstance = getInstanceSynchronized(context.getApplicationContext()); - } - return mInstance; - } - - private static synchronized BackgroundFetch getInstanceSynchronized(Context context) { - if (mInstance == null) mInstance = new BackgroundFetch(context.getApplicationContext()); - return mInstance; - } - - private Context mContext; - private BackgroundFetch.Callback mFetchCallback; - - private final Map mConfig = new HashMap<>(); - - private BackgroundFetch(Context context) { - mContext = context; - } - - @SuppressWarnings({"unused"}) - public void configure(BackgroundFetchConfig config, BackgroundFetch.Callback callback) { - Log.d(TAG, "- " + ACTION_CONFIGURE); - mFetchCallback = callback; - - synchronized (mConfig) { - mConfig.put(config.getTaskId(), config); - } - start(config.getTaskId()); - } - - void onBoot() { - BackgroundFetchConfig.load(mContext, new BackgroundFetchConfig.OnLoadCallback() { - @Override public void onLoad(List result) { - for (BackgroundFetchConfig config : result) { - if (!config.getStartOnBoot() || config.getStopOnTerminate()) { - config.destroy(mContext); - continue; - } - synchronized (mConfig) { - mConfig.put(config.getTaskId(), config); - } - if ((android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP_MR1) || config.getForceAlarmManager()) { - if (config.isFetchTask()) { - start(config.getTaskId()); - } else { - scheduleTask(config); - } - } - } - } - }); - } - - @SuppressWarnings({"WeakerAccess"}) - @TargetApi(21) - public void start(String fetchTaskId) { - Log.d(TAG, "- " + ACTION_START); - - BGTask task = BGTask.getTask(fetchTaskId); - if (task != null) { - Log.e(TAG, "[" + TAG + " start] Task " + fetchTaskId + " already registered"); - return; - } - registerTask(fetchTaskId); - } - - @SuppressWarnings({"WeakerAccess"}) - public void stop(String taskId) { - String msg = "- " + ACTION_STOP; - if (taskId != null) { - msg += ": " + taskId; - } - Log.d(TAG, msg); - - if (taskId == null) { - synchronized (mConfig) { - for (BackgroundFetchConfig config : mConfig.values()) { - BGTask task = BGTask.getTask(config.getTaskId()); - if (task != null) { - task.finish(); - BGTask.removeTask(config.getTaskId()); - } - BGTask.cancel(mContext, config.getTaskId(), config.getJobId()); - config.destroy(mContext); - } - BGTask.clear(); - } - } else { - BGTask task = BGTask.getTask(taskId); - if (task != null) { - task.finish(); - BGTask.removeTask(task.getTaskId()); - } - BackgroundFetchConfig config = getConfig(taskId); - if (config != null) { - config.destroy(mContext); - BGTask.cancel(mContext, config.getTaskId(), config.getJobId()); - } - } - } - - @SuppressWarnings({"WeakerAccess"}) - public void scheduleTask(BackgroundFetchConfig config) { - synchronized (mConfig) { - if (mConfig.containsKey(config.getTaskId())) { - // This BackgroundFetchConfig already exists? Should we halt any existing Job/Alarm here? - } - config.save(mContext); - mConfig.put(config.getTaskId(), config); - } - String taskId = config.getTaskId(); - registerTask(taskId); - } - - @SuppressWarnings({"WeakerAccess"}) - public void finish(String taskId) { - Log.d(TAG, "- " + ACTION_FINISH + ": " + taskId); - - BGTask task = BGTask.getTask(taskId); - if (task != null) { - task.finish(); - } - - BackgroundFetchConfig config = getConfig(taskId); - - if ((config != null) && !config.getPeriodic()) { - config.destroy(mContext); - synchronized (mConfig) { - mConfig.remove(taskId); - } - } - } - - public int status() { - return STATUS_AVAILABLE; - } - - BackgroundFetch.Callback getFetchCallback() { - return mFetchCallback; - } - - void onFetch(final BGTask task) { - BGTask.addTask(task); - Log.d(TAG, "- Background Fetch event received: " + task.getTaskId()); - synchronized (mConfig) { - if (mConfig.isEmpty()) { - BackgroundFetchConfig.load(mContext, new BackgroundFetchConfig.OnLoadCallback() { - @Override - public void onLoad(List result) { - synchronized (mConfig) { - for (BackgroundFetchConfig config : result) { - mConfig.put(config.getTaskId(), config); - } - } - doFetch(task); - } - }); - - return; - } - } - doFetch(task); - } - - private void registerTask(String taskId) { - Log.d(TAG, "- registerTask: " + taskId); - - BackgroundFetchConfig config = getConfig(taskId); - - if (config == null) { - Log.e(TAG, "- registerTask failed to find BackgroundFetchConfig for taskId " + taskId); - return; - } - config.save(mContext); - - BGTask.schedule(mContext, config); - } - - private void doFetch(BGTask task) { - BackgroundFetchConfig config = getConfig(task.getTaskId()); - - if (config == null) { - BGTask.cancel(mContext, task.getTaskId(), task.getJobId()); - return; - } - - if (isMainActivityActive()) { - if (mFetchCallback != null) { - mFetchCallback.onFetch(task.getTaskId()); - } - } else if (config.getStopOnTerminate()) { - Log.d(TAG, "- Stopping on terminate"); - stop(task.getTaskId()); - } else if (config.getJobService() != null) { - try { - task.fireHeadlessEvent(mContext, config); - } catch (BGTask.Error e) { - Log.e(TAG, "Headless task error: " + e.getMessage()); - e.printStackTrace(); - } - } else { - // {stopOnTerminate: false, forceReload: false} with no Headless JobService?? Don't know what else to do here but stop - Log.w(TAG, "- BackgroundFetch event has occurred while app is terminated but there's no jobService configured to handle the event. BackgroundFetch will terminate."); - finish(task.getTaskId()); - stop(task.getTaskId()); - } - } - - @SuppressWarnings({"WeakerAccess", "deprecation"}) - public Boolean isMainActivityActive() { - Boolean isActive = false; - - if (mContext == null || mFetchCallback == null) { - return false; - } - ActivityManager activityManager = (ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE); - try { - List tasks = activityManager.getRunningTasks(Integer.MAX_VALUE); - for (ActivityManager.RunningTaskInfo task : tasks) { - if (mContext.getPackageName().equalsIgnoreCase(task.baseActivity.getPackageName())) { - isActive = true; - break; - } - } - } catch (java.lang.SecurityException e) { - Log.w(TAG, "TSBackgroundFetch attempted to determine if MainActivity is active but was stopped due to a missing permission. Please add the permission 'android.permission.GET_TASKS' to your AndroidManifest. See Installation steps for more information"); - throw e; - } - return isActive; - } - - BackgroundFetchConfig getConfig(String taskId) { - synchronized (mConfig) { - return (mConfig.containsKey(taskId)) ? mConfig.get(taskId) : null; - } - } - - /** - * @interface BackgroundFetch.Callback - */ - public interface Callback { - void onFetch(String taskId); - void onTimeout(String taskId); - } -} diff --git a/mobile/thirdparty/transistor-background-fetch/android/tsbackgroundfetch/src/main/java/com/transistorsoft/tsbackgroundfetch/BackgroundFetchConfig.java b/mobile/thirdparty/transistor-background-fetch/android/tsbackgroundfetch/src/main/java/com/transistorsoft/tsbackgroundfetch/BackgroundFetchConfig.java deleted file mode 100644 index 5ff655fe9..000000000 --- a/mobile/thirdparty/transistor-background-fetch/android/tsbackgroundfetch/src/main/java/com/transistorsoft/tsbackgroundfetch/BackgroundFetchConfig.java +++ /dev/null @@ -1,362 +0,0 @@ -package com.transistorsoft.tsbackgroundfetch; - -import android.app.job.JobInfo; -import android.content.Context; -import android.content.SharedPreferences; -import android.os.Build; -import android.util.Log; - -import org.json.JSONException; -import org.json.JSONObject; - -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -/** - * Created by chris on 2018-01-11. - */ - -public class BackgroundFetchConfig { - private Builder config; - - private static final int MINIMUM_FETCH_INTERVAL = 1; - private static final int DEFAULT_FETCH_INTERVAL = 15; - - public static final String FIELD_TASK_ID = "taskId"; - public static final String FIELD_MINIMUM_FETCH_INTERVAL = "minimumFetchInterval"; - public static final String FIELD_START_ON_BOOT = "startOnBoot"; - public static final String FIELD_REQUIRED_NETWORK_TYPE = "requiredNetworkType"; - public static final String FIELD_REQUIRES_BATTERY_NOT_LOW = "requiresBatteryNotLow"; - public static final String FIELD_REQUIRES_CHARGING = "requiresCharging"; - public static final String FIELD_REQUIRES_DEVICE_IDLE = "requiresDeviceIdle"; - public static final String FIELD_REQUIRES_STORAGE_NOT_LOW = "requiresStorageNotLow"; - public static final String FIELD_STOP_ON_TERMINATE = "stopOnTerminate"; - public static final String FIELD_JOB_SERVICE = "jobService"; - public static final String FIELD_FORCE_ALARM_MANAGER = "forceAlarmManager"; - public static final String FIELD_PERIODIC = "periodic"; - public static final String FIELD_DELAY = "delay"; - public static final String FIELD_IS_FETCH_TASK = "isFetchTask"; - - public static class Builder { - private String taskId; - private int minimumFetchInterval = DEFAULT_FETCH_INTERVAL; - private long delay = -1; - private boolean periodic = false; - private boolean forceAlarmManager = false; - private boolean stopOnTerminate = true; - private boolean startOnBoot = false; - private int requiredNetworkType = 0; - private boolean requiresBatteryNotLow = false; - private boolean requiresCharging = false; - private boolean requiresDeviceIdle = false; - private boolean requiresStorageNotLow = false; - private boolean isFetchTask = false; - - private String jobService = null; - - public Builder setTaskId(String taskId) { - this.taskId = taskId; - return this; - } - - public Builder setIsFetchTask(boolean value) { - this.isFetchTask = value; - return this; - } - - public Builder setMinimumFetchInterval(int fetchInterval) { - if (fetchInterval >= MINIMUM_FETCH_INTERVAL) { - this.minimumFetchInterval = fetchInterval; - } - return this; - } - - public Builder setStopOnTerminate(boolean stopOnTerminate) { - this.stopOnTerminate = stopOnTerminate; - return this; - } - - public Builder setStartOnBoot(boolean startOnBoot) { - this.startOnBoot = startOnBoot; - return this; - } - - public Builder setRequiredNetworkType(int networkType) { - - if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) { - if ( - (networkType != JobInfo.NETWORK_TYPE_ANY) && - (networkType != JobInfo.NETWORK_TYPE_CELLULAR) && - (networkType != JobInfo.NETWORK_TYPE_NONE) && - (networkType != JobInfo.NETWORK_TYPE_NOT_ROAMING) && - (networkType != JobInfo.NETWORK_TYPE_UNMETERED) - ) { - Log.e(BackgroundFetch.TAG, "[ERROR] Invalid " + FIELD_REQUIRED_NETWORK_TYPE + ": " + networkType + "; Defaulting to NETWORK_TYPE_NONE"); - networkType = JobInfo.NETWORK_TYPE_NONE; - } - this.requiredNetworkType = networkType; - } - return this; - } - - public Builder setRequiresBatteryNotLow(boolean value) { - this.requiresBatteryNotLow = value; - return this; - } - - public Builder setRequiresCharging(boolean value) { - this.requiresCharging = value; - return this; - } - - public Builder setRequiresDeviceIdle(boolean value) { - this.requiresDeviceIdle = value; - return this; - } - - public Builder setRequiresStorageNotLow(boolean value) { - this.requiresStorageNotLow = value; - return this; - } - - public Builder setJobService(String className) { - this.jobService = className; - return this; - } - - public Builder setForceAlarmManager(boolean value) { - this.forceAlarmManager = value; - return this; - } - - public Builder setPeriodic(boolean value) { - this.periodic = value; - return this; - } - - public Builder setDelay(long value) { - this.delay = value; - return this; - } - - public BackgroundFetchConfig build() { - return new BackgroundFetchConfig(this); - } - - public BackgroundFetchConfig load(Context context, String taskId) { - SharedPreferences preferences = context.getSharedPreferences(BackgroundFetch.TAG + ":" + taskId, 0); - if (preferences.contains(FIELD_TASK_ID)) { - setTaskId(preferences.getString(FIELD_TASK_ID, taskId)); - } - if (preferences.contains(FIELD_IS_FETCH_TASK)) { - setIsFetchTask(preferences.getBoolean(FIELD_IS_FETCH_TASK, isFetchTask)); - } - if (preferences.contains(FIELD_MINIMUM_FETCH_INTERVAL)) { - setMinimumFetchInterval(preferences.getInt(FIELD_MINIMUM_FETCH_INTERVAL, minimumFetchInterval)); - } - if (preferences.contains(FIELD_STOP_ON_TERMINATE)) { - setStopOnTerminate(preferences.getBoolean(FIELD_STOP_ON_TERMINATE, stopOnTerminate)); - } - if (preferences.contains(FIELD_REQUIRED_NETWORK_TYPE)) { - setRequiredNetworkType(preferences.getInt(FIELD_REQUIRED_NETWORK_TYPE, requiredNetworkType)); - } - if (preferences.contains(FIELD_REQUIRES_BATTERY_NOT_LOW)) { - setRequiresBatteryNotLow(preferences.getBoolean(FIELD_REQUIRES_BATTERY_NOT_LOW, requiresBatteryNotLow)); - } - if (preferences.contains(FIELD_REQUIRES_CHARGING)) { - setRequiresCharging(preferences.getBoolean(FIELD_REQUIRES_CHARGING, requiresCharging)); - } - if (preferences.contains(FIELD_REQUIRES_DEVICE_IDLE)) { - setRequiresDeviceIdle(preferences.getBoolean(FIELD_REQUIRES_DEVICE_IDLE, requiresDeviceIdle)); - } - if (preferences.contains(FIELD_REQUIRES_STORAGE_NOT_LOW)) { - setRequiresStorageNotLow(preferences.getBoolean(FIELD_REQUIRES_STORAGE_NOT_LOW, requiresStorageNotLow)); - } - if (preferences.contains(FIELD_START_ON_BOOT)) { - setStartOnBoot(preferences.getBoolean(FIELD_START_ON_BOOT, startOnBoot)); - } - if (preferences.contains(FIELD_JOB_SERVICE)) { - setJobService(preferences.getString(FIELD_JOB_SERVICE, null)); - } - if (preferences.contains(FIELD_FORCE_ALARM_MANAGER)) { - setForceAlarmManager(preferences.getBoolean(FIELD_FORCE_ALARM_MANAGER, forceAlarmManager)); - } - if (preferences.contains(FIELD_PERIODIC)) { - setPeriodic(preferences.getBoolean(FIELD_PERIODIC, periodic)); - } - if (preferences.contains(FIELD_DELAY)) { - setDelay(preferences.getLong(FIELD_DELAY, delay)); - } - return new BackgroundFetchConfig(this); - } - } - - private BackgroundFetchConfig(Builder builder) { - config = builder; - // Validate config - if (config.jobService == null) { - if (!config.stopOnTerminate) { - Log.w(BackgroundFetch.TAG, "- Configuration error: In order to use stopOnTerminate: false, you must set enableHeadless: true"); - config.setStopOnTerminate(true); - } - if (config.startOnBoot) { - Log.w(BackgroundFetch.TAG, "- Configuration error: In order to use startOnBoot: true, you must enableHeadless: true"); - config.setStartOnBoot(false); - } - } - } - - void save(Context context) { - SharedPreferences preferences = context.getSharedPreferences(BackgroundFetch.TAG, 0); - Set taskIds = preferences.getStringSet("tasks", new HashSet()); - if (taskIds == null) { - taskIds = new HashSet<>(); - } - if (!taskIds.contains(config.taskId)) { - Set newIds = new HashSet<>(taskIds); - newIds.add(config.taskId); - - SharedPreferences.Editor editor = preferences.edit(); - editor.putStringSet("tasks", newIds); - editor.apply(); - } - - SharedPreferences.Editor editor = context.getSharedPreferences(BackgroundFetch.TAG + ":" + config.taskId, 0).edit(); - - editor.putString(FIELD_TASK_ID, config.taskId); - editor.putBoolean(FIELD_IS_FETCH_TASK, config.isFetchTask); - editor.putInt(FIELD_MINIMUM_FETCH_INTERVAL, config.minimumFetchInterval); - editor.putBoolean(FIELD_STOP_ON_TERMINATE, config.stopOnTerminate); - editor.putBoolean(FIELD_START_ON_BOOT, config.startOnBoot); - editor.putInt(FIELD_REQUIRED_NETWORK_TYPE, config.requiredNetworkType); - editor.putBoolean(FIELD_REQUIRES_BATTERY_NOT_LOW, config.requiresBatteryNotLow); - editor.putBoolean(FIELD_REQUIRES_CHARGING, config.requiresCharging); - editor.putBoolean(FIELD_REQUIRES_DEVICE_IDLE, config.requiresDeviceIdle); - editor.putBoolean(FIELD_REQUIRES_STORAGE_NOT_LOW, config.requiresStorageNotLow); - editor.putString(FIELD_JOB_SERVICE, config.jobService); - editor.putBoolean(FIELD_FORCE_ALARM_MANAGER, config.forceAlarmManager); - editor.putBoolean(FIELD_PERIODIC, config.periodic); - editor.putLong(FIELD_DELAY, config.delay); - - editor.apply(); - } - - void destroy(Context context) { - SharedPreferences preferences = context.getSharedPreferences(BackgroundFetch.TAG, 0); - Set taskIds = preferences.getStringSet("tasks", new HashSet()); - if (taskIds == null) { - taskIds = new HashSet<>(); - } - if (taskIds.contains(config.taskId)) { - Set newIds = new HashSet<>(taskIds); - newIds.remove(config.taskId); - SharedPreferences.Editor editor = preferences.edit(); - editor.putStringSet("tasks", newIds); - editor.apply(); - } - if (!config.isFetchTask) { - SharedPreferences.Editor editor = context.getSharedPreferences(BackgroundFetch.TAG + ":" + config.taskId, 0).edit(); - editor.clear(); - editor.apply(); - } - } - - static int FETCH_JOB_ID = 999; - - boolean isFetchTask() { - return config.isFetchTask; - } - - public String getTaskId() { return config.taskId; } - public int getMinimumFetchInterval() { - return config.minimumFetchInterval; - } - - public int getRequiredNetworkType() { return config.requiredNetworkType; } - public boolean getRequiresBatteryNotLow() { return config.requiresBatteryNotLow; } - public boolean getRequiresCharging() { return config.requiresCharging; } - public boolean getRequiresDeviceIdle() { return config.requiresDeviceIdle; } - public boolean getRequiresStorageNotLow() { return config.requiresStorageNotLow; } - public boolean getStopOnTerminate() { - return config.stopOnTerminate; - } - public boolean getStartOnBoot() { - return config.startOnBoot; - } - - public String getJobService() { return config.jobService; } - - public boolean getForceAlarmManager() { - return config.forceAlarmManager; - } - - public boolean getPeriodic() { - return config.periodic || isFetchTask(); - } - - public long getDelay() { - return config.delay; - } - - int getJobId() { - if (config.forceAlarmManager) { - return 0; - } else { - return (isFetchTask()) ? FETCH_JOB_ID : config.taskId.hashCode(); - } - } - - public String toString() { - JSONObject output = new JSONObject(); - try { - output.put(FIELD_TASK_ID, config.taskId); - output.put(FIELD_IS_FETCH_TASK, config.isFetchTask); - output.put(FIELD_MINIMUM_FETCH_INTERVAL, config.minimumFetchInterval); - output.put(FIELD_STOP_ON_TERMINATE, config.stopOnTerminate); - output.put(FIELD_REQUIRED_NETWORK_TYPE, config.requiredNetworkType); - output.put(FIELD_REQUIRES_BATTERY_NOT_LOW, config.requiresBatteryNotLow); - output.put(FIELD_REQUIRES_CHARGING, config.requiresCharging); - output.put(FIELD_REQUIRES_DEVICE_IDLE, config.requiresDeviceIdle); - output.put(FIELD_REQUIRES_STORAGE_NOT_LOW, config.requiresStorageNotLow); - output.put(FIELD_START_ON_BOOT, config.startOnBoot); - output.put(FIELD_JOB_SERVICE, config.jobService); - output.put(FIELD_FORCE_ALARM_MANAGER, config.forceAlarmManager); - output.put(FIELD_PERIODIC, getPeriodic()); - output.put(FIELD_DELAY, config.delay); - - return output.toString(2); - } catch (JSONException e) { - e.printStackTrace(); - return output.toString(); - } - } - - static void load(final Context context, final OnLoadCallback callback) { - BackgroundFetch.getThreadPool().execute(new Runnable() { - @Override - public void run() { - final List result = new ArrayList<>(); - - SharedPreferences preferences = context.getSharedPreferences(BackgroundFetch.TAG, 0); - Set taskIds = preferences.getStringSet("tasks", new HashSet()); - - if (taskIds != null) { - for (String taskId : taskIds) { - result.add(new BackgroundFetchConfig.Builder().load(context, taskId)); - } - } - BackgroundFetch.getUiHandler().post(new Runnable() { - @Override public void run() { - callback.onLoad(result); - } - }); - } - }); - } - - interface OnLoadCallback { - void onLoad(Listconfig); - } -} diff --git a/mobile/thirdparty/transistor-background-fetch/android/tsbackgroundfetch/src/main/java/com/transistorsoft/tsbackgroundfetch/BootReceiver.java b/mobile/thirdparty/transistor-background-fetch/android/tsbackgroundfetch/src/main/java/com/transistorsoft/tsbackgroundfetch/BootReceiver.java deleted file mode 100644 index b161c082e..000000000 --- a/mobile/thirdparty/transistor-background-fetch/android/tsbackgroundfetch/src/main/java/com/transistorsoft/tsbackgroundfetch/BootReceiver.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.transistorsoft.tsbackgroundfetch; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.util.Log; - -/** - * Created by chris on 2018-01-15. - */ - -public class BootReceiver extends BroadcastReceiver { - - @Override - public void onReceive(final Context context, Intent intent) { - String action = intent.getAction(); - Log.d(BackgroundFetch.TAG, "BootReceiver: " + action); - BackgroundFetch.getThreadPool().execute(new Runnable() { - @Override public void run() { - BackgroundFetch.getInstance(context.getApplicationContext()).onBoot(); - } - }); - } -} diff --git a/mobile/thirdparty/transistor-background-fetch/android/tsbackgroundfetch/src/main/java/com/transistorsoft/tsbackgroundfetch/FetchAlarmReceiver.java b/mobile/thirdparty/transistor-background-fetch/android/tsbackgroundfetch/src/main/java/com/transistorsoft/tsbackgroundfetch/FetchAlarmReceiver.java deleted file mode 100644 index afcb900dd..000000000 --- a/mobile/thirdparty/transistor-background-fetch/android/tsbackgroundfetch/src/main/java/com/transistorsoft/tsbackgroundfetch/FetchAlarmReceiver.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.transistorsoft.tsbackgroundfetch; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.os.PowerManager; -import android.util.Log; - -import static android.content.Context.POWER_SERVICE; - -/** - * Created by chris on 2018-01-11. - */ - -public class FetchAlarmReceiver extends BroadcastReceiver { - - @Override - public void onReceive(final Context context, Intent intent) { - PowerManager powerManager = (PowerManager) context.getSystemService(POWER_SERVICE); - final PowerManager.WakeLock wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, BackgroundFetch.TAG + "::" + intent.getAction()); - // WakeLock expires in MAX_TIME + 4s buffer. - wakeLock.acquire((BGTask.MAX_TIME + 4000)); - - final String taskId = intent.getAction(); - - final FetchJobService.CompletionHandler completionHandler = new FetchJobService.CompletionHandler() { - @Override - public void finish() { - if (wakeLock.isHeld()) { - wakeLock.release(); - Log.d(BackgroundFetch.TAG, "- FetchAlarmReceiver finish"); - } - } - }; - - BGTask task = new BGTask(context, taskId, completionHandler, 0); - - BackgroundFetch.getInstance(context.getApplicationContext()).onFetch(task); - } -} diff --git a/mobile/thirdparty/transistor-background-fetch/android/tsbackgroundfetch/src/main/java/com/transistorsoft/tsbackgroundfetch/FetchJobService.java b/mobile/thirdparty/transistor-background-fetch/android/tsbackgroundfetch/src/main/java/com/transistorsoft/tsbackgroundfetch/FetchJobService.java deleted file mode 100644 index 0938d5356..000000000 --- a/mobile/thirdparty/transistor-background-fetch/android/tsbackgroundfetch/src/main/java/com/transistorsoft/tsbackgroundfetch/FetchJobService.java +++ /dev/null @@ -1,50 +0,0 @@ -package com.transistorsoft.tsbackgroundfetch; - -import android.annotation.TargetApi; -import android.app.job.JobParameters; -import android.app.job.JobService; -import android.os.PersistableBundle; -import android.util.Log; - -/** - * Created by chris on 2018-01-11. - */ -@TargetApi(21) -public class FetchJobService extends JobService { - @Override - public boolean onStartJob(final JobParameters params) { - PersistableBundle extras = params.getExtras(); - final String taskId = extras.getString(BackgroundFetchConfig.FIELD_TASK_ID); - - CompletionHandler completionHandler = new CompletionHandler() { - @Override - public void finish() { - Log.d(BackgroundFetch.TAG, "- jobFinished"); - jobFinished(params, false); - } - }; - BGTask task = new BGTask(this, taskId, completionHandler, params.getJobId()); - BackgroundFetch.getInstance(getApplicationContext()).onFetch(task); - - return true; - } - - @Override - public boolean onStopJob(final JobParameters params) { - Log.d(BackgroundFetch.TAG, "- onStopJob"); - - PersistableBundle extras = params.getExtras(); - final String taskId = extras.getString(BackgroundFetchConfig.FIELD_TASK_ID); - - BGTask task = BGTask.getTask(taskId); - if (task != null) { - task.onTimeout(getApplicationContext()); - } - jobFinished(params, false); - return true; - } - - public interface CompletionHandler { - void finish(); - } -} diff --git a/mobile/thirdparty/transistor-background-fetch/android/tsbackgroundfetch/src/main/res/values/strings.xml b/mobile/thirdparty/transistor-background-fetch/android/tsbackgroundfetch/src/main/res/values/strings.xml deleted file mode 100644 index 17b4bbaa0..000000000 --- a/mobile/thirdparty/transistor-background-fetch/android/tsbackgroundfetch/src/main/res/values/strings.xml +++ /dev/null @@ -1,3 +0,0 @@ - - TSBackgroundFetch - diff --git a/mobile/thirdparty/transistor-background-fetch/android/tsbackgroundfetch/src/test/java/com/transistorsoft/tsbackgroundfetch/ExampleUnitTest.java b/mobile/thirdparty/transistor-background-fetch/android/tsbackgroundfetch/src/test/java/com/transistorsoft/tsbackgroundfetch/ExampleUnitTest.java deleted file mode 100644 index 99449e071..000000000 --- a/mobile/thirdparty/transistor-background-fetch/android/tsbackgroundfetch/src/test/java/com/transistorsoft/tsbackgroundfetch/ExampleUnitTest.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.transistorsoft.tsbackgroundfetch; - -import org.junit.Test; - -import static org.junit.Assert.*; - -/** - * Example local unit test, which will execute on the development machine (host). - * - * @see Testing documentation - */ -public class ExampleUnitTest { - @Test - public void addition_isCorrect() throws Exception { - assertEquals(4, 2 + 2); - } -} \ No newline at end of file diff --git a/server/README.md b/server/README.md index 9c4b2f696..06fdcb518 100644 --- a/server/README.md +++ b/server/README.md @@ -113,6 +113,11 @@ repository's [Discussions](https://github.com/ente-io/ente/discussions), or on try to clarify, and also document such FAQs. Please feel free to open documentation PRs around this too. +> [!TIP] +> +> You can find more guides and documentation around self-hosting at +> [help.ente.io/self-hosting](https://help.ente.io/self-hosting). + ## Thanks ❤️ We've had great fun with this combination (Golang + Postgres + Docker), and we diff --git a/server/cmd/museum/main.go b/server/cmd/museum/main.go index cb684499d..14ceb3e3b 100644 --- a/server/cmd/museum/main.go +++ b/server/cmd/museum/main.go @@ -14,6 +14,8 @@ import ( "syscall" "time" + "github.com/ente-io/museum/pkg/repo/two_factor_recovery" + "github.com/ente-io/museum/pkg/controller/cast" "github.com/ente-io/museum/pkg/controller/commonbilling" @@ -117,6 +119,14 @@ func main() { if err != nil { log.Fatal("Could not get host name", err) } + taskLockingRepo := &repo.TaskLockRepository{DB: db} + lockController := &lock.LockController{ + TaskLockingRepo: taskLockingRepo, + HostName: hostName, + } + // Note: during boot-up, release any locks that might have been left behind. + // This is a safety measure to ensure that no locks are left behind in case of a crash or restart. + lockController.ReleaseHostLock() var latencyLogger = promauto.NewHistogramVec(prometheus.HistogramOpts{ Name: "museum_method_latency", @@ -137,13 +147,14 @@ func main() { twoFactorRepo := &repo.TwoFactorRepository{DB: db, SecretEncryptionKey: secretEncryptionKeyBytes} userAuthRepo := &repo.UserAuthRepository{DB: db} + 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} - taskLockingRepo := &repo.TaskLockRepository{DB: db} + notificationHistoryRepo := &repo.NotificationHistoryRepository{DB: db} queueRepo := &repo.QueueRepository{DB: db} objectRepo := &repo.ObjectRepository{DB: db, QueueRepo: queueRepo} @@ -169,10 +180,6 @@ func main() { discordController := discord.NewDiscordController(userRepo, hostName, environment) rateLimiter := middleware.NewRateLimitMiddleware(discordController) - lockController := &lock.LockController{ - TaskLockingRepo: taskLockingRepo, - HostName: hostName, - } emailNotificationCtrl := &email.EmailNotificationController{ UserRepo: userRepo, LockController: lockController, @@ -304,6 +311,7 @@ func main() { usageRepo, userAuthRepo, twoFactorRepo, + twoFactorRecoveryRepo, passkeysRepo, storagBonusRepo, fileRepo, @@ -429,6 +437,8 @@ func main() { publicAPI.POST("/users/two-factor/remove", userHandler.RemoveTwoFactor) publicAPI.POST("/users/two-factor/passkeys/begin", userHandler.BeginPasskeyAuthenticationCeremony) publicAPI.POST("/users/two-factor/passkeys/finish", userHandler.FinishPasskeyAuthenticationCeremony) + privateAPI.GET("/users/two-factor/recovery-status", userHandler.GetTwoFactorRecoveryStatus) + privateAPI.POST("/users/two-factor/passkeys/configure-recovery", userHandler.ConfigurePasskeyRecovery) privateAPI.GET("/users/two-factor/status", userHandler.GetTwoFactorStatus) privateAPI.POST("/users/two-factor/setup", userHandler.SetupTwoFactor) privateAPI.POST("/users/two-factor/enable", userHandler.EnableTwoFactor) @@ -494,7 +504,6 @@ func main() { privateAPI.GET("/collections/v2/diff", collectionHandler.GetDiffV2) privateAPI.GET("/collections/file", collectionHandler.GetFile) privateAPI.GET("/collections/sharees", collectionHandler.GetSharees) - privateAPI.DELETE("/collections/v2/:collectionID", collectionHandler.Trash) privateAPI.DELETE("/collections/v3/:collectionID", collectionHandler.TrashV3) privateAPI.POST("/collections/rename", collectionHandler.Rename) privateAPI.PUT("/collections/magic-metadata", collectionHandler.PrivateMagicMetadataUpdate) @@ -611,6 +620,7 @@ func main() { adminAPI.POST("/user/disable-2fa", adminHandler.DisableTwoFactor) adminAPI.POST("/user/disable-passkeys", adminHandler.RemovePasskeys) adminAPI.POST("/user/close-family", adminHandler.CloseFamily) + adminAPI.PUT("/user/change-email", adminHandler.ChangeEmail) adminAPI.DELETE("/user/delete", adminHandler.DeleteUser) adminAPI.POST("/user/recover", adminHandler.RecoverAccount) adminAPI.GET("/email-hash", adminHandler.GetEmailHash) @@ -676,7 +686,6 @@ func main() { publicAPI.GET("/offers/black-friday", offerHandler.GetBlackFridayOffers) setKnownAPIs(server.Routes()) - setupAndStartBackgroundJobs(objectCleanupController, replicationController3) setupAndStartCrons( userAuthRepo, publicCollectionRepo, twoFactorRepo, passkeysRepo, fileController, taskLockingRepo, emailNotificationCtrl, @@ -849,18 +858,18 @@ func setupAndStartCrons(userAuthRepo *repo.UserAuthRepository, publicCollectionR } }) - schedule(c, "@every 193s", func() { + schedule(c, "@every 2m", func() { fileController.CleanupDeletedFiles() }) schedule(c, "@every 101s", func() { embeddingCtrl.CleanupDeletedEmbeddings() }) - schedule(c, "@every 120s", func() { + schedule(c, "@every 10m", func() { trashController.DropFileMetadataCron() }) - schedule(c, "@every 2m", func() { + schedule(c, "@every 90s", func() { objectController.RemoveComplianceHolds() }) diff --git a/server/ente/admin.go b/server/ente/admin.go index 66ad27741..c56e48e5b 100644 --- a/server/ente/admin.go +++ b/server/ente/admin.go @@ -46,6 +46,11 @@ type UpdateSubscriptionRequest struct { Attributes SubscriptionAttributes `json:"attributes"` } +type ChangeEmailRequest struct { + UserID int64 `json:"userID" binding:"required"` + Email string `json:"email" binding:"required"` +} + type AddOnAction string const ( diff --git a/server/ente/billing.go b/server/ente/billing.go index 4d8d3401d..5a0ff08a8 100644 --- a/server/ente/billing.go +++ b/server/ente/billing.go @@ -176,11 +176,6 @@ type SubscriptionUpdateResponse struct { ClientSecret string `json:"clientSecret"` } -type StripeSubscriptionInfo struct { - PlanCountry string - AccountCountry StripeAccountCountry -} - type StripeEventLog struct { UserID int64 StripeSubscription stripe.Subscription diff --git a/server/ente/passkey.go b/server/ente/passkey.go index 0ed41965c..edeb99fd5 100644 --- a/server/ente/passkey.go +++ b/server/ente/passkey.go @@ -12,3 +12,19 @@ type Passkey struct { } var MaxPasskeys = 10 + +type SetPasskeyRecoveryRequest struct { + Secret string `json:"secret" binding:"required"` + // The UserSecretCipher has SkipSecret encrypted with the user's recoveryKey + // If the user sends the correct UserSecretCipher, we can be sure that the user has the recoveryKey, + // and we can allow the user to recover their MFA. + UserSecretCipher string `json:"userSecretCipher" binding:"required"` + UserSecretNonce string `json:"userSecretNonce" binding:"required"` +} + +type TwoFactorRecoveryStatus struct { + // AllowAdminReset is a boolean that determines if the admin can reset the user's MFA. + // If true, in the event that the user loses their MFA device, the admin can reset the user's MFA. + AllowAdminReset bool `json:"allowAdminReset" binding:"required"` + IsPasskeyRecoveryEnabled bool `json:"isPasskeyRecoveryEnabled" binding:"required"` +} diff --git a/server/ente/user.go b/server/ente/user.go index 5d80dc983..387d2627b 100644 --- a/server/ente/user.go +++ b/server/ente/user.go @@ -192,8 +192,9 @@ type TwoFactorRecoveryResponse struct { // TwoFactorRemovalRequest represents the the body of two factor removal request consist of decrypted two factor secret and sessionID type TwoFactorRemovalRequest struct { - Secret string `json:"secret"` - SessionID string `json:"sessionID"` + Secret string `json:"secret"` + SessionID string `json:"sessionID"` + TwoFactorType string `json:"twoFactorType"` } type ProfileData struct { diff --git a/server/migrations/80_two_factor_recovery.down.sql b/server/migrations/80_two_factor_recovery.down.sql new file mode 100644 index 000000000..7d95020b0 --- /dev/null +++ b/server/migrations/80_two_factor_recovery.down.sql @@ -0,0 +1,2 @@ +DROP TABLE IF EXISTS two_factor_recovery; +DROP TRIGGER IF EXISTS update_two_factor_recovery_updated_at ON two_factor_recovery; diff --git a/server/migrations/80_two_factor_recovery.up.sql b/server/migrations/80_two_factor_recovery.up.sql new file mode 100644 index 000000000..93b2b3676 --- /dev/null +++ b/server/migrations/80_two_factor_recovery.up.sql @@ -0,0 +1,18 @@ +CREATE TABLE IF NOT EXISTS two_factor_recovery ( + user_id bigint NOT NULL PRIMARY KEY, + -- if false, the support team team will not be able to reset the MFA for the user + enable_admin_mfa_reset boolean NOT NULL DEFAULT true, + server_passkey_secret_data bytea, + server_passkey_secret_nonce bytea, + user_passkey_secret_data text, + user_passkey_secret_nonce text, + created_at bigint NOT NULL DEFAULT now_utc_micro_seconds(), + updated_at bigint NOT NULL DEFAULT now_utc_micro_seconds() +); + +CREATE TRIGGER update_two_factor_recovery_updated_at + BEFORE UPDATE + ON two_factor_recovery + FOR EACH ROW + EXECUTE PROCEDURE + trigger_updated_at_microseconds_column(); diff --git a/server/pkg/api/admin.go b/server/pkg/api/admin.go index 90c9fd3e6..b153e19bb 100644 --- a/server/pkg/api/admin.go +++ b/server/pkg/api/admin.go @@ -306,6 +306,25 @@ func (h *AdminHandler) UpdateSubscription(c *gin.Context) { c.JSON(http.StatusOK, gin.H{}) } +func (h *AdminHandler) ChangeEmail(c *gin.Context) { + var r ente.ChangeEmailRequest + if err := c.ShouldBindJSON(&r); err != nil { + handler.Error(c, stacktrace.Propagate(ente.ErrBadRequest, "Bad request")) + return + } + adminID := auth.GetUserID(c.Request.Header) + go h.DiscordController.NotifyAdminAction( + fmt.Sprintf("Admin (%d) updating email for user: %d", adminID, r.UserID)) + err := h.UserController.UpdateEmail(c, r.UserID, r.Email) + if err != nil { + logrus.WithError(err).Error("Failed to update email") + handler.Error(c, stacktrace.Propagate(err, "")) + return + } + logrus.Info("Updated email") + c.JSON(http.StatusOK, gin.H{}) +} + func (h *AdminHandler) ReQueueItem(c *gin.Context) { var r ente.ReQueueItemRequest if err := c.ShouldBindJSON(&r); err != nil { diff --git a/server/pkg/api/collection.go b/server/pkg/api/collection.go index fff98e7f4..65c38bc61 100644 --- a/server/pkg/api/collection.go +++ b/server/pkg/api/collection.go @@ -360,18 +360,6 @@ func (h *CollectionHandler) GetSharees(c *gin.Context) { }) } -// Trash deletes a given collection and move file exclusive to the collection to trash -func (h *CollectionHandler) Trash(c *gin.Context) { - cID, _ := strconv.ParseInt(c.Param("collectionID"), 10, 64) - userID := auth.GetUserID(c.Request.Header) - err := h.Controller.Trash(c, userID, cID) - if err != nil { - handler.Error(c, stacktrace.Propagate(err, "")) - return - } - c.Status(http.StatusOK) -} - func (h *CollectionHandler) TrashV3(c *gin.Context) { var req ente.TrashCollectionV3Request if err := c.ShouldBindQuery(&req); err != nil { diff --git a/server/pkg/api/user.go b/server/pkg/api/user.go index 554f6654b..eca3804e5 100644 --- a/server/pkg/api/user.go +++ b/server/pkg/api/user.go @@ -244,6 +244,31 @@ func (h *UserHandler) GetTwoFactorStatus(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"status": status}) } +func (h *UserHandler) GetTwoFactorRecoveryStatus(c *gin.Context) { + res, err := h.UserController.GetTwoFactorRecoveryStatus(c) + if err != nil { + handler.Error(c, stacktrace.Propagate(err, "")) + return + } + c.JSON(http.StatusOK, res) +} + +// ConfigurePasskeyRecovery configures the passkey skip challenge for a user. In case the user does not +// have access to passkey, the user can bypass the passkey by providing the recovery key +func (h *UserHandler) ConfigurePasskeyRecovery(c *gin.Context) { + var request ente.SetPasskeyRecoveryRequest + if err := c.ShouldBindJSON(&request); err != nil { + handler.Error(c, stacktrace.Propagate(err, "")) + return + } + err := h.UserController.ConfigurePasskeyRecovery(c, &request) + if err != nil { + handler.Error(c, stacktrace.Propagate(err, "")) + return + } + c.JSON(http.StatusOK, gin.H{}) +} + // SetupTwoFactor generates a two factor secret and sends it to user to setup his authenticator app with func (h *UserHandler) SetupTwoFactor(c *gin.Context) { userID := auth.GetUserID(c.Request.Header) @@ -352,6 +377,16 @@ func (h *UserHandler) FinishPasskeyAuthenticationCeremony(c *gin.Context) { c.JSON(http.StatusOK, response) } +func (h *UserHandler) IsPasskeyRecoveryEnabled(c *gin.Context) { + userID := auth.GetUserID(c.Request.Header) + response, err := h.UserController.GetKeyAttributeAndToken(c, userID) + if err != nil { + handler.Error(c, stacktrace.Propagate(err, "")) + return + } + c.JSON(http.StatusOK, response) +} + // DisableTwoFactor disables the two factor authentication for a user func (h *UserHandler) DisableTwoFactor(c *gin.Context) { userID := auth.GetUserID(c.Request.Header) @@ -367,7 +402,14 @@ func (h *UserHandler) DisableTwoFactor(c *gin.Context) { // recoveryKeyEncryptedTwoFactorSecret for the user to decrypt it and make twoFactor removal api call func (h *UserHandler) RecoverTwoFactor(c *gin.Context) { sessionID := c.Query("sessionID") - response, err := h.UserController.RecoverTwoFactor(sessionID) + twoFactorType := c.Query("twoFactorType") + var response *ente.TwoFactorRecoveryResponse + var err error + if twoFactorType == "passkey" { + response, err = h.UserController.GetPasskeyRecoveryResponse(c, sessionID) + } else { + response, err = h.UserController.RecoverTwoFactor(sessionID) + } if err != nil { handler.Error(c, stacktrace.Propagate(err, "")) return @@ -383,7 +425,13 @@ func (h *UserHandler) RemoveTwoFactor(c *gin.Context) { handler.Error(c, stacktrace.Propagate(err, "")) return } - response, err := h.UserController.RemoveTwoFactor(c, request.SessionID, request.Secret) + var response *ente.TwoFactorAuthorizationResponse + var err error + if request.TwoFactorType == "passkey" { + response, err = h.UserController.SkipPasskeyVerification(c, &request) + } else { + response, err = h.UserController.RemoveTOTPTwoFactor(c, request.SessionID, request.Secret) + } if err != nil { handler.Error(c, stacktrace.Propagate(err, "")) return diff --git a/server/pkg/controller/billing.go b/server/pkg/controller/billing.go index 228a3344b..0d01aeeb9 100644 --- a/server/pkg/controller/billing.go +++ b/server/pkg/controller/billing.go @@ -85,30 +85,27 @@ func (c *BillingController) GetPlansV2(countryCode string, stripeAccountCountry // GetStripeAccountCountry returns the stripe account country the user's existing plan is from // if he doesn't have a stripe subscription then ente.DefaultStripeAccountCountry is returned func (c *BillingController) GetStripeAccountCountry(userID int64) (ente.StripeAccountCountry, error) { - stipeSubInfo, hasStripeSub, err := c.GetUserStripeSubscriptionInfo(userID) + subscription, err := c.BillingRepo.GetUserSubscription(userID) if err != nil { return "", stacktrace.Propagate(err, "") } - if hasStripeSub { - return stipeSubInfo.AccountCountry, nil - } else { + if subscription.PaymentProvider != ente.Stripe { //if user doesn't have a stripe subscription, return the default stripe account country return ente.DefaultStripeAccountCountry, nil + } else { + return subscription.Attributes.StripeAccountCountry, nil } } // GetUserPlans returns the active plans for a user func (c *BillingController) GetUserPlans(ctx *gin.Context, userID int64) ([]ente.BillingPlan, error) { - stripeSubInfo, hasStripeSub, err := c.GetUserStripeSubscriptionInfo(userID) + stripeAccountCountry, err := c.GetStripeAccountCountry(userID) if err != nil { - return []ente.BillingPlan{}, stacktrace.Propagate(err, "Failed to get user's subscription country and stripe account") - } - if hasStripeSub { - return c.GetPlansV2(stripeSubInfo.PlanCountry, stripeSubInfo.AccountCountry), nil - } else { - // user doesn't have a stipe subscription, so return the default account plans for the country the user is from - return c.GetPlansV2(network.GetClientCountry(ctx), ente.DefaultStripeAccountCountry), nil + return []ente.BillingPlan{}, stacktrace.Propagate(err, "Failed to get user's country stripe account") } + // always return the plans based on the user's country determined by the IP + return c.GetPlansV2(network.GetClientCountry(ctx), stripeAccountCountry), nil + } // GetSubscription returns the current subscription for a user if any @@ -208,23 +205,6 @@ func (c *BillingController) HasActiveSelfOrFamilySubscription(userID int64) erro return nil } -func (c *BillingController) GetUserStripeSubscriptionInfo(userID int64) (ente.StripeSubscriptionInfo, bool, error) { - s, err := c.BillingRepo.GetUserSubscription(userID) - if err != nil { - return ente.StripeSubscriptionInfo{}, false, stacktrace.Propagate(err, "") - } - // skipping country code extraction for non-stripe subscriptions - // as they have same product id across countries and hence can't be distinquished - if s.PaymentProvider != ente.Stripe { - return ente.StripeSubscriptionInfo{}, false, nil - } - _, countryCode, err := c.getPlanWithCountry(s) - if err != nil { - return ente.StripeSubscriptionInfo{}, false, stacktrace.Propagate(err, "") - } - return ente.StripeSubscriptionInfo{PlanCountry: countryCode, AccountCountry: s.Attributes.StripeAccountCountry}, true, nil -} - // VerifySubscription verifies and returns the verified subscription func (c *BillingController) VerifySubscription( userID int64, diff --git a/server/pkg/controller/collection.go b/server/pkg/controller/collection.go index 554f0d493..6e51072ad 100644 --- a/server/pkg/controller/collection.go +++ b/server/pkg/controller/collection.go @@ -472,17 +472,14 @@ func (c *CollectionController) GetDiffV2(ctx *gin.Context, cID int64, userID int "since_time": sinceTime, "req_id": requestid.Get(ctx), }) - reqContextLogger.Info("Start") _, err := c.AccessCtrl.GetCollection(ctx, &access.GetCollectionParams{ CollectionID: cID, ActorUserID: userID, }) - reqContextLogger.Info("Accessible") if err != nil { return nil, false, stacktrace.Propagate(err, "failed to verify access") } diff, hasMore, err := c.getDiff(cID, sinceTime, CollectionDiffLimit, reqContextLogger) - reqContextLogger.Info("Received diff") if err != nil { return nil, false, stacktrace.Propagate(err, "") } @@ -492,7 +489,6 @@ func (c *CollectionController) GetDiffV2(ctx *gin.Context, cID int64, userID int diff[idx].MagicMetadata = nil } } - reqContextLogger.Info("Function end") return diff, hasMore, nil } @@ -559,10 +555,8 @@ func (c *CollectionController) GetPublicDiff(ctx *gin.Context, sinceTime int64) // case 4: (sinceTime: v0, limit >=10): // The method will all 10 entries in the diff func (c *CollectionController) getDiff(cID int64, sinceTime int64, limit int, logger *log.Entry) ([]ente.File, bool, error) { - logger.Info("getDiff") // request for limit +1 files diffLimitPlusOne, err := c.CollectionRepo.GetDiff(cID, sinceTime, limit+1) - logger.Info("Got diff from repo") if err != nil { return nil, false, stacktrace.Propagate(err, "") } @@ -572,13 +566,24 @@ func (c *CollectionController) getDiff(cID int64, sinceTime int64, limit int, lo } lastFileVersion := diffLimitPlusOne[limit].UpdationTime filteredDiffs := c.removeFilesWithVersion(diffLimitPlusOne, lastFileVersion) - logger.Info("Removed files with out of bounds version") - if len(filteredDiffs) > 0 { // case 1 or case 3 + filteredDiffLen := len(filteredDiffs) + + if filteredDiffLen > 0 { // case 1 or case 3 + if filteredDiffLen < limit { + // logging case 1 + logger. + WithField("last_file_version", lastFileVersion). + WithField("filtered_diff_len", filteredDiffLen). + Info(fmt.Sprintf("less than limit (%d) files in diff", limit)) + } return filteredDiffs, true, nil } // case 2 diff, err := c.CollectionRepo.GetFilesWithVersion(cID, lastFileVersion) - logger.Info("Got diff of files with latest file version") + logger. + WithField("last_file_version", lastFileVersion). + WithField("count", len(diff)). + Info(fmt.Sprintf("more than limit (%d) files with same version", limit)) if err != nil { return nil, false, stacktrace.Propagate(err, "") } @@ -614,38 +619,6 @@ func (c *CollectionController) GetSharees(ctx *gin.Context, cID int64, userID in return sharees, nil } -// Trash deletes a given collection and files exclusive to the collection -func (c *CollectionController) Trash(ctx *gin.Context, userID int64, cID int64) error { - resp, err := c.AccessCtrl.GetCollection(ctx, &access.GetCollectionParams{ - CollectionID: cID, - ActorUserID: userID, - IncludeDeleted: true, - VerifyOwner: true, - }) - if err != nil { - return stacktrace.Propagate(err, "") - } - if !resp.Collection.AllowDelete() { - return stacktrace.Propagate(ente.ErrBadRequest, fmt.Sprintf("deleting albums of type %s is not allowed", resp.Collection.Type)) - } - if resp.Collection.IsDeleted { - log.WithFields(log.Fields{ - "c_id": cID, - "user_id": userID, - }).Warning("Collection is already deleted") - return nil - } - err = c.PublicCollectionCtrl.Disable(ctx, cID) - if err != nil { - return stacktrace.Propagate(err, "failed to disabled public share url") - } - err = c.CollectionRepo.ScheduleDelete(cID, true) - if err != nil { - return stacktrace.Propagate(err, "") - } - return nil -} - // TrashV3 deletes a given collection and based on user input (TrashCollectionV3Request.KeepFiles as FALSE) , it will move all files present in the underlying collection // to trash. func (c *CollectionController) TrashV3(ctx *gin.Context, req ente.TrashCollectionV3Request) error { @@ -694,7 +667,7 @@ func (c *CollectionController) TrashV3(ctx *gin.Context, req ente.TrashCollectio return stacktrace.Propagate(err, "failed to revoke cast token") } // Continue with current delete flow till. This disables sharing for this collection and then queue it up for deletion - err = c.CollectionRepo.ScheduleDelete(cID, false) + err = c.CollectionRepo.ScheduleDelete(cID) if err != nil { return stacktrace.Propagate(err, "") } diff --git a/server/pkg/controller/data_cleanup/controller.go b/server/pkg/controller/data_cleanup/controller.go index d3e2c61a6..05727ff0c 100644 --- a/server/pkg/controller/data_cleanup/controller.go +++ b/server/pkg/controller/data_cleanup/controller.go @@ -127,7 +127,7 @@ func (c *DeleteUserCleanupController) deleteCollections(ctx context.Context, ite for collectionID, isAlreadyDeleted := range collectionsMap { if !isAlreadyDeleted { // Delete all files in the collection - err = c.CollectionRepo.ScheduleDelete(collectionID, false) + err = c.CollectionRepo.ScheduleDelete(collectionID) if err != nil { return stacktrace.Propagate(err, fmt.Sprintf("error while deleting collection %d", collectionID)) } diff --git a/server/pkg/controller/file.go b/server/pkg/controller/file.go index 1323ee4c0..12d173e25 100644 --- a/server/pkg/controller/file.go +++ b/server/pkg/controller/file.go @@ -5,6 +5,7 @@ import ( "database/sql" "encoding/json" "errors" + "fmt" "runtime/debug" "strconv" "strings" @@ -54,6 +55,9 @@ const MaxFileSize = int64(1024 * 1024 * 1024 * 5) // MaxUploadURLsLimit indicates the max number of upload urls which can be request in one go const MaxUploadURLsLimit = 50 +const ( + DeletedObjectQueueLock = "deleted_objects_queue_lock" +) // Create adds an entry for a file in the respective tables func (c *FileController) Create(ctx context.Context, userID int64, file ente.File, userAgent string, app ente.App) (ente.File, error) { @@ -600,7 +604,16 @@ func (c *FileController) CleanupDeletedFiles() { defer func() { c.cleanupCronRunning = false }() - items, err := c.QueueRepo.GetItemsReadyForDeletion(repo.DeleteObjectQueue, 200) + + lockStatus := c.LockController.TryLock(DeletedObjectQueueLock, time.MicrosecondsAfterHours(2)) + if !lockStatus { + log.Warning(fmt.Sprintf("Failed to acquire lock %s", DeletedObjectQueueLock)) + return + } + defer func() { + c.LockController.ReleaseLock(DeletedObjectQueueLock) + }() + items, err := c.QueueRepo.GetItemsReadyForDeletion(repo.DeleteObjectQueue, 2000) if err != nil { log.WithError(err).Error("Failed to fetch items from queue") return diff --git a/server/pkg/controller/lock/lock.go b/server/pkg/controller/lock/lock.go index ea7d8bf73..3746ff679 100644 --- a/server/pkg/controller/lock/lock.go +++ b/server/pkg/controller/lock/lock.go @@ -56,3 +56,11 @@ func (c *LockController) ReleaseLock(lockID string) { log.Errorf("Error while releasing lock %v: %s", lockID, err) } } + +func (c *LockController) ReleaseHostLock() { + count, err := c.TaskLockingRepo.ReleaseLocksBy(c.HostName) + if err != nil { + log.Errorf("Error while releasing host lock: %s", err) + } + log.Infof("Released %d locks held by %s", *count, c.HostName) +} diff --git a/server/pkg/controller/object.go b/server/pkg/controller/object.go index 8f197fe46..f270a0f5e 100644 --- a/server/pkg/controller/object.go +++ b/server/pkg/controller/object.go @@ -1,6 +1,7 @@ package controller import ( + "fmt" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/s3" "github.com/ente-io/museum/pkg/controller/lock" @@ -30,6 +31,10 @@ type ObjectController struct { complianceCronRunning bool } +const ( + RemoveComplianceHoldsLock = "remove_compliance_holds_lock" +) + // RemoveComplianceHolds removes the Wasabi compliance hold from objects in // Wasabi for files which have been deleted. // @@ -41,7 +46,6 @@ func (c *ObjectController) RemoveComplianceHolds() { // Wasabi compliance is currently disabled in config, nothing to do. return } - if c.complianceCronRunning { log.Info("Skipping RemoveComplianceHolds cron run as another instance is still running") return @@ -51,7 +55,16 @@ func (c *ObjectController) RemoveComplianceHolds() { c.complianceCronRunning = false }() - items, err := c.QueueRepo.GetItemsReadyForDeletion(repo.RemoveComplianceHoldQueue, 200) + lockStatus := c.LockController.TryLock(RemoveComplianceHoldsLock, time.MicrosecondsAfterHours(2)) + if !lockStatus { + log.Warning(fmt.Sprintf("Failed to acquire lock %s", RemoveComplianceHoldsLock)) + return + } + defer func() { + c.LockController.ReleaseLock(RemoveComplianceHoldsLock) + }() + + items, err := c.QueueRepo.GetItemsReadyForDeletion(repo.RemoveComplianceHoldQueue, 1500) if err != nil { log.WithError(err).Error("Failed to fetch items from queue") return @@ -62,7 +75,7 @@ func (c *ObjectController) RemoveComplianceHolds() { c.removeComplianceHold(i) } - log.Infof("Revmoed compliance holds on %d deleted files", len(items)) + log.Infof("Removed compliance holds on %d deleted files", len(items)) } func (c *ObjectController) removeComplianceHold(qItem repo.QueueItem) { diff --git a/server/pkg/controller/trash.go b/server/pkg/controller/trash.go index 0b3d242a9..7fde973e4 100644 --- a/server/pkg/controller/trash.go +++ b/server/pkg/controller/trash.go @@ -121,17 +121,6 @@ func (t *TrashController) CleanupTrashedCollections() { t.collectionTrashRunning = false }() - // process delete collection request for DELETE V2 - items, err := t.QueueRepo.GetItemsReadyForDeletion(repo.TrashCollectionQueue, 100) - if err != nil { - log.Error("Could not fetch from collection trash queue", err) - return - } - item_processed_count += len(items) - for _, item := range items { - t.trashCollection(item, repo.TrashCollectionQueue, true, ctxLogger) - } - // process delete collection request for DELETE V3 itemsV3, err2 := t.QueueRepo.GetItemsReadyForDeletion(repo.TrashCollectionQueueV3, 100) if err2 != nil { @@ -140,7 +129,7 @@ func (t *TrashController) CleanupTrashedCollections() { } item_processed_count += len(itemsV3) for _, item := range itemsV3 { - t.trashCollection(item, repo.TrashCollectionQueueV3, false, ctxLogger) + t.trashCollection(item, repo.TrashCollectionQueueV3, ctxLogger) } } @@ -221,7 +210,7 @@ func (t *TrashController) removeFilesWithVersion(trashedFiles []ente.Trash, vers return trashedFiles[0 : i+1] } -func (t *TrashController) trashCollection(item repo.QueueItem, queueName string, trashOnlyExclusiveFiles bool, logger *log.Entry) { +func (t *TrashController) trashCollection(item repo.QueueItem, queueName string, logger *log.Entry) { cID, _ := strconv.ParseInt(item.Item, 10, 64) collection, err := t.CollectionRepo.Get(cID) if err != nil { @@ -252,11 +241,7 @@ func (t *TrashController) trashCollection(item repo.QueueItem, queueName string, } }() ctxLogger.Info("start trashing collection") - if trashOnlyExclusiveFiles { - err = t.CollectionRepo.TrashV2(cID, collection.Owner.ID) - } else { - err = t.CollectionRepo.TrashV3(context.Background(), cID) - } + err = t.CollectionRepo.TrashV3(context.Background(), cID) if err != nil { ctxLogger.WithError(err).Error("failed to trash collection") return diff --git a/server/pkg/controller/user/passkey.go b/server/pkg/controller/user/passkey.go new file mode 100644 index 000000000..cb7294638 --- /dev/null +++ b/server/pkg/controller/user/passkey.go @@ -0,0 +1,61 @@ +package user + +import ( + "github.com/ente-io/museum/ente" + "github.com/ente-io/museum/pkg/utils/auth" + "github.com/ente-io/stacktrace" + "github.com/gin-gonic/gin" +) + +// GetTwoFactorRecoveryStatus returns a user's passkey reset status +func (c *UserController) GetTwoFactorRecoveryStatus(ctx *gin.Context) (*ente.TwoFactorRecoveryStatus, error) { + userID := auth.GetUserID(ctx.Request.Header) + return c.TwoFactorRecoveryRepo.GetStatus(userID) +} + +func (c *UserController) ConfigurePasskeyRecovery(ctx *gin.Context, req *ente.SetPasskeyRecoveryRequest) error { + userID := auth.GetUserID(ctx.Request.Header) + return c.TwoFactorRecoveryRepo.SetPasskeyRecovery(ctx, userID, req) +} + +func (c *UserController) GetPasskeyRecoveryResponse(ctx *gin.Context, passKeySessionID string) (*ente.TwoFactorRecoveryResponse, error) { + userID, err := c.PasskeyRepo.GetUserIDWithPasskeyTwoFactorSession(passKeySessionID) + if err != nil { + return nil, err + } + recoveryStatus, err := c.TwoFactorRecoveryRepo.GetStatus(userID) + if err != nil { + return nil, err + } + if !recoveryStatus.IsPasskeyRecoveryEnabled { + return nil, ente.NewBadRequestWithMessage("Passkey reset is not configured") + } + + result, err := c.TwoFactorRecoveryRepo.GetPasskeyRecoveryData(ctx, userID) + if err != nil { + return nil, err + } + if result == nil { + return nil, ente.NewBadRequestWithMessage("Passkey reset is not configured") + } + return result, nil +} + +func (c *UserController) SkipPasskeyVerification(context *gin.Context, req *ente.TwoFactorRemovalRequest) (*ente.TwoFactorAuthorizationResponse, error) { + userID, err := c.PasskeyRepo.GetUserIDWithPasskeyTwoFactorSession(req.SessionID) + if err != nil { + return nil, stacktrace.Propagate(err, "") + } + exists, err := c.TwoFactorRecoveryRepo.ValidatePasskeyRecoverySecret(userID, req.Secret) + if err != nil { + return nil, stacktrace.Propagate(err, "") + } + if !exists { + return nil, stacktrace.Propagate(ente.ErrPermissionDenied, "") + } + response, err := c.GetKeyAttributeAndToken(context, userID) + if err != nil { + return nil, stacktrace.Propagate(err, "") + } + return &response, nil +} diff --git a/server/pkg/controller/user/twofactor.go b/server/pkg/controller/user/twofactor.go index ac5473b06..d788b66fd 100644 --- a/server/pkg/controller/user/twofactor.go +++ b/server/pkg/controller/user/twofactor.go @@ -131,47 +131,47 @@ func (c *UserController) DisableTwoFactor(userID int64) error { // RecoverTwoFactor handles the two factor recovery request by sending the // recoveryKeyEncryptedTwoFactorSecret for the user to decrypt it and make twoFactor removal api call -func (c *UserController) RecoverTwoFactor(sessionID string) (ente.TwoFactorRecoveryResponse, error) { +func (c *UserController) RecoverTwoFactor(sessionID string) (*ente.TwoFactorRecoveryResponse, error) { userID, err := c.TwoFactorRepo.GetUserIDWithTwoFactorSession(sessionID) if err != nil { - return ente.TwoFactorRecoveryResponse{}, stacktrace.Propagate(err, "") + return nil, stacktrace.Propagate(err, "") } response, err := c.TwoFactorRepo.GetRecoveryKeyEncryptedTwoFactorSecret(userID) if err != nil { - return ente.TwoFactorRecoveryResponse{}, stacktrace.Propagate(err, "") + return nil, stacktrace.Propagate(err, "") } - return response, nil + return &response, nil } -// RemoveTwoFactor handles two factor deactivation request if user lost his device +// RemoveTOTPTwoFactor handles two factor deactivation request if user lost his device // by authenticating him using his twoFactorsessionToken and twoFactor secret -func (c *UserController) RemoveTwoFactor(context *gin.Context, sessionID string, secret string) (ente.TwoFactorAuthorizationResponse, error) { +func (c *UserController) RemoveTOTPTwoFactor(context *gin.Context, sessionID string, secret string) (*ente.TwoFactorAuthorizationResponse, error) { userID, err := c.TwoFactorRepo.GetUserIDWithTwoFactorSession(sessionID) if err != nil { - return ente.TwoFactorAuthorizationResponse{}, stacktrace.Propagate(err, "") + return nil, stacktrace.Propagate(err, "") } secretHash, err := crypto.GetHash(secret, c.HashingKey) if err != nil { - return ente.TwoFactorAuthorizationResponse{}, stacktrace.Propagate(err, "") + return nil, stacktrace.Propagate(err, "") } exists, err := c.TwoFactorRepo.VerifyTwoFactorSecret(userID, secretHash) if err != nil { - return ente.TwoFactorAuthorizationResponse{}, stacktrace.Propagate(err, "") + return nil, stacktrace.Propagate(err, "") } if !exists { - return ente.TwoFactorAuthorizationResponse{}, stacktrace.Propagate(ente.ErrPermissionDenied, "") + return nil, stacktrace.Propagate(ente.ErrPermissionDenied, "") } err = c.TwoFactorRepo.UpdateTwoFactorStatus(userID, false) if err != nil { - return ente.TwoFactorAuthorizationResponse{}, stacktrace.Propagate(err, "") + return nil, stacktrace.Propagate(err, "") } response, err := c.GetKeyAttributeAndToken(context, userID) if err != nil { - return ente.TwoFactorAuthorizationResponse{}, stacktrace.Propagate(err, "") + return nil, stacktrace.Propagate(err, "") } - return response, nil + return &response, nil } func (c *UserController) GetKeyAttributeAndToken(context *gin.Context, userID int64) (ente.TwoFactorAuthorizationResponse, error) { diff --git a/server/pkg/controller/user/user.go b/server/pkg/controller/user/user.go index 4be02b24f..afba09058 100644 --- a/server/pkg/controller/user/user.go +++ b/server/pkg/controller/user/user.go @@ -3,6 +3,7 @@ package user import ( "errors" "fmt" + "github.com/ente-io/museum/pkg/repo/two_factor_recovery" "strings" cache2 "github.com/ente-io/museum/ente/cache" @@ -30,6 +31,7 @@ import ( // UserController exposes request handlers for all user related requests type UserController struct { UserRepo *repo.UserRepository + TwoFactorRecoveryRepo *two_factor_recovery.Repository UsageRepo *repo.UsageRepository UserAuthRepo *repo.UserAuthRepository TwoFactorRepo *repo.TwoFactorRepository @@ -99,6 +101,7 @@ func NewUserController( usageRepo *repo.UsageRepository, userAuthRepo *repo.UserAuthRepository, twoFactorRepo *repo.TwoFactorRepository, + twoFactorRecoveryRepo *two_factor_recovery.Repository, passkeyRepo *passkey.Repository, storageBonusRepo *storageBonusRepo.Repository, fileRepo *repo.FileRepository, @@ -121,6 +124,7 @@ func NewUserController( return &UserController{ UserRepo: userRepo, UsageRepo: usageRepo, + TwoFactorRecoveryRepo: twoFactorRecoveryRepo, UserAuthRepo: userAuthRepo, StorageBonusRepo: storageBonusRepo, TwoFactorRepo: twoFactorRepo, diff --git a/server/pkg/controller/user/userauth.go b/server/pkg/controller/user/userauth.go index bf092cb14..bbc9942de 100644 --- a/server/pkg/controller/user/userauth.go +++ b/server/pkg/controller/user/userauth.go @@ -197,7 +197,13 @@ func (c *UserController) ChangeEmail(ctx *gin.Context, request ente.EmailVerific if err != nil { return stacktrace.Propagate(err, "") } - _, err = c.UserRepo.GetUserIDWithEmail(email) + + return c.UpdateEmail(ctx, auth.GetUserID(ctx.Request.Header), email) +} + +// UpdateEmail updates the email address of the user with the provided userID +func (c *UserController) UpdateEmail(ctx *gin.Context, userID int64, email string) error { + _, err := c.UserRepo.GetUserIDWithEmail(email) if err == nil { // email already owned by a user return stacktrace.Propagate(ente.ErrPermissionDenied, "") @@ -206,7 +212,6 @@ func (c *UserController) ChangeEmail(ctx *gin.Context, request ente.EmailVerific // unknown error, rethrow return stacktrace.Propagate(err, "") } - userID := auth.GetUserID(ctx.Request.Header) user, err := c.UserRepo.Get(userID) if err != nil { return stacktrace.Propagate(err, "") diff --git a/server/pkg/repo/collection.go b/server/pkg/repo/collection.go index 952c03aa6..38700dec4 100644 --- a/server/pkg/repo/collection.go +++ b/server/pkg/repo/collection.go @@ -775,27 +775,6 @@ func (repo *CollectionRepository) GetSharees(cID int64) ([]ente.CollectionUser, return users, nil } -// getCollectionExclusiveFiles return a list of filesIDs that are exclusive to the collection -func (repo *CollectionRepository) getCollectionExclusiveFiles(collectionID int64, collectionOwnerID int64) ([]int64, error) { - rows, err := repo.DB.Query(` - SELECT file_id - FROM collection_files - WHERE is_deleted=false - AND file_id IN ( - SELECT file_id - FROM collection_files - WHERE is_deleted=false - AND collection_id =$1 - ) - AND collection_id IN (SELECT collection_id from collections where owner_id = $2) - GROUP BY file_id - HAVING COUNT(file_id) = 1`, collectionID, collectionOwnerID) - if err != nil { - return make([]int64, 0), stacktrace.Propagate(err, "") - } - return convertRowsToFileId(rows) -} - // GetCollectionFileIDs return list of fileIDs are currently present in the given collection // and fileIDs are owned by the collection owner func (repo *CollectionRepository) GetCollectionFileIDs(collectionID int64, collectionOwnerID int64) ([]int64, error) { @@ -824,41 +803,6 @@ func convertRowsToFileId(rows *sql.Rows) ([]int64, error) { return fileIDs, nil } -// TrashV2 removes an entry in the database for the collection referred to by `collectionID` and move all files -// which are exclusive to this collection to trash -// Deprecated. Please use TrashV3 -func (repo *CollectionRepository) TrashV2(collectionID int64, userID int64) error { - ctx := context.Background() - tx, err := repo.DB.BeginTx(ctx, nil) - if err != nil { - return stacktrace.Propagate(err, "") - } - fileIDs, err := repo.getCollectionExclusiveFiles(collectionID, userID) - if err != nil { - tx.Rollback() - return stacktrace.Propagate(err, "") - } - items := make([]ente.TrashItemRequest, 0) - for _, fileID := range fileIDs { - items = append(items, ente.TrashItemRequest{ - FileID: fileID, - CollectionID: collectionID, - }) - } - _, err = tx.ExecContext(ctx, `UPDATE collection_files SET is_deleted = true WHERE collection_id = $1`, collectionID) - if err != nil { - tx.Rollback() - return stacktrace.Propagate(err, "") - } - err = repo.TrashRepo.InsertItems(ctx, tx, userID, items) - - if err != nil { - tx.Rollback() - return stacktrace.Propagate(err, "") - } - return tx.Commit() -} - // TrashV3 move the files belonging to the collection owner to the trash func (repo *CollectionRepository) TrashV3(ctx context.Context, collectionID int64) error { log := logrus.WithFields(logrus.Fields{ @@ -949,11 +893,8 @@ func (repo *CollectionRepository) removeAllFilesAddedByOthers(collectionID int64 // ScheduleDelete marks the collection as deleted and queue up an operation to // move the collection files to user's trash. -// The deleteOnlyExcluiveFiles flag is true for v2 collection delete and is false for v3 version. // See [Collection Delete Versions] for more details -func (repo *CollectionRepository) ScheduleDelete( - collectionID int64, - deleteOnlyExcluiveFiles bool) error { +func (repo *CollectionRepository) ScheduleDelete(collectionID int64) error { updationTime := time.Microseconds() ctx := context.Background() tx, err := repo.DB.BeginTx(ctx, nil) @@ -974,12 +915,7 @@ func (repo *CollectionRepository) ScheduleDelete( tx.Rollback() return stacktrace.Propagate(err, "") } - if deleteOnlyExcluiveFiles { - err = repo.QueueRepo.AddItems(ctx, tx, TrashCollectionQueue, []string{strconv.FormatInt(collectionID, 10)}) - } else { - err = repo.QueueRepo.AddItems(ctx, tx, TrashCollectionQueueV3, []string{strconv.FormatInt(collectionID, 10)}) - } - + err = repo.QueueRepo.AddItems(ctx, tx, TrashCollectionQueueV3, []string{strconv.FormatInt(collectionID, 10)}) if err != nil { tx.Rollback() return stacktrace.Propagate(err, "") diff --git a/server/pkg/repo/queue.go b/server/pkg/repo/queue.go index 1f706aa88..49544dbc8 100644 --- a/server/pkg/repo/queue.go +++ b/server/pkg/repo/queue.go @@ -23,17 +23,17 @@ var itemDeletionDelayInMinMap = map[string]int64{ DropFileEncMedataQueue: -1 * 24 * 60, // -ve value to ensure attributes are immediately removed DeleteObjectQueue: 45 * 24 * 60, // 45 days in minutes DeleteEmbeddingsQueue: -1 * 24 * 60, // -ve value to ensure embeddings are immediately removed - TrashCollectionQueue: -1 * 24 * 60, // -ve value to ensure collections are immediately marked as trashed TrashCollectionQueueV3: -1 * 24 * 60, // -ve value to ensure collections are immediately marked as trashed TrashEmptyQueue: -1 * 24 * 60, // -ve value to ensure empty trash request are processed in next cron run RemoveComplianceHoldQueue: -1 * 24 * 60, // -ve value to ensure compliance hold is removed in next cron run } const ( - DropFileEncMedataQueue string = "dropFileEncMetata" - DeleteObjectQueue string = "deleteObject" - DeleteEmbeddingsQueue string = "deleteEmbedding" - OutdatedObjectsQueue string = "outdatedObject" + DropFileEncMedataQueue string = "dropFileEncMetata" + DeleteObjectQueue string = "deleteObject" + DeleteEmbeddingsQueue string = "deleteEmbedding" + OutdatedObjectsQueue string = "outdatedObject" + // Deprecated: Keeping it till we clean up items from the queue DB. TrashCollectionQueue string = "trashCollection" TrashCollectionQueueV3 string = "trashCollectionV3" TrashEmptyQueue string = "trashEmpty" diff --git a/server/pkg/repo/tasklock.go b/server/pkg/repo/tasklock.go index 3aa593e13..ce6cca8f4 100644 --- a/server/pkg/repo/tasklock.go +++ b/server/pkg/repo/tasklock.go @@ -71,6 +71,18 @@ func (repo *TaskLockRepository) ReleaseLock(name string) error { return stacktrace.Propagate(err, "") } +func (repo *TaskLockRepository) ReleaseLocksBy(lockedBy string) (*int64, error) { + result, err := repo.DB.Exec(`DELETE FROM task_lock WHERE locked_by = $1`, lockedBy) + if err != nil { + return nil, stacktrace.Propagate(err, "") + } + rowsAffected, err := result.RowsAffected() + if err != nil { + return nil, stacktrace.Propagate(err, "") + } + return &rowsAffected, nil +} + func (repo *TaskLockRepository) CleanupExpiredLocks() error { result, err := repo.DB.Exec(`DELETE FROM task_lock WHERE lock_until < $1`, time.Microseconds()) if err != nil { diff --git a/server/pkg/repo/two_factor_recovery/repository.go b/server/pkg/repo/two_factor_recovery/repository.go new file mode 100644 index 000000000..4bc63fdd5 --- /dev/null +++ b/server/pkg/repo/two_factor_recovery/repository.go @@ -0,0 +1,80 @@ +package two_factor_recovery + +import ( + "context" + "database/sql" + + "github.com/ente-io/museum/ente" + "github.com/ente-io/museum/pkg/utils/crypto" + "github.com/ente-io/stacktrace" + "github.com/sirupsen/logrus" +) + +type Repository struct { + Db *sql.DB + SecretEncryptionKey []byte +} + +// GetStatus returns `ente.TwoFactorRecoveryStatus` for a user +func (r *Repository) GetStatus(userID int64) (*ente.TwoFactorRecoveryStatus, error) { + var isAdminResetEnabled bool + var resetKey []byte + row := r.Db.QueryRow(`SELECT enable_admin_mfa_reset, server_passkey_secret_data FROM two_factor_recovery WHERE user_id = $1`, userID) + err := row.Scan(&isAdminResetEnabled, &resetKey) + if err != nil { + if err == sql.ErrNoRows { + // by default, admin + return &ente.TwoFactorRecoveryStatus{ + AllowAdminReset: true, + IsPasskeyRecoveryEnabled: false, + }, nil + } + return nil, err + } + return &ente.TwoFactorRecoveryStatus{AllowAdminReset: isAdminResetEnabled, IsPasskeyRecoveryEnabled: len(resetKey) > 0}, nil +} + +func (r *Repository) SetPasskeyRecovery(ctx context.Context, userID int64, req *ente.SetPasskeyRecoveryRequest) error { + serveEncPasskey, encErr := crypto.Encrypt(req.Secret, r.SecretEncryptionKey) + if encErr != nil { + return stacktrace.Propagate(encErr, "failed to encrypt passkey secret") + } + _, err := r.Db.ExecContext(ctx, `INSERT INTO two_factor_recovery + (user_id, server_passkey_secret_data, server_passkey_secret_nonce, user_passkey_secret_data, user_passkey_secret_nonce) + VALUES ($1, $2, $3, $4, $5) ON CONFLICT (user_id) + DO UPDATE SET server_passkey_secret_data = $2, server_passkey_secret_nonce = $3, user_passkey_secret_data = $4, user_passkey_secret_nonce = $5 + WHERE two_factor_recovery.user_passkey_secret_data IS NULL AND two_factor_recovery.server_passkey_secret_data IS NULL`, + userID, serveEncPasskey.Cipher, serveEncPasskey.Nonce, req.UserSecretCipher, req.UserSecretNonce) + return err +} + +func (r *Repository) GetPasskeyRecoveryData(ctx context.Context, userID int64) (*ente.TwoFactorRecoveryResponse, error) { + var result ente.TwoFactorRecoveryResponse + err := r.Db.QueryRowContext(ctx, "SELECT user_passkey_secret_data, user_passkey_secret_nonce FROM two_factor_recovery WHERE user_id= $1", userID).Scan(&result.EncryptedSecret, &result.SecretDecryptionNonce) + if err != nil { + return nil, err + } + return &result, nil +} + +// ValidatePasskeyRecoverySecret checks if the passkey skip secret is valid for a user +func (r *Repository) ValidatePasskeyRecoverySecret(userID int64, secret string) (bool, error) { + // get server_passkey_secret_data and server_passkey_secret_nonce for given user id + var severSecreteData, serverSecretNonce []byte + row := r.Db.QueryRow(`SELECT server_passkey_secret_data, server_passkey_secret_nonce FROM two_factor_recovery WHERE user_id = $1`, userID) + err := row.Scan(&severSecreteData, &serverSecretNonce) + if err != nil { + return false, stacktrace.Propagate(err, "") + } + // decrypt server_passkey_secret_data + serverSkipSecretKey, decErr := crypto.Decrypt(severSecreteData, r.SecretEncryptionKey, serverSecretNonce) + // serverSkipSecretKey, decErr := crypto.Decrypt(severSecreteData,serverSecretNonce, r.SecretEncryptionKey ) + if decErr != nil { + return false, stacktrace.Propagate(decErr, "failed to decrypt passkey reset key") + } + if secret != serverSkipSecretKey { + logrus.Warn("invalid passkey skip secret") + return false, nil + } + return true, nil +} diff --git a/web/.gitignore b/web/.gitignore index adcd3d880..f07d93259 100644 --- a/web/.gitignore +++ b/web/.gitignore @@ -1,14 +1,17 @@ # Node node_modules/ +# macOS +.DS_Store + +# Editors +.vscode/ + +# Local env files +.env +.env.*.local + # Next.js .next/ out/ next-env.d.ts - -# macOS -.DS_Store - -# Local env files -.env -.env.*.local diff --git a/web/README.md b/web/README.md index fe5d419db..36eb1fb25 100644 --- a/web/README.md +++ b/web/README.md @@ -32,8 +32,8 @@ 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, see -[docs/new](docs/new.md). +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/sentry.client.config.ts b/web/apps/accounts/sentry.client.config.ts deleted file mode 100644 index c43273663..000000000 --- a/web/apps/accounts/sentry.client.config.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { initSentry } from "@ente/shared/sentry/config/sentry.config.base"; - -initSentry("https://0f7214c7feb9b1dd2fed5db09b42fa1b@sentry.ente.io/5"); diff --git a/web/apps/accounts/sentry.edge.config.ts b/web/apps/accounts/sentry.edge.config.ts deleted file mode 100644 index e69de29bb..000000000 diff --git a/web/apps/accounts/sentry.properties b/web/apps/accounts/sentry.properties deleted file mode 100644 index 27c3a286f..000000000 --- a/web/apps/accounts/sentry.properties +++ /dev/null @@ -1,6 +0,0 @@ -# This file is used by the SentryWebpackPlugin to upload sourcemaps when the -# SENTRY_AUTH_TOKEN environment variable is defined. - -defaults.url = https://sentry.ente.io/ -defaults.org = ente -defaults.project = web-photos diff --git a/web/apps/accounts/sentry.server.config.ts b/web/apps/accounts/sentry.server.config.ts deleted file mode 100644 index e69de29bb..000000000 diff --git a/web/apps/accounts/src/pages/passkeys/flow/index.tsx b/web/apps/accounts/src/pages/passkeys/flow/index.tsx index 61c56a97c..517777b9c 100644 --- a/web/apps/accounts/src/pages/passkeys/flow/index.tsx +++ b/web/apps/accounts/src/pages/passkeys/flow/index.tsx @@ -40,6 +40,7 @@ const PasskeysFlow = () => { redirect !== "" && !( redirectURL.host.endsWith(".ente.io") || + redirectURL.host.endsWith(".ente.sh") || redirectURL.host.endsWith("bada-frame.pages.dev") ) && redirectURL.protocol !== "ente:" && diff --git a/web/apps/auth/next.config.js b/web/apps/auth/next.config.js index eea88bf93..81a64d7dd 100644 --- a/web/apps/auth/next.config.js +++ b/web/apps/auth/next.config.js @@ -1,3 +1 @@ -const nextConfigBase = require("@/next/next.config.base.js"); - -module.exports = nextConfigBase; +module.exports = require("@/next/next.config.base.js"); diff --git a/web/apps/auth/sentry.client.config.ts b/web/apps/auth/sentry.client.config.ts deleted file mode 100644 index 373718e8e..000000000 --- a/web/apps/auth/sentry.client.config.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { initSentry } from "@ente/shared/sentry/config/sentry.config.base"; - -initSentry("https://5d344112b570b1a368b6f5c1d0bb798b@sentry.ente.io/8"); diff --git a/web/apps/auth/sentry.edge.config.ts b/web/apps/auth/sentry.edge.config.ts deleted file mode 100644 index e69de29bb..000000000 diff --git a/web/apps/auth/sentry.properties b/web/apps/auth/sentry.properties deleted file mode 100644 index e9b0cad16..000000000 --- a/web/apps/auth/sentry.properties +++ /dev/null @@ -1,6 +0,0 @@ -# This file is used by the SentryWebpackPlugin to upload sourcemaps when the -# SENTRY_AUTH_TOKEN environment variable is defined. - -defaults.url = https://sentry.ente.io/ -defaults.org = ente -defaults.project = web-auth diff --git a/web/apps/auth/sentry.server.config.ts b/web/apps/auth/sentry.server.config.ts deleted file mode 100644 index e69de29bb..000000000 diff --git a/web/apps/auth/src/pages/_app.tsx b/web/apps/auth/src/pages/_app.tsx index 156f2e455..c06531ab4 100644 --- a/web/apps/auth/src/pages/_app.tsx +++ b/web/apps/auth/src/pages/_app.tsx @@ -93,16 +93,6 @@ export default function App(props: EnteAppProps) { const setUserOnline = () => setOffline(false); const setUserOffline = () => setOffline(true); - useEffect(() => { - if (isI18nReady) { - console.log( - `%c${t("CONSOLE_WARNING_STOP")}`, - "color: red; font-size: 52px;", - ); - console.log(`%c${t("CONSOLE_WARNING_DESC")}`, "font-size: 20px;"); - } - }, [isI18nReady]); - useEffect(() => { router.events.on("routeChangeStart", (url: string) => { const newPathname = url.split("?")[0] as PAGES; diff --git a/web/apps/auth/src/pages/_error.tsx b/web/apps/auth/src/pages/_error.tsx deleted file mode 100644 index bf1bb89be..000000000 --- a/web/apps/auth/src/pages/_error.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { APPS } from "@ente/shared/apps/constants"; -import ErrorPage from "@ente/shared/next/pages/_error"; -import { useRouter } from "next/router"; -import { AppContext } from "pages/_app"; -import { useContext } from "react"; - -export default function Error() { - const appContext = useContext(AppContext); - const router = useRouter(); - return ( - - ); -} diff --git a/web/apps/auth/src/pages/passkeys/finish/index.tsx b/web/apps/auth/src/pages/passkeys/finish/index.tsx new file mode 100644 index 000000000..289f351de --- /dev/null +++ b/web/apps/auth/src/pages/passkeys/finish/index.tsx @@ -0,0 +1,11 @@ +import PasskeysFinishPage from "@ente/accounts/pages/passkeys/finish"; + +const PasskeysFinish = () => { + return ( + <> + + + ); +}; + +export default PasskeysFinish; diff --git a/web/apps/cast/next.config.js b/web/apps/cast/next.config.js index eea88bf93..81a64d7dd 100644 --- a/web/apps/cast/next.config.js +++ b/web/apps/cast/next.config.js @@ -1,3 +1 @@ -const nextConfigBase = require("@/next/next.config.base.js"); - -module.exports = nextConfigBase; +module.exports = require("@/next/next.config.base.js"); diff --git a/web/apps/cast/sentry.client.config.ts b/web/apps/cast/sentry.client.config.ts deleted file mode 100644 index c43273663..000000000 --- a/web/apps/cast/sentry.client.config.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { initSentry } from "@ente/shared/sentry/config/sentry.config.base"; - -initSentry("https://0f7214c7feb9b1dd2fed5db09b42fa1b@sentry.ente.io/5"); diff --git a/web/apps/cast/sentry.edge.config.ts b/web/apps/cast/sentry.edge.config.ts deleted file mode 100644 index e69de29bb..000000000 diff --git a/web/apps/cast/sentry.properties b/web/apps/cast/sentry.properties deleted file mode 100644 index 27c3a286f..000000000 --- a/web/apps/cast/sentry.properties +++ /dev/null @@ -1,6 +0,0 @@ -# This file is used by the SentryWebpackPlugin to upload sourcemaps when the -# SENTRY_AUTH_TOKEN environment variable is defined. - -defaults.url = https://sentry.ente.io/ -defaults.org = ente -defaults.project = web-photos diff --git a/web/apps/cast/sentry.server.config.ts b/web/apps/cast/sentry.server.config.ts deleted file mode 100644 index e69de29bb..000000000 diff --git a/web/apps/cast/src/services/InMemoryStore.ts b/web/apps/cast/src/services/InMemoryStore.ts index ded73faf0..88e77b869 100644 --- a/web/apps/cast/src/services/InMemoryStore.ts +++ b/web/apps/cast/src/services/InMemoryStore.ts @@ -1,5 +1,4 @@ export enum MS_KEYS { - OPT_OUT_OF_CRASH_REPORTS = "optOutOfCrashReports", SRP_CONFIGURE_IN_PROGRESS = "srpConfigureInProgress", REDIRECT_URL = "redirectUrl", } diff --git a/web/apps/photos/.env.development b/web/apps/photos/.env.development index 045fffaee..548e5bbfb 100644 --- a/web/apps/photos/.env.development +++ b/web/apps/photos/.env.development @@ -22,7 +22,6 @@ # # - Logs go to the browser console (in addition to the log file) # - There is some additional logging -# - Sentry is not initialized # - ... (search for isDevBuild to see all impacts) # # Note that even in development build, the app still connects to the production @@ -69,7 +68,12 @@ # # NEXT_PUBLIC_ENTE_FAMILY_PORTAL_ENDPOINT = http://localhost:3003 -# The path of the JSON file which contains the expected results of our -# integration tests. See `upload.test.ts` for more details. +# The JSON which describes the expected results of our integration tests. See +# `upload.test.ts` for more details of the expected format. # -# NEXT_PUBLIC_ENTE_TEST_EXPECTED_JSON_PATH = /path/to/dataset/expected.json +# This is perhaps easier to specify as an environment variable, since then we +# can directly read from the source file when running `yarn dev`. For example, +# +# NEXT_PUBLIC_ENTE_TEST_EXPECTED_JSON=`cat path/to/expected.json` yarn dev +# +# NEXT_PUBLIC_ENTE_TEST_EXPECTED_JSON = {} diff --git a/web/apps/photos/next.config.js b/web/apps/photos/next.config.js index eea88bf93..81a64d7dd 100644 --- a/web/apps/photos/next.config.js +++ b/web/apps/photos/next.config.js @@ -1,3 +1 @@ -const nextConfigBase = require("@/next/next.config.base.js"); - -module.exports = nextConfigBase; +module.exports = require("@/next/next.config.base.js"); diff --git a/web/apps/photos/public/locales/bg-BG/translation.json b/web/apps/photos/public/locales/bg-BG/translation.json new file mode 100644 index 000000000..6b02fae65 --- /dev/null +++ b/web/apps/photos/public/locales/bg-BG/translation.json @@ -0,0 +1,641 @@ +{ + "HERO_SLIDE_1_TITLE": "
Личен бекъп
на твоите спомени
", + "HERO_SLIDE_1": "Криптиран от край до край по подразбиране", + "HERO_SLIDE_2_TITLE": "", + "HERO_SLIDE_2": "", + "HERO_SLIDE_3_TITLE": "", + "HERO_SLIDE_3": "", + "LOGIN": "", + "SIGN_UP": "", + "NEW_USER": "", + "EXISTING_USER": "", + "ENTER_NAME": "", + "PUBLIC_UPLOADER_NAME_MESSAGE": "", + "ENTER_EMAIL": "", + "EMAIL_ERROR": "", + "REQUIRED": "", + "EMAIL_SENT": "", + "CHECK_INBOX": "", + "ENTER_OTT": "", + "RESEND_MAIL": "", + "VERIFY": "", + "UNKNOWN_ERROR": "", + "INVALID_CODE": "", + "EXPIRED_CODE": "", + "SENDING": "", + "SENT": "", + "PASSWORD": "", + "LINK_PASSWORD": "", + "RETURN_PASSPHRASE_HINT": "", + "SET_PASSPHRASE": "", + "VERIFY_PASSPHRASE": "", + "INCORRECT_PASSPHRASE": "", + "ENTER_ENC_PASSPHRASE": "", + "PASSPHRASE_DISCLAIMER": "", + "WELCOME_TO_ENTE_HEADING": "", + "WELCOME_TO_ENTE_SUBHEADING": "", + "WHERE_YOUR_BEST_PHOTOS_LIVE": "", + "KEY_GENERATION_IN_PROGRESS_MESSAGE": "", + "PASSPHRASE_HINT": "", + "CONFIRM_PASSPHRASE": "", + "REFERRAL_CODE_HINT": "", + "REFERRAL_INFO": "", + "PASSPHRASE_MATCH_ERROR": "", + "CREATE_COLLECTION": "", + "ENTER_ALBUM_NAME": "", + "CLOSE_OPTION": "", + "ENTER_FILE_NAME": "", + "CLOSE": "", + "NO": "", + "NOTHING_HERE": "", + "UPLOAD": "", + "IMPORT": "", + "ADD_PHOTOS": "", + "ADD_MORE_PHOTOS": "", + "add_photos_one": "", + "add_photos_other": "", + "SELECT_PHOTOS": "", + "FILE_UPLOAD": "", + "UPLOAD_STAGE_MESSAGE": { + "0": "", + "1": "", + "2": "", + "3": "", + "4": "", + "5": "" + }, + "FILE_NOT_UPLOADED_LIST": "", + "SUBSCRIPTION_EXPIRED": "", + "SUBSCRIPTION_EXPIRED_MESSAGE": "", + "STORAGE_QUOTA_EXCEEDED": "", + "INITIAL_LOAD_DELAY_WARNING": "", + "USER_DOES_NOT_EXIST": "", + "NO_ACCOUNT": "", + "ACCOUNT_EXISTS": "", + "CREATE": "", + "DOWNLOAD": "", + "DOWNLOAD_OPTION": "", + "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": "", + "RECOVER_KEY_GENERATION_FAILED": "", + "KEY_NOT_STORED_DISCLAIMER": "", + "FORGOT_PASSWORD": "", + "RECOVER_ACCOUNT": "", + "RECOVERY_KEY_HINT": "", + "RECOVER": "", + "NO_RECOVERY_KEY": "", + "INCORRECT_RECOVERY_KEY": "", + "SORRY": "", + "NO_RECOVERY_KEY_MESSAGE": "", + "NO_TWO_FACTOR_RECOVERY_KEY_MESSAGE": "", + "CONTACT_SUPPORT": "", + "REQUEST_FEATURE": "", + "SUPPORT": "", + "CONFIRM": "", + "CANCEL": "", + "LOGOUT": "", + "DELETE_ACCOUNT": "", + "DELETE_ACCOUNT_MESSAGE": "", + "LOGOUT_MESSAGE": "", + "CHANGE_EMAIL": "", + "OK": "", + "SUCCESS": "", + "ERROR": "", + "MESSAGE": "", + "INSTALL_MOBILE_APP": "", + "DOWNLOAD_APP_MESSAGE": "", + "DOWNLOAD_APP": "", + "EXPORT": "", + "SUBSCRIPTION": "", + "SUBSCRIBE": "", + "MANAGEMENT_PORTAL": "", + "MANAGE_FAMILY_PORTAL": "", + "LEAVE_FAMILY_PLAN": "", + "LEAVE": "", + "LEAVE_FAMILY_CONFIRM": "", + "CHOOSE_PLAN": "", + "MANAGE_PLAN": "", + "ACTIVE": "", + "OFFLINE_MSG": "", + "FREE_SUBSCRIPTION_INFO": "", + "FAMILY_SUBSCRIPTION_INFO": "", + "RENEWAL_ACTIVE_SUBSCRIPTION_STATUS": "", + "RENEWAL_CANCELLED_SUBSCRIPTION_STATUS": "", + "RENEWAL_CANCELLED_SUBSCRIPTION_INFO": "", + "ADD_ON_AVAILABLE_TILL": "", + "STORAGE_QUOTA_EXCEEDED_SUBSCRIPTION_INFO": "", + "SUBSCRIPTION_PURCHASE_SUCCESS": "", + "SUBSCRIPTION_PURCHASE_CANCELLED": "", + "SUBSCRIPTION_PURCHASE_FAILED": "", + "SUBSCRIPTION_UPDATE_FAILED": "", + "UPDATE_PAYMENT_METHOD_MESSAGE": "", + "STRIPE_AUTHENTICATION_FAILED": "", + "UPDATE_PAYMENT_METHOD": "", + "MONTHLY": "", + "YEARLY": "", + "UPDATE_SUBSCRIPTION_MESSAGE": "", + "UPDATE_SUBSCRIPTION": "", + "CANCEL_SUBSCRIPTION": "", + "CANCEL_SUBSCRIPTION_MESSAGE": "", + "CANCEL_SUBSCRIPTION_WITH_ADDON_MESSAGE": "", + "SUBSCRIPTION_CANCEL_FAILED": "", + "SUBSCRIPTION_CANCEL_SUCCESS": "", + "REACTIVATE_SUBSCRIPTION": "", + "REACTIVATE_SUBSCRIPTION_MESSAGE": "", + "SUBSCRIPTION_ACTIVATE_SUCCESS": "", + "SUBSCRIPTION_ACTIVATE_FAILED": "", + "SUBSCRIPTION_PURCHASE_SUCCESS_TITLE": "", + "CANCEL_SUBSCRIPTION_ON_MOBILE": "", + "CANCEL_SUBSCRIPTION_ON_MOBILE_MESSAGE": "", + "MAIL_TO_MANAGE_SUBSCRIPTION": "", + "RENAME": "", + "RENAME_FILE": "", + "RENAME_COLLECTION": "", + "DELETE_COLLECTION_TITLE": "", + "DELETE_COLLECTION": "", + "DELETE_COLLECTION_MESSAGE": "", + "DELETE_PHOTOS": "", + "KEEP_PHOTOS": "", + "SHARE": "", + "SHARE_COLLECTION": "", + "SHAREES": "", + "SHARE_WITH_SELF": "", + "ALREADY_SHARED": "", + "SHARING_BAD_REQUEST_ERROR": "", + "SHARING_DISABLED_FOR_FREE_ACCOUNTS": "", + "DOWNLOAD_COLLECTION": "", + "DOWNLOAD_COLLECTION_MESSAGE": "", + "CREATE_ALBUM_FAILED": "", + "SEARCH": "", + "SEARCH_RESULTS": "", + "NO_RESULTS": "", + "SEARCH_HINT": "", + "SEARCH_TYPE": { + "COLLECTION": "", + "LOCATION": "", + "CITY": "", + "DATE": "", + "FILE_NAME": "", + "THING": "", + "FILE_CAPTION": "", + "FILE_TYPE": "", + "CLIP": "" + }, + "photos_count_zero": "", + "photos_count_one": "", + "photos_count_other": "", + "TERMS_AND_CONDITIONS": "", + "ADD_TO_COLLECTION": "", + "SELECTED": "", + "VIDEO_PLAYBACK_FAILED_DOWNLOAD_INSTEAD": "", + "PEOPLE": "", + "INDEXING_SCHEDULED": "", + "ANALYZING_PHOTOS": "", + "INDEXING_PEOPLE": "", + "INDEXING_DONE": "", + "UNIDENTIFIED_FACES": "", + "OBJECTS": "", + "TEXT": "", + "INFO": "", + "INFO_OPTION": "", + "FILE_NAME": "", + "CAPTION_PLACEHOLDER": "", + "LOCATION": "", + "SHOW_ON_MAP": "", + "MAP": "", + "MAP_SETTINGS": "", + "ENABLE_MAPS": "", + "ENABLE_MAP": "", + "DISABLE_MAPS": "", + "ENABLE_MAP_DESCRIPTION": "", + "DISABLE_MAP_DESCRIPTION": "", + "DISABLE_MAP": "", + "DETAILS": "", + "VIEW_EXIF": "", + "NO_EXIF": "", + "EXIF": "", + "ISO": "", + "TWO_FACTOR": "", + "TWO_FACTOR_AUTHENTICATION": "", + "TWO_FACTOR_QR_INSTRUCTION": "", + "ENTER_CODE_MANUALLY": "", + "TWO_FACTOR_MANUAL_CODE_INSTRUCTION": "", + "SCAN_QR_CODE": "", + "ENABLE_TWO_FACTOR": "", + "ENABLE": "", + "LOST_DEVICE": "", + "INCORRECT_CODE": "", + "TWO_FACTOR_INFO": "", + "DISABLE_TWO_FACTOR_LABEL": "", + "UPDATE_TWO_FACTOR_LABEL": "", + "DISABLE": "", + "RECONFIGURE": "", + "UPDATE_TWO_FACTOR": "", + "UPDATE_TWO_FACTOR_MESSAGE": "", + "UPDATE": "", + "DISABLE_TWO_FACTOR": "", + "DISABLE_TWO_FACTOR_MESSAGE": "", + "TWO_FACTOR_DISABLE_FAILED": "", + "EXPORT_DATA": "", + "SELECT_FOLDER": "", + "DESTINATION": "", + "START": "", + "LAST_EXPORT_TIME": "", + "EXPORT_AGAIN": "", + "LOCAL_STORAGE_NOT_ACCESSIBLE": "", + "LOCAL_STORAGE_NOT_ACCESSIBLE_MESSAGE": "", + "SEND_OTT": "", + "EMAIl_ALREADY_OWNED": "", + "ETAGS_BLOCKED": "", + "SKIPPED_VIDEOS_INFO": "", + "LIVE_PHOTOS_DETECTED": "", + "RETRY_FAILED": "", + "FAILED_UPLOADS": "", + "SKIPPED_FILES": "", + "THUMBNAIL_GENERATION_FAILED_UPLOADS": "", + "UNSUPPORTED_FILES": "", + "SUCCESSFUL_UPLOADS": "", + "SKIPPED_INFO": "", + "UNSUPPORTED_INFO": "", + "BLOCKED_UPLOADS": "", + "SKIPPED_VIDEOS": "", + "INPROGRESS_METADATA_EXTRACTION": "", + "INPROGRESS_UPLOADS": "", + "TOO_LARGE_UPLOADS": "", + "LARGER_THAN_AVAILABLE_STORAGE_UPLOADS": "", + "LARGER_THAN_AVAILABLE_STORAGE_INFO": "", + "TOO_LARGE_INFO": "", + "THUMBNAIL_GENERATION_FAILED_INFO": "", + "UPLOAD_TO_COLLECTION": "", + "UNCATEGORIZED": "", + "ARCHIVE": "", + "FAVORITES": "", + "ARCHIVE_COLLECTION": "", + "ARCHIVE_SECTION_NAME": "", + "ALL_SECTION_NAME": "", + "MOVE_TO_COLLECTION": "", + "UNARCHIVE": "", + "UNARCHIVE_COLLECTION": "", + "HIDE_COLLECTION": "", + "UNHIDE_COLLECTION": "", + "MOVE": "", + "ADD": "", + "REMOVE": "", + "YES_REMOVE": "", + "REMOVE_FROM_COLLECTION": "", + "TRASH": "", + "MOVE_TO_TRASH": "", + "TRASH_FILES_MESSAGE": "", + "TRASH_FILE_MESSAGE": "", + "DELETE_PERMANENTLY": "", + "RESTORE": "", + "RESTORE_TO_COLLECTION": "", + "EMPTY_TRASH": "", + "EMPTY_TRASH_TITLE": "", + "EMPTY_TRASH_MESSAGE": "", + "LEAVE_SHARED_ALBUM": "", + "LEAVE_ALBUM": "", + "LEAVE_SHARED_ALBUM_TITLE": "", + "LEAVE_SHARED_ALBUM_MESSAGE": "", + "NOT_FILE_OWNER": "", + "CONFIRM_SELF_REMOVE_MESSAGE": "", + "CONFIRM_SELF_AND_OTHER_REMOVE_MESSAGE": "", + "SORT_BY_CREATION_TIME_ASCENDING": "", + "SORT_BY_UPDATION_TIME_DESCENDING": "", + "SORT_BY_NAME": "", + "COMPRESS_THUMBNAILS": "", + "THUMBNAIL_REPLACED": "", + "FIX_THUMBNAIL": "", + "FIX_THUMBNAIL_LATER": "", + "REPLACE_THUMBNAIL_NOT_STARTED": "", + "REPLACE_THUMBNAIL_COMPLETED": "", + "REPLACE_THUMBNAIL_NOOP": "", + "REPLACE_THUMBNAIL_COMPLETED_WITH_ERROR": "", + "FIX_CREATION_TIME": "", + "FIX_CREATION_TIME_IN_PROGRESS": "", + "CREATION_TIME_UPDATED": "", + "UPDATE_CREATION_TIME_NOT_STARTED": "", + "UPDATE_CREATION_TIME_COMPLETED": "", + "UPDATE_CREATION_TIME_COMPLETED_WITH_ERROR": "", + "CAPTION_CHARACTER_LIMIT": "", + "DATE_TIME_ORIGINAL": "", + "DATE_TIME_DIGITIZED": "", + "METADATA_DATE": "", + "CUSTOM_TIME": "", + "REOPEN_PLAN_SELECTOR_MODAL": "", + "OPEN_PLAN_SELECTOR_MODAL_FAILED": "", + "INSTALL": "", + "SHARING_DETAILS": "", + "MODIFY_SHARING": "", + "ADD_COLLABORATORS": "", + "ADD_NEW_EMAIL": "", + "shared_with_people_zero": "", + "shared_with_people_one": "", + "shared_with_people_other": "", + "participants_zero": "", + "participants_one": "", + "participants_other": "", + "ADD_VIEWERS": "", + "PARTICIPANTS": "", + "CHANGE_PERMISSIONS_TO_VIEWER": "", + "CHANGE_PERMISSIONS_TO_COLLABORATOR": "", + "CONVERT_TO_VIEWER": "", + "CONVERT_TO_COLLABORATOR": "", + "CHANGE_PERMISSION": "", + "REMOVE_PARTICIPANT": "", + "CONFIRM_REMOVE": "", + "MANAGE": "", + "ADDED_AS": "", + "COLLABORATOR_RIGHTS": "", + "REMOVE_PARTICIPANT_HEAD": "", + "OWNER": "", + "COLLABORATORS": "", + "ADD_MORE": "", + "VIEWERS": "", + "OR_ADD_EXISTING": "", + "REMOVE_PARTICIPANT_MESSAGE": "", + "NOT_FOUND": "", + "LINK_EXPIRED": "", + "LINK_EXPIRED_MESSAGE": "", + "MANAGE_LINK": "", + "LINK_TOO_MANY_REQUESTS": "", + "FILE_DOWNLOAD": "", + "LINK_PASSWORD_LOCK": "", + "PUBLIC_COLLECT": "", + "LINK_DEVICE_LIMIT": "", + "NO_DEVICE_LIMIT": "", + "LINK_EXPIRY": "", + "NEVER": "", + "DISABLE_FILE_DOWNLOAD": "", + "DISABLE_FILE_DOWNLOAD_MESSAGE": "", + "MALICIOUS_CONTENT": "", + "COPYRIGHT": "", + "SHARED_USING": "", + "ENTE_IO": "", + "SHARING_REFERRAL_CODE": "", + "LIVE": "", + "DISABLE_PASSWORD": "", + "DISABLE_PASSWORD_MESSAGE": "", + "PASSWORD_LOCK": "", + "LOCK": "", + "DOWNLOAD_UPLOAD_LOGS": "", + "UPLOAD_FILES": "", + "UPLOAD_DIRS": "", + "UPLOAD_GOOGLE_TAKEOUT": "", + "DEDUPLICATE_FILES": "", + "AUTHENTICATOR_SECTION": "", + "NO_DUPLICATES_FOUND": "", + "CLUB_BY_CAPTURE_TIME": "", + "FILES": "", + "EACH": "", + "DEDUPLICATE_BASED_ON_SIZE": "", + "STOP_ALL_UPLOADS_MESSAGE": "", + "STOP_UPLOADS_HEADER": "", + "YES_STOP_UPLOADS": "", + "STOP_DOWNLOADS_HEADER": "", + "YES_STOP_DOWNLOADS": "", + "STOP_ALL_DOWNLOADS_MESSAGE": "", + "albums_one": "", + "albums_other": "", + "ALL_ALBUMS": "", + "ALBUMS": "", + "ALL_HIDDEN_ALBUMS": "", + "HIDDEN_ALBUMS": "", + "HIDDEN_ITEMS": "", + "HIDDEN_ITEMS_SECTION_NAME": "", + "ENTER_TWO_FACTOR_OTP": "", + "CREATE_ACCOUNT": "", + "COPIED": "", + "CANVAS_BLOCKED_TITLE": "", + "CANVAS_BLOCKED_MESSAGE": "", + "WATCH_FOLDERS": "", + "UPGRADE_NOW": "", + "RENEW_NOW": "", + "STORAGE": "", + "USED": "", + "YOU": "", + "FAMILY": "", + "FREE": "", + "OF": "", + "WATCHED_FOLDERS": "", + "NO_FOLDERS_ADDED": "", + "FOLDERS_AUTOMATICALLY_MONITORED": "", + "UPLOAD_NEW_FILES_TO_ENTE": "", + "REMOVE_DELETED_FILES_FROM_ENTE": "", + "ADD_FOLDER": "", + "STOP_WATCHING": "", + "STOP_WATCHING_FOLDER": "", + "STOP_WATCHING_DIALOG_MESSAGE": "", + "YES_STOP": "", + "MONTH_SHORT": "", + "YEAR": "", + "FAMILY_PLAN": "", + "DOWNLOAD_LOGS": "", + "DOWNLOAD_LOGS_MESSAGE": "", + "CHANGE_FOLDER": "", + "TWO_MONTHS_FREE": "", + "GB": "", + "POPULAR": "", + "FREE_PLAN_OPTION_LABEL": "", + "FREE_PLAN_DESCRIPTION": "", + "CURRENT_USAGE": "", + "WEAK_DEVICE": "", + "DRAG_AND_DROP_HINT": "", + "CONFIRM_ACCOUNT_DELETION_MESSAGE": "", + "AUTHENTICATE": "", + "UPLOADED_TO_SINGLE_COLLECTION": "", + "UPLOADED_TO_SEPARATE_COLLECTIONS": "", + "NEVERMIND": "", + "UPDATE_AVAILABLE": "", + "UPDATE_INSTALLABLE_MESSAGE": "", + "INSTALL_NOW": "", + "INSTALL_ON_NEXT_LAUNCH": "", + "UPDATE_AVAILABLE_MESSAGE": "", + "DOWNLOAD_AND_INSTALL": "", + "IGNORE_THIS_VERSION": "", + "TODAY": "", + "YESTERDAY": "", + "NAME_PLACEHOLDER": "", + "ROOT_LEVEL_FILE_WITH_FOLDER_NOT_ALLOWED": "", + "ROOT_LEVEL_FILE_WITH_FOLDER_NOT_ALLOWED_MESSAGE": "", + "CHOSE_THEME": "", + "ML_SEARCH": "", + "ENABLE_ML_SEARCH_DESCRIPTION": "", + "ML_MORE_DETAILS": "", + "ENABLE_FACE_SEARCH": "", + "ENABLE_FACE_SEARCH_TITLE": "", + "ENABLE_FACE_SEARCH_DESCRIPTION": "", + "DISABLE_BETA": "", + "DISABLE_FACE_SEARCH": "", + "DISABLE_FACE_SEARCH_TITLE": "", + "DISABLE_FACE_SEARCH_DESCRIPTION": "", + "ADVANCED": "", + "FACE_SEARCH_CONFIRMATION": "", + "LABS": "", + "YOURS": "", + "PASSPHRASE_STRENGTH_WEAK": "", + "PASSPHRASE_STRENGTH_MODERATE": "", + "PASSPHRASE_STRENGTH_STRONG": "", + "PREFERENCES": "", + "LANGUAGE": "", + "EXPORT_DIRECTORY_DOES_NOT_EXIST": "", + "EXPORT_DIRECTORY_DOES_NOT_EXIST_MESSAGE": "", + "SUBSCRIPTION_VERIFICATION_ERROR": "", + "STORAGE_UNITS": { + "B": "", + "KB": "", + "MB": "", + "GB": "", + "TB": "" + }, + "AFTER_TIME": { + "HOUR": "", + "DAY": "", + "WEEK": "", + "MONTH": "", + "YEAR": "" + }, + "COPY_LINK": "", + "DONE": "", + "LINK_SHARE_TITLE": "", + "REMOVE_LINK": "", + "CREATE_PUBLIC_SHARING": "", + "PUBLIC_LINK_CREATED": "", + "PUBLIC_LINK_ENABLED": "", + "COLLECT_PHOTOS": "", + "PUBLIC_COLLECT_SUBTEXT": "", + "STOP_EXPORT": "", + "EXPORT_PROGRESS": "", + "MIGRATING_EXPORT": "", + "RENAMING_COLLECTION_FOLDERS": "", + "TRASHING_DELETED_FILES": "", + "TRASHING_DELETED_COLLECTIONS": "", + "EXPORT_NOTIFICATION": { + "START": "", + "IN_PROGRESS": "", + "FINISH": "", + "UP_TO_DATE": "" + }, + "CONTINUOUS_EXPORT": "", + "TOTAL_ITEMS": "", + "PENDING_ITEMS": "", + "EXPORT_STARTING": "", + "DELETE_ACCOUNT_REASON_LABEL": "", + "DELETE_ACCOUNT_REASON_PLACEHOLDER": "", + "DELETE_REASON": { + "MISSING_FEATURE": "", + "BROKEN_BEHAVIOR": "", + "FOUND_ANOTHER_SERVICE": "", + "NOT_LISTED": "" + }, + "DELETE_ACCOUNT_FEEDBACK_LABEL": "", + "DELETE_ACCOUNT_FEEDBACK_PLACEHOLDER": "", + "CONFIRM_DELETE_ACCOUNT_CHECKBOX_LABEL": "", + "CONFIRM_DELETE_ACCOUNT": "", + "FEEDBACK_REQUIRED": "", + "FEEDBACK_REQUIRED_FOUND_ANOTHER_SERVICE": "", + "RECOVER_TWO_FACTOR": "", + "at": "", + "AUTH_NEXT": "", + "AUTH_DOWNLOAD_MOBILE_APP": "", + "HIDDEN": "", + "HIDE": "", + "UNHIDE": "", + "UNHIDE_TO_COLLECTION": "", + "SORT_BY": "", + "NEWEST_FIRST": "", + "OLDEST_FIRST": "", + "CONVERSION_FAILED_NOTIFICATION_MESSAGE": "", + "SELECT_COLLECTION": "", + "PIN_ALBUM": "", + "UNPIN_ALBUM": "", + "DOWNLOAD_COMPLETE": "", + "DOWNLOADING_COLLECTION": "", + "DOWNLOAD_FAILED": "", + "DOWNLOAD_PROGRESS": "", + "CHRISTMAS": "", + "CHRISTMAS_EVE": "", + "NEW_YEAR": "", + "NEW_YEAR_EVE": "", + "IMAGE": "", + "VIDEO": "", + "LIVE_PHOTO": "", + "CONVERT": "", + "CONFIRM_EDITOR_CLOSE_MESSAGE": "", + "CONFIRM_EDITOR_CLOSE_DESCRIPTION": "", + "BRIGHTNESS": "", + "CONTRAST": "", + "SATURATION": "", + "BLUR": "", + "INVERT_COLORS": "", + "ASPECT_RATIO": "", + "SQUARE": "", + "ROTATE_LEFT": "", + "ROTATE_RIGHT": "", + "FLIP_VERTICALLY": "", + "FLIP_HORIZONTALLY": "", + "DOWNLOAD_EDITED": "", + "SAVE_A_COPY_TO_ENTE": "", + "RESTORE_ORIGINAL": "", + "TRANSFORM": "", + "COLORS": "", + "FLIP": "", + "ROTATION": "", + "RESET": "", + "PHOTO_EDITOR": "", + "FASTER_UPLOAD": "", + "FASTER_UPLOAD_DESCRIPTION": "", + "MAGIC_SEARCH_STATUS": "", + "INDEXED_ITEMS": "", + "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": "", + "CACHE_DIRECTORY": "", + "PASSKEYS": "", + "FREEHAND": "", + "APPLY_CROP": "", + "PHOTO_EDIT_REQUIRED_TO_SAVE": "" +} diff --git a/web/apps/photos/public/locales/de-DE/translation.json b/web/apps/photos/public/locales/de-DE/translation.json index c0333d131..a515e7366 100644 --- a/web/apps/photos/public/locales/de-DE/translation.json +++ b/web/apps/photos/public/locales/de-DE/translation.json @@ -31,80 +31,78 @@ "VERIFY_PASSPHRASE": "Einloggen", "INCORRECT_PASSPHRASE": "Falsches Passwort", "ENTER_ENC_PASSPHRASE": "Bitte gib ein Passwort ein, mit dem wir deine Daten verschlüsseln können", - "PASSPHRASE_DISCLAIMER": "", + "PASSPHRASE_DISCLAIMER": "Wir speichern dein Passwort nicht. Wenn du es vergisst, können wir dir nicht helfen, deine Daten ohne einen Wiederherstellungsschlüssel wiederherzustellen.", "WELCOME_TO_ENTE_HEADING": "Willkommen bei ", - "WELCOME_TO_ENTE_SUBHEADING": "", + "WELCOME_TO_ENTE_SUBHEADING": "Ende-zu-Ende verschlüsselte Fotospeicherung und Freigabe", "WHERE_YOUR_BEST_PHOTOS_LIVE": "Wo deine besten Fotos leben", "KEY_GENERATION_IN_PROGRESS_MESSAGE": "Generierung von Verschlüsselungsschlüsseln...", "PASSPHRASE_HINT": "Passwort", "CONFIRM_PASSPHRASE": "Passwort bestätigen", - "REFERRAL_CODE_HINT": "", - "REFERRAL_INFO": "", + "REFERRAL_CODE_HINT": "Wie hast du von Ente erfahren? (optional)", + "REFERRAL_INFO": "Wir tracken keine App-Installationen. Es würde uns jedoch helfen, wenn du uns mitteilst, wie du von uns erfahren hast!", "PASSPHRASE_MATCH_ERROR": "Die Passwörter stimmen nicht überein", - "CONSOLE_WARNING_STOP": "STOPP!", - "CONSOLE_WARNING_DESC": "", "CREATE_COLLECTION": "Neues Album", "ENTER_ALBUM_NAME": "Albumname", "CLOSE_OPTION": "Schließen (Esc)", "ENTER_FILE_NAME": "Dateiname", "CLOSE": "Schließen", "NO": "Nein", - "NOTHING_HERE": "", + "NOTHING_HERE": "Hier gibt es noch nichts zu sehen 👀", "UPLOAD": "Hochladen", "IMPORT": "Importieren", "ADD_PHOTOS": "Fotos hinzufügen", "ADD_MORE_PHOTOS": "Mehr Fotos hinzufügen", - "add_photos_one": "", - "add_photos_other": "", + "add_photos_one": "Eine Datei hinzufügen", + "add_photos_other": "{{count, number}} Dateien hinzufügen", "SELECT_PHOTOS": "Foto auswählen", "FILE_UPLOAD": "Datei hochladen", "UPLOAD_STAGE_MESSAGE": { "0": "Hochladen wird vorbereitet", - "1": "", - "2": "", - "3": "", - "4": "", + "1": "Lese Google-Metadaten", + "2": "Metadaten von {{uploadCounter.finished, number}} / {{uploadCounter.total, number}} Dateien extrahiert", + "3": "{{uploadCounter.finished, number}} / {{uploadCounter.total, number}} Dateien verarbeitet", + "4": "Verbleibende Uploads werden abgebrochen", "5": "Sicherung abgeschlossen" }, - "FILE_NOT_UPLOADED_LIST": "", + "FILE_NOT_UPLOADED_LIST": "Die folgenden Dateien wurden nicht hochgeladen", "SUBSCRIPTION_EXPIRED": "Abonnement abgelaufen", - "SUBSCRIPTION_EXPIRED_MESSAGE": "", + "SUBSCRIPTION_EXPIRED_MESSAGE": "Dein Abonnement ist abgelaufen, bitte erneuere es", "STORAGE_QUOTA_EXCEEDED": "Speichergrenze überschritten", - "INITIAL_LOAD_DELAY_WARNING": "", - "USER_DOES_NOT_EXIST": "", - "NO_ACCOUNT": "", - "ACCOUNT_EXISTS": "", + "INITIAL_LOAD_DELAY_WARNING": "Das erste Laden kann einige Zeit in Anspruch nehmen", + "USER_DOES_NOT_EXIST": "Leider konnte kein Benutzer mit dieser E-Mail gefunden werden", + "NO_ACCOUNT": "Kein Konto vorhanden", + "ACCOUNT_EXISTS": "Es ist bereits ein Account vorhanden", "CREATE": "Erstellen", "DOWNLOAD": "Herunterladen", "DOWNLOAD_OPTION": "Herunterladen (D)", "DOWNLOAD_FAVORITES": "Favoriten herunterladen", - "DOWNLOAD_UNCATEGORIZED": "", - "DOWNLOAD_HIDDEN_ITEMS": "", + "DOWNLOAD_UNCATEGORIZED": "Download unkategorisiert", + "DOWNLOAD_HIDDEN_ITEMS": "Versteckte Dateien herunterladen", "COPY_OPTION": "Als PNG kopieren (Strg / Cmd - C)", - "TOGGLE_FULLSCREEN": "", + "TOGGLE_FULLSCREEN": "Vollbild umschalten (F)", "ZOOM_IN_OUT": "Herein-/Herauszoomen", - "PREVIOUS": "", - "NEXT": "", - "TITLE_PHOTOS": "", - "TITLE_ALBUMS": "", - "TITLE_AUTH": "", + "PREVIOUS": "Vorherige (←)", + "NEXT": "Weitere (→)", + "TITLE_PHOTOS": "Ente Fotos", + "TITLE_ALBUMS": "Ente Fotos", + "TITLE_AUTH": "Ente Auth", "UPLOAD_FIRST_PHOTO": "Lade dein erstes Foto hoch", "IMPORT_YOUR_FOLDERS": "Importiere deiner Ordner", - "UPLOAD_DROPZONE_MESSAGE": "", - "WATCH_FOLDER_DROPZONE_MESSAGE": "", + "UPLOAD_DROPZONE_MESSAGE": "Loslassen, um Dateien zu sichern", + "WATCH_FOLDER_DROPZONE_MESSAGE": "Loslassen, um beobachteten Ordner hinzuzufügen", "TRASH_FILES_TITLE": "Dateien löschen?", "TRASH_FILE_TITLE": "Datei löschen?", "DELETE_FILES_TITLE": "Sofort löschen?", - "DELETE_FILES_MESSAGE": "", + "DELETE_FILES_MESSAGE": "Ausgewählte Dateien werden dauerhaft aus Ihrem Ente-Konto gelöscht.", "DELETE": "Löschen", "DELETE_OPTION": "Löschen (DEL)", - "FAVORITE_OPTION": "", - "UNFAVORITE_OPTION": "", - "MULTI_FOLDER_UPLOAD": "", - "UPLOAD_STRATEGY_CHOICE": "", + "FAVORITE_OPTION": "Zu Favoriten hinzufügen (L)", + "UNFAVORITE_OPTION": "Von Favoriten entfernen (L)", + "MULTI_FOLDER_UPLOAD": "Mehrere Ordner erkannt", + "UPLOAD_STRATEGY_CHOICE": "Möchtest du sie hochladen in", "UPLOAD_STRATEGY_SINGLE_COLLECTION": "Ein einzelnes Album", "OR": "oder", - "UPLOAD_STRATEGY_COLLECTION_PER_FOLDER": "", + "UPLOAD_STRATEGY_COLLECTION_PER_FOLDER": "Getrennte Alben", "SESSION_EXPIRED_MESSAGE": "Ihre Sitzung ist abgelaufen. Bitte loggen Sie sich erneut ein, um fortzufahren", "SESSION_EXPIRED": "Sitzung abgelaufen", "PASSWORD_GENERATION_FAILED": "Dein Browser konnte keinen starken Schlüssel generieren, der den Verschlüsselungsstandards des Entes entspricht, bitte versuche die mobile App oder einen anderen Browser zu verwenden", @@ -113,9 +111,9 @@ "RECOVERY_KEY": "Wiederherstellungsschlüssel", "SAVE_LATER": "Auf später verschieben", "SAVE": "Schlüssel speichern", - "RECOVERY_KEY_DESCRIPTION": "", - "RECOVER_KEY_GENERATION_FAILED": "", - "KEY_NOT_STORED_DISCLAIMER": "", + "RECOVERY_KEY_DESCRIPTION": "Falls du dein Passwort vergisst, kannst du deine Daten nur mit diesem Schlüssel wiederherstellen.", + "RECOVER_KEY_GENERATION_FAILED": "Wiederherstellungsschlüssel konnte nicht generiert werden, bitte versuche es erneut", + "KEY_NOT_STORED_DISCLAIMER": "Wir speichern diesen Schlüssel nicht, also speichere ihn bitte an einem sicheren Ort", "FORGOT_PASSWORD": "Passwort vergessen", "RECOVER_ACCOUNT": "Konto wiederherstellen", "RECOVERY_KEY_HINT": "Wiederherstellungsschlüssel", @@ -132,15 +130,15 @@ "CANCEL": "Abbrechen", "LOGOUT": "Ausloggen", "DELETE_ACCOUNT": "Konto löschen", - "DELETE_ACCOUNT_MESSAGE": "", + "DELETE_ACCOUNT_MESSAGE": "

Bitte sende eine E-Mail an {{emailID}} mit deiner registrierten E-Mail-Adresse.

Deine Anfrage wird innerhalb von 72 Stunden bearbeitet.

", "LOGOUT_MESSAGE": "Sind sie sicher, dass sie sich ausloggen möchten?", "CHANGE_EMAIL": "E-Mail-Adresse ändern", "OK": "OK", "SUCCESS": "Erfolgreich", "ERROR": "Fehler", "MESSAGE": "Nachricht", - "INSTALL_MOBILE_APP": "", - "DOWNLOAD_APP_MESSAGE": "", + "INSTALL_MOBILE_APP": "Installiere unsere Android oder iOS App, um automatisch alle deine Fotos zu sichern", + "DOWNLOAD_APP_MESSAGE": "Entschuldigung, dieser Vorgang wird derzeit nur von unserer Desktop-App unterstützt", "DOWNLOAD_APP": "Desktopanwendung herunterladen", "EXPORT": "Daten exportieren", "SUBSCRIPTION": "Abonnement", @@ -154,37 +152,37 @@ "MANAGE_PLAN": "Verwalte dein Abonnement", "ACTIVE": "Aktiv", "OFFLINE_MSG": "Du bist offline, gecachte Erinnerungen werden angezeigt", - "FREE_SUBSCRIPTION_INFO": "", + "FREE_SUBSCRIPTION_INFO": "Du bist auf dem kostenlosen Plan, der am {{date, dateTime}} ausläuft", "FAMILY_SUBSCRIPTION_INFO": "Sie haben einen Familienplan verwaltet von", "RENEWAL_ACTIVE_SUBSCRIPTION_STATUS": "Erneuert am {{date, dateTime}}", "RENEWAL_CANCELLED_SUBSCRIPTION_STATUS": "Endet am {{date, dateTime}}", "RENEWAL_CANCELLED_SUBSCRIPTION_INFO": "Ihr Abo endet am {{date, dateTime}}", - "ADD_ON_AVAILABLE_TILL": "", + "ADD_ON_AVAILABLE_TILL": "Dein {{storage, string}} Add-on ist gültig bis {{date, dateTime}}", "STORAGE_QUOTA_EXCEEDED_SUBSCRIPTION_INFO": "Sie haben Ihr Speichervolumen überschritten, bitte upgraden Sie", - "SUBSCRIPTION_PURCHASE_SUCCESS": "", - "SUBSCRIPTION_PURCHASE_CANCELLED": "", + "SUBSCRIPTION_PURCHASE_SUCCESS": "

Wir haben deine Zahlung erhalten

Dein Abonnement ist gültig bis {{date, dateTime}}

", + "SUBSCRIPTION_PURCHASE_CANCELLED": "Dein Kauf wurde abgebrochen. Bitte versuche es erneut, wenn du abonnieren willst", "SUBSCRIPTION_PURCHASE_FAILED": "Kauf des Abonnements fehlgeschlagen Bitte versuchen Sie es erneut", "SUBSCRIPTION_UPDATE_FAILED": "Aktualisierung des Abonnements fehlgeschlagen Bitte versuchen Sie es erneut", "UPDATE_PAYMENT_METHOD_MESSAGE": "", - "STRIPE_AUTHENTICATION_FAILED": "", + "STRIPE_AUTHENTICATION_FAILED": "Wir können deine Zahlungsmethode nicht authentifizieren. Bitte wähle eine andere Zahlungsmethode und versuche es erneut", "UPDATE_PAYMENT_METHOD": "Zahlungsmethode aktualisieren", "MONTHLY": "Monatlich", "YEARLY": "Jährlich", "UPDATE_SUBSCRIPTION_MESSAGE": "Sind Sie sicher, dass Sie Ihren Tarif ändern möchten?", "UPDATE_SUBSCRIPTION": "Plan ändern", "CANCEL_SUBSCRIPTION": "Abonnement kündigen", - "CANCEL_SUBSCRIPTION_MESSAGE": "", - "CANCEL_SUBSCRIPTION_WITH_ADDON_MESSAGE": "", - "SUBSCRIPTION_CANCEL_FAILED": "", - "SUBSCRIPTION_CANCEL_SUCCESS": "", + "CANCEL_SUBSCRIPTION_MESSAGE": "

Alle deine Daten werden am Ende dieses Abrechnungszeitraums von unseren Servern gelöscht.

Bist du sicher, dass du dein Abonnement kündigen möchtest?

", + "CANCEL_SUBSCRIPTION_WITH_ADDON_MESSAGE": "

Bist du sicher, dass du dein Abonnement beenden möchtest?

", + "SUBSCRIPTION_CANCEL_FAILED": "Abonnement konnte nicht storniert werden", + "SUBSCRIPTION_CANCEL_SUCCESS": "Abonnement erfolgreich beendet", "REACTIVATE_SUBSCRIPTION": "Abonnement reaktivieren", - "REACTIVATE_SUBSCRIPTION_MESSAGE": "", - "SUBSCRIPTION_ACTIVATE_SUCCESS": "", - "SUBSCRIPTION_ACTIVATE_FAILED": "", - "SUBSCRIPTION_PURCHASE_SUCCESS_TITLE": "", - "CANCEL_SUBSCRIPTION_ON_MOBILE": "", - "CANCEL_SUBSCRIPTION_ON_MOBILE_MESSAGE": "", - "MAIL_TO_MANAGE_SUBSCRIPTION": "", + "REACTIVATE_SUBSCRIPTION_MESSAGE": "Nach der Reaktivierung wird am {{date, dateTime}} abgerechnet", + "SUBSCRIPTION_ACTIVATE_SUCCESS": "Abonnement erfolgreich aktiviert ", + "SUBSCRIPTION_ACTIVATE_FAILED": "Reaktivierung der Abonnementverlängerung fehlgeschlagen", + "SUBSCRIPTION_PURCHASE_SUCCESS_TITLE": "Vielen Dank", + "CANCEL_SUBSCRIPTION_ON_MOBILE": "Mobiles Abonnement kündigen", + "CANCEL_SUBSCRIPTION_ON_MOBILE_MESSAGE": "Bitte kündige dein Abonnement in der mobilen App, um hier ein Abonnement zu aktivieren", + "MAIL_TO_MANAGE_SUBSCRIPTION": "Bitte kontaktiere uns über {{emailID}}, um dein Abo zu verwalten", "RENAME": "Umbenennen", "RENAME_FILE": "Datei umbenennen", "RENAME_COLLECTION": "Album umbenennen", @@ -198,43 +196,43 @@ "SHAREES": "Geteilt mit", "SHARE_WITH_SELF": "Du kannst nicht mit dir selbst teilen", "ALREADY_SHARED": "Hoppla, Sie teilen dies bereits mit {{email}}", - "SHARING_BAD_REQUEST_ERROR": "", - "SHARING_DISABLED_FOR_FREE_ACCOUNTS": "", + "SHARING_BAD_REQUEST_ERROR": "Albumfreigabe nicht erlaubt", + "SHARING_DISABLED_FOR_FREE_ACCOUNTS": "Freigabe ist für kostenlose Konten deaktiviert", "DOWNLOAD_COLLECTION": "Album herunterladen", - "DOWNLOAD_COLLECTION_MESSAGE": "", - "CREATE_ALBUM_FAILED": "", + "DOWNLOAD_COLLECTION_MESSAGE": "

Bist du sicher, dass du das komplette Album herunterladen möchtest?

Alle Dateien werden der Warteschlange zum sequenziellen Download hinzugefügt

", + "CREATE_ALBUM_FAILED": "Fehler beim Erstellen des Albums, bitte versuche es erneut", "SEARCH": "Suchen", "SEARCH_RESULTS": "Ergebnisse durchsuchen", - "NO_RESULTS": "", - "SEARCH_HINT": "", + "NO_RESULTS": "Keine Ergebnisse gefunden", + "SEARCH_HINT": "Suche nach Alben, Datum, Beschreibungen, ...", "SEARCH_TYPE": { "COLLECTION": "Album", "LOCATION": "Standort", - "CITY": "", + "CITY": "Ort", "DATE": "Datum", "FILE_NAME": "Dateiname", "THING": "Inhalt", "FILE_CAPTION": "Beschreibung", - "FILE_TYPE": "", - "CLIP": "" + "FILE_TYPE": "Dateityp", + "CLIP": "Magie" }, "photos_count_zero": "Keine Erinnerungen", - "photos_count_one": "", - "photos_count_other": "", - "TERMS_AND_CONDITIONS": "", + "photos_count_one": "Eine Erinnerung", + "photos_count_other": "{{count, number}} Erinnerungen", + "TERMS_AND_CONDITIONS": "Ich stimme den Bedingungen und Datenschutzrichtlinien zu", "ADD_TO_COLLECTION": "Zum Album hinzufügen", - "SELECTED": "", - "VIDEO_PLAYBACK_FAILED_DOWNLOAD_INSTEAD": "", + "SELECTED": "ausgewählt", + "VIDEO_PLAYBACK_FAILED_DOWNLOAD_INSTEAD": "Dieses Video kann in deinem Browser nicht abgespielt werden", "PEOPLE": "Personen", - "INDEXING_SCHEDULED": "", - "ANALYZING_PHOTOS": "", - "INDEXING_PEOPLE": "", - "INDEXING_DONE": "", - "UNIDENTIFIED_FACES": "", - "OBJECTS": "", - "TEXT": "", + "INDEXING_SCHEDULED": "Indizierung ist geplant...", + "ANALYZING_PHOTOS": "Indiziere Fotos ({{indexStatus.nSyncedFiles,number}} / {{indexStatus.nTotalFiles,number}})", + "INDEXING_PEOPLE": "Indiziere Personen in {{indexStatus.nSyncedFiles,number}} Fotos...", + "INDEXING_DONE": "{{indexStatus.nSyncedFiles,number}} Fotos wurden indiziert", + "UNIDENTIFIED_FACES": "unidentifizierte Gesichter", + "OBJECTS": "Objekte", + "TEXT": "Text", "INFO": "Info ", - "INFO_OPTION": "", + "INFO_OPTION": "Info (I)", "FILE_NAME": "Dateiname", "CAPTION_PLACEHOLDER": "Eine Beschreibung hinzufügen", "LOCATION": "Standort", @@ -244,8 +242,8 @@ "ENABLE_MAPS": "Karten aktivieren?", "ENABLE_MAP": "Karte aktivieren", "DISABLE_MAPS": "Karten deaktivieren?", - "ENABLE_MAP_DESCRIPTION": "", - "DISABLE_MAP_DESCRIPTION": "", + "ENABLE_MAP_DESCRIPTION": "

Dies wird deine Fotos auf einer Weltkarte anzeigen.

Die Karte wird von OpenStreetMap gehostet und die genauen Standorte deiner Fotos werden niemals geteilt.

Diese Funktion kannst du jederzeit in den Einstellungen deaktivieren.

", + "DISABLE_MAP_DESCRIPTION": "

Dies wird die Anzeige deiner Fotos auf einer Weltkarte deaktivieren.

Du kannst diese Funktion jederzeit in den Einstellungen aktivieren.

", "DISABLE_MAP": "Karte deaktivieren", "DETAILS": "Details", "VIEW_EXIF": "Alle EXIF-Daten anzeigen", @@ -254,32 +252,32 @@ "ISO": "ISO", "TWO_FACTOR": "Zwei-Faktor", "TWO_FACTOR_AUTHENTICATION": "Zwei-Faktor-Authentifizierung", - "TWO_FACTOR_QR_INSTRUCTION": "", + "TWO_FACTOR_QR_INSTRUCTION": "Scanne den QR-Code unten mit deiner bevorzugten Authentifizierungs-App", "ENTER_CODE_MANUALLY": "Geben Sie den Code manuell ein", - "TWO_FACTOR_MANUAL_CODE_INSTRUCTION": "", + "TWO_FACTOR_MANUAL_CODE_INSTRUCTION": "Bitte gib diesen Code in deiner bevorzugten Authentifizierungs-App ein", "SCAN_QR_CODE": "QR‐Code stattdessen scannen", "ENABLE_TWO_FACTOR": "Zwei-Faktor-Authentifizierung aktivieren", "ENABLE": "Aktivieren", - "LOST_DEVICE": "", + "LOST_DEVICE": "Zwei-Faktor-Gerät verloren", "INCORRECT_CODE": "Falscher Code", "TWO_FACTOR_INFO": "", "DISABLE_TWO_FACTOR_LABEL": "Deaktiviere die Zwei-Faktor-Authentifizierung", - "UPDATE_TWO_FACTOR_LABEL": "", + "UPDATE_TWO_FACTOR_LABEL": "Authentifizierungsgerät aktualisieren", "DISABLE": "Deaktivieren", "RECONFIGURE": "Neu einrichten", - "UPDATE_TWO_FACTOR": "", + "UPDATE_TWO_FACTOR": "Zweiten Faktor aktualisieren", "UPDATE_TWO_FACTOR_MESSAGE": "", - "UPDATE": "", - "DISABLE_TWO_FACTOR": "", - "DISABLE_TWO_FACTOR_MESSAGE": "", - "TWO_FACTOR_DISABLE_FAILED": "", + "UPDATE": "Aktualisierung", + "DISABLE_TWO_FACTOR": "Zweiten Faktor deaktivieren", + "DISABLE_TWO_FACTOR_MESSAGE": "Bist du sicher, dass du die Zwei-Faktor-Authentifizierung deaktivieren willst", + "TWO_FACTOR_DISABLE_FAILED": "Fehler beim Deaktivieren des zweiten Faktors, bitte versuchen Sie es erneut", "EXPORT_DATA": "Daten exportieren", "SELECT_FOLDER": "Ordner auswählen", "DESTINATION": "Zielort", "START": "Start", - "LAST_EXPORT_TIME": "", - "EXPORT_AGAIN": "", - "LOCAL_STORAGE_NOT_ACCESSIBLE": "", + "LAST_EXPORT_TIME": "Letztes Exportdatum", + "EXPORT_AGAIN": "Neusynchronisation", + "LOCAL_STORAGE_NOT_ACCESSIBLE": "Lokaler Speicher nicht zugänglich", "LOCAL_STORAGE_NOT_ACCESSIBLE_MESSAGE": "", "SEND_OTT": "OTP senden", "EMAIl_ALREADY_OWNED": "Diese E-Mail wird bereits verwendet", @@ -304,17 +302,17 @@ "TOO_LARGE_INFO": "Diese Dateien wurden nicht hochgeladen, da sie unsere maximale Dateigröße überschreiten", "THUMBNAIL_GENERATION_FAILED_INFO": "Diese Dateien wurden hochgeladen, aber leider konnten wir nicht die Thumbnails für sie generieren.", "UPLOAD_TO_COLLECTION": "In Album hochladen", - "UNCATEGORIZED": "", + "UNCATEGORIZED": "Unkategorisiert", "ARCHIVE": "Archiv", "FAVORITES": "Favoriten", "ARCHIVE_COLLECTION": "Album archivieren", "ARCHIVE_SECTION_NAME": "Archiv", "ALL_SECTION_NAME": "Alle", "MOVE_TO_COLLECTION": "Zum Album verschieben", - "UNARCHIVE": "", - "UNARCHIVE_COLLECTION": "", - "HIDE_COLLECTION": "", - "UNHIDE_COLLECTION": "", + "UNARCHIVE": "Dearchivieren", + "UNARCHIVE_COLLECTION": "Album dearchivieren", + "HIDE_COLLECTION": "Album ausblenden", + "UNHIDE_COLLECTION": "Album wieder einblenden", "MOVE": "Verschieben", "ADD": "Hinzufügen", "REMOVE": "Entfernen", @@ -336,14 +334,14 @@ "LEAVE_SHARED_ALBUM_MESSAGE": "", "NOT_FILE_OWNER": "Dateien in einem freigegebenen Album können nicht gelöscht werden", "CONFIRM_SELF_REMOVE_MESSAGE": "", - "CONFIRM_SELF_AND_OTHER_REMOVE_MESSAGE": "", + "CONFIRM_SELF_AND_OTHER_REMOVE_MESSAGE": "Einige der Elemente, die du entfernst, wurden von anderen Nutzern hinzugefügt und du wirst den Zugriff auf sie verlieren.", "SORT_BY_CREATION_TIME_ASCENDING": "Ältestem", "SORT_BY_UPDATION_TIME_DESCENDING": "Zuletzt aktualisiert", "SORT_BY_NAME": "Name", "COMPRESS_THUMBNAILS": "", "THUMBNAIL_REPLACED": "", "FIX_THUMBNAIL": "Komprimiere", - "FIX_THUMBNAIL_LATER": "", + "FIX_THUMBNAIL_LATER": "Später komprimieren", "REPLACE_THUMBNAIL_NOT_STARTED": "", "REPLACE_THUMBNAIL_COMPLETED": "", "REPLACE_THUMBNAIL_NOOP": "", @@ -358,49 +356,49 @@ "DATE_TIME_ORIGINAL": "", "DATE_TIME_DIGITIZED": "", "METADATA_DATE": "", - "CUSTOM_TIME": "", + "CUSTOM_TIME": "Benutzerdefinierte Zeit", "REOPEN_PLAN_SELECTOR_MODAL": "", "OPEN_PLAN_SELECTOR_MODAL_FAILED": "", "INSTALL": "Installieren", "SHARING_DETAILS": "Details teilen", - "MODIFY_SHARING": "", - "ADD_COLLABORATORS": "", - "ADD_NEW_EMAIL": "", + "MODIFY_SHARING": "Freigabe ändern", + "ADD_COLLABORATORS": "Bearbeiter hinzufügen", + "ADD_NEW_EMAIL": "Neue E-Mail-Adresse hinzufügen", "shared_with_people_zero": "", - "shared_with_people_one": "", + "shared_with_people_one": "Geteilt mit einer Person", "shared_with_people_other": "", - "participants_zero": "", - "participants_one": "", + "participants_zero": "Keine Teilnehmer", + "participants_one": "1 Teilnehmer", "participants_other": "", - "ADD_VIEWERS": "", - "PARTICIPANTS": "", + "ADD_VIEWERS": "Betrachter hinzufügen", + "PARTICIPANTS": "Teilnehmer", "CHANGE_PERMISSIONS_TO_VIEWER": "", "CHANGE_PERMISSIONS_TO_COLLABORATOR": "", - "CONVERT_TO_VIEWER": "", + "CONVERT_TO_VIEWER": "Ja, zu \"Beobachter\" ändern", "CONVERT_TO_COLLABORATOR": "", "CHANGE_PERMISSION": "", "REMOVE_PARTICIPANT": "Entfernen?", "CONFIRM_REMOVE": "Ja, entfernen", "MANAGE": "Verwalten", - "ADDED_AS": "", - "COLLABORATOR_RIGHTS": "", + "ADDED_AS": "Hinzugefügt als", + "COLLABORATOR_RIGHTS": "Bearbeiter können Fotos & Videos zu dem geteilten Album hinzufügen", "REMOVE_PARTICIPANT_HEAD": "Teilnehmer entfernen", "OWNER": "Besitzer", "COLLABORATORS": "", - "ADD_MORE": "", + "ADD_MORE": "Mehr hinzufügen", "VIEWERS": "", - "OR_ADD_EXISTING": "", + "OR_ADD_EXISTING": "Oder eine Vorherige auswählen", "REMOVE_PARTICIPANT_MESSAGE": "", "NOT_FOUND": "404 - Nicht gefunden", "LINK_EXPIRED": "Link ist abgelaufen", "LINK_EXPIRED_MESSAGE": "Dieser Link ist abgelaufen oder wurde deaktiviert!", - "MANAGE_LINK": "", + "MANAGE_LINK": "Link verwalten", "LINK_TOO_MANY_REQUESTS": "Sorry, dieses Album wurde auf zu vielen Geräten angezeigt!", "FILE_DOWNLOAD": "Downloads erlauben", "LINK_PASSWORD_LOCK": "Passwort Sperre", - "PUBLIC_COLLECT": "", + "PUBLIC_COLLECT": "Hinzufügen von Fotos erlauben", "LINK_DEVICE_LIMIT": "Geräte Limit", - "NO_DEVICE_LIMIT": "", + "NO_DEVICE_LIMIT": "Keins", "LINK_EXPIRY": "Ablaufdatum des Links", "NEVER": "Niemals", "DISABLE_FILE_DOWNLOAD": "", @@ -414,7 +412,7 @@ "DISABLE_PASSWORD": "", "DISABLE_PASSWORD_MESSAGE": "", "PASSWORD_LOCK": "Passwort Sperre", - "LOCK": "", + "LOCK": "Sperren", "DOWNLOAD_UPLOAD_LOGS": "", "UPLOAD_FILES": "Datei", "UPLOAD_DIRS": "Ordner", @@ -590,7 +588,6 @@ "DOWNLOADING_COLLECTION": "", "DOWNLOAD_FAILED": "", "DOWNLOAD_PROGRESS": "", - "CRASH_REPORTING": "", "CHRISTMAS": "", "CHRISTMAS_EVE": "", "NEW_YEAR": "", diff --git a/web/apps/photos/public/locales/en-US/translation.json b/web/apps/photos/public/locales/en-US/translation.json index 6870df319..ea74f4f5f 100644 --- a/web/apps/photos/public/locales/en-US/translation.json +++ b/web/apps/photos/public/locales/en-US/translation.json @@ -41,8 +41,6 @@ "REFERRAL_CODE_HINT": "How did you hear about Ente? (optional)", "REFERRAL_INFO": "We don't track app installs, It'd help us if you told us where you found us!", "PASSPHRASE_MATCH_ERROR": "Passwords don't match", - "CONSOLE_WARNING_STOP": "STOP!", - "CONSOLE_WARNING_DESC": "This is a browser feature intended for developers. Please don't copy-paste unverified code here.", "CREATE_COLLECTION": "New album", "ENTER_ALBUM_NAME": "Album name", "CLOSE_OPTION": "Close (Esc)", @@ -590,7 +588,6 @@ "DOWNLOADING_COLLECTION": "Downloading {{name}}", "DOWNLOAD_FAILED": "Download failed", "DOWNLOAD_PROGRESS": "{{progress.current}} / {{progress.total}} files", - "CRASH_REPORTING": "Crash reporting", "CHRISTMAS": "Christmas", "CHRISTMAS_EVE": "Christmas Eve", "NEW_YEAR": "New Year", diff --git a/web/apps/photos/public/locales/es-ES/translation.json b/web/apps/photos/public/locales/es-ES/translation.json index 4adb22ee5..64c2849eb 100644 --- a/web/apps/photos/public/locales/es-ES/translation.json +++ b/web/apps/photos/public/locales/es-ES/translation.json @@ -41,8 +41,6 @@ "REFERRAL_CODE_HINT": "", "REFERRAL_INFO": "", "PASSPHRASE_MATCH_ERROR": "Las contraseñas no coinciden", - "CONSOLE_WARNING_STOP": "STOP!", - "CONSOLE_WARNING_DESC": "Esta es una característica del navegador destinada a los desarrolladores. Por favor, no copie y pegue código sin verificar aquí.", "CREATE_COLLECTION": "Nuevo álbum", "ENTER_ALBUM_NAME": "Nombre del álbum", "CLOSE_OPTION": "Cerrar (Esc)", @@ -590,7 +588,6 @@ "DOWNLOADING_COLLECTION": "", "DOWNLOAD_FAILED": "", "DOWNLOAD_PROGRESS": "", - "CRASH_REPORTING": "", "CHRISTMAS": "", "CHRISTMAS_EVE": "", "NEW_YEAR": "", diff --git a/web/apps/photos/public/locales/fa-IR/translation.json b/web/apps/photos/public/locales/fa-IR/translation.json index 3817bccd8..68e1da3b9 100644 --- a/web/apps/photos/public/locales/fa-IR/translation.json +++ b/web/apps/photos/public/locales/fa-IR/translation.json @@ -41,8 +41,6 @@ "REFERRAL_CODE_HINT": "", "REFERRAL_INFO": "", "PASSPHRASE_MATCH_ERROR": "", - "CONSOLE_WARNING_STOP": "", - "CONSOLE_WARNING_DESC": "", "CREATE_COLLECTION": "", "ENTER_ALBUM_NAME": "", "CLOSE_OPTION": "", @@ -590,7 +588,6 @@ "DOWNLOADING_COLLECTION": "", "DOWNLOAD_FAILED": "", "DOWNLOAD_PROGRESS": "", - "CRASH_REPORTING": "", "CHRISTMAS": "", "CHRISTMAS_EVE": "", "NEW_YEAR": "", diff --git a/web/apps/photos/public/locales/fi-FI/translation.json b/web/apps/photos/public/locales/fi-FI/translation.json index bc335bc77..fbc0b937a 100644 --- a/web/apps/photos/public/locales/fi-FI/translation.json +++ b/web/apps/photos/public/locales/fi-FI/translation.json @@ -41,8 +41,6 @@ "REFERRAL_CODE_HINT": "", "REFERRAL_INFO": "", "PASSPHRASE_MATCH_ERROR": "", - "CONSOLE_WARNING_STOP": "", - "CONSOLE_WARNING_DESC": "", "CREATE_COLLECTION": "", "ENTER_ALBUM_NAME": "", "CLOSE_OPTION": "", @@ -590,7 +588,6 @@ "DOWNLOADING_COLLECTION": "", "DOWNLOAD_FAILED": "", "DOWNLOAD_PROGRESS": "", - "CRASH_REPORTING": "", "CHRISTMAS": "", "CHRISTMAS_EVE": "", "NEW_YEAR": "", diff --git a/web/apps/photos/public/locales/fr-FR/translation.json b/web/apps/photos/public/locales/fr-FR/translation.json index 0d878a703..2d183c9eb 100644 --- a/web/apps/photos/public/locales/fr-FR/translation.json +++ b/web/apps/photos/public/locales/fr-FR/translation.json @@ -41,8 +41,6 @@ "REFERRAL_CODE_HINT": "Comment avez-vous entendu parler de Ente? (facultatif)", "REFERRAL_INFO": "Nous ne suivons pas les installations d'applications. Il serait utile que vous nous disiez comment vous nous avez trouvés !", "PASSPHRASE_MATCH_ERROR": "Les mots de passe ne correspondent pas", - "CONSOLE_WARNING_STOP": "STOP!", - "CONSOLE_WARNING_DESC": "Ceci est une fonction de navigateur dédiée aux développeurs. Veuillez ne pas copier-coller un code non vérifié à cet endroit.", "CREATE_COLLECTION": "Nouvel album", "ENTER_ALBUM_NAME": "Nom de l'album", "CLOSE_OPTION": "Fermer (Échap)", @@ -590,7 +588,6 @@ "DOWNLOADING_COLLECTION": "Téléchargement de {{name}}", "DOWNLOAD_FAILED": "Échec du téléchargement", "DOWNLOAD_PROGRESS": "{{progress.current}} / {{progress.total}} fichiers", - "CRASH_REPORTING": "Rapport de plantage", "CHRISTMAS": "Noël", "CHRISTMAS_EVE": "Réveillon de Noël", "NEW_YEAR": "Nouvel an", diff --git a/web/apps/photos/public/locales/it-IT/translation.json b/web/apps/photos/public/locales/it-IT/translation.json index 3d8dcb3c6..dd7afc158 100644 --- a/web/apps/photos/public/locales/it-IT/translation.json +++ b/web/apps/photos/public/locales/it-IT/translation.json @@ -41,8 +41,6 @@ "REFERRAL_CODE_HINT": "Come hai conosciuto Ente? (opzionale)", "REFERRAL_INFO": "", "PASSPHRASE_MATCH_ERROR": "Le password non corrispondono", - "CONSOLE_WARNING_STOP": "STOP!", - "CONSOLE_WARNING_DESC": "Questa è una funzionalità del browser destinata agli sviluppatori. Non copiare né incollare codice non verificato qui.", "CREATE_COLLECTION": "Nuovo album", "ENTER_ALBUM_NAME": "Nome album", "CLOSE_OPTION": "Chiudi (Esc)", @@ -590,7 +588,6 @@ "DOWNLOADING_COLLECTION": "", "DOWNLOAD_FAILED": "", "DOWNLOAD_PROGRESS": "", - "CRASH_REPORTING": "", "CHRISTMAS": "", "CHRISTMAS_EVE": "", "NEW_YEAR": "", diff --git a/web/apps/photos/public/locales/ko-KR/translation.json b/web/apps/photos/public/locales/ko-KR/translation.json index bc335bc77..7482dd397 100644 --- a/web/apps/photos/public/locales/ko-KR/translation.json +++ b/web/apps/photos/public/locales/ko-KR/translation.json @@ -1,83 +1,81 @@ { - "HERO_SLIDE_1_TITLE": "", - "HERO_SLIDE_1": "", - "HERO_SLIDE_2_TITLE": "", - "HERO_SLIDE_2": "", - "HERO_SLIDE_3_TITLE": "", - "HERO_SLIDE_3": "", - "LOGIN": "", - "SIGN_UP": "", - "NEW_USER": "", - "EXISTING_USER": "", - "ENTER_NAME": "", - "PUBLIC_UPLOADER_NAME_MESSAGE": "", - "ENTER_EMAIL": "", - "EMAIL_ERROR": "", - "REQUIRED": "", - "EMAIL_SENT": "", - "CHECK_INBOX": "", - "ENTER_OTT": "", - "RESEND_MAIL": "", - "VERIFY": "", - "UNKNOWN_ERROR": "", - "INVALID_CODE": "", - "EXPIRED_CODE": "", - "SENDING": "", - "SENT": "", - "PASSWORD": "", - "LINK_PASSWORD": "", - "RETURN_PASSPHRASE_HINT": "", - "SET_PASSPHRASE": "", - "VERIFY_PASSPHRASE": "", - "INCORRECT_PASSPHRASE": "", - "ENTER_ENC_PASSPHRASE": "", - "PASSPHRASE_DISCLAIMER": "", - "WELCOME_TO_ENTE_HEADING": "", - "WELCOME_TO_ENTE_SUBHEADING": "", - "WHERE_YOUR_BEST_PHOTOS_LIVE": "", - "KEY_GENERATION_IN_PROGRESS_MESSAGE": "", - "PASSPHRASE_HINT": "", - "CONFIRM_PASSPHRASE": "", - "REFERRAL_CODE_HINT": "", - "REFERRAL_INFO": "", - "PASSPHRASE_MATCH_ERROR": "", - "CONSOLE_WARNING_STOP": "", - "CONSOLE_WARNING_DESC": "", - "CREATE_COLLECTION": "", - "ENTER_ALBUM_NAME": "", - "CLOSE_OPTION": "", - "ENTER_FILE_NAME": "", - "CLOSE": "", - "NO": "", - "NOTHING_HERE": "", - "UPLOAD": "", - "IMPORT": "", - "ADD_PHOTOS": "", - "ADD_MORE_PHOTOS": "", - "add_photos_one": "", - "add_photos_other": "", - "SELECT_PHOTOS": "", - "FILE_UPLOAD": "", + "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": "기존 사용자", + "ENTER_NAME": "이름 입력", + "PUBLIC_UPLOADER_NAME_MESSAGE": "친구들이 이 멋진 사진에 대해 고마워할 수 있도록 이름을 추가하세요!", + "ENTER_EMAIL": "이메일 주소를 입력하세요", + "EMAIL_ERROR": "올바른 이메일을 입력하세요", + "REQUIRED": "필수", + "EMAIL_SENT": "{{email}} 로 인증 코드가 전송되었습니다", + "CHECK_INBOX": "인증을 완료하기 위해 당신의 메일 수신함(그리고 스팸 수신함)을 확인하세요.", + "ENTER_OTT": "인증 코드", + "RESEND_MAIL": "코드 재전송하기", + "VERIFY": "인증", + "UNKNOWN_ERROR": "문제가 생긴 것 같아요. 다시 시도하세요", + "INVALID_CODE": "잘못된 인증 코드", + "EXPIRED_CODE": "입력한 인증 코드가 만료되었습니다", + "SENDING": "전송 중...", + "SENT": "발송 완료!", + "PASSWORD": "비밀번호", + "LINK_PASSWORD": "앨범 잠금해제를 위해 비밀번호를 입력하세요", + "RETURN_PASSPHRASE_HINT": "비밀번호", + "SET_PASSPHRASE": "비밀번호 설정", + "VERIFY_PASSPHRASE": "로그인", + "INCORRECT_PASSPHRASE": "잘못된 비밀번호입니다", + "ENTER_ENC_PASSPHRASE": "당신의 데이터를 암호화하는 데 사용할 수 있는 비밀번호를 입력하세요", + "PASSPHRASE_DISCLAIMER": "우리는 귀하의 비밀번호를 저장하지 않습니다. 만약 비밀번호를 잊어버린 경우 복구 키 없다면 데이터 복구를 도와드릴 수 없습니다.", + "WELCOME_TO_ENTE_HEADING": "환영합니다 ", + "WELCOME_TO_ENTE_SUBHEADING": "End-to-End 암호화된 사진 저장 및 공유", + "WHERE_YOUR_BEST_PHOTOS_LIVE": "당신 최고의 사진이 있는 곳", + "KEY_GENERATION_IN_PROGRESS_MESSAGE": "암호 키 생성 중...", + "PASSPHRASE_HINT": "비밀번호", + "CONFIRM_PASSPHRASE": "비밀번호 확인", + "REFERRAL_CODE_HINT": "어떻게 Ente에 대해 들으셨나요? (선택사항)", + "REFERRAL_INFO": "우리는 앱 설치를 추적하지 않습니다. 우리를 알게 된 곳을 남겨주시면 우리에게 도움이 될꺼에요!", + "PASSPHRASE_MATCH_ERROR": "비밀번호가 일치하지 않습니다", + "CREATE_COLLECTION": "새 앨범", + "ENTER_ALBUM_NAME": "앨범 이름", + "CLOSE_OPTION": "닫기 (Esc)", + "ENTER_FILE_NAME": "파일 이름", + "CLOSE": "닫기", + "NO": "아니오", + "NOTHING_HERE": "아직 볼 수 있는 것이 없어요 👀", + "UPLOAD": "업로드", + "IMPORT": "가져오기", + "ADD_PHOTOS": "사진 추가", + "ADD_MORE_PHOTOS": "사진 더 추가하기", + "add_photos_one": "아이템 하나 추가", + "add_photos_other": "아이템 {{count, number}} 개 추가하기", + "SELECT_PHOTOS": "사진 선택하기", + "FILE_UPLOAD": "파일 업로드", "UPLOAD_STAGE_MESSAGE": { - "0": "", - "1": "", - "2": "", - "3": "", - "4": "", - "5": "" + "0": "업로드 준비중", + "1": "구글 메타데이타 파일들 읽는중", + "2": "{{uploadCounter.finished, number}} / {{uploadCounter.total, number}} 파일 메타데이터가 추출되었습니다", + "3": "{{uploadCounter.finished, number}} / {{uploadCounter.total, number}} 파일이 처리되었습니다", + "4": "남은 업로드 취소중", + "5": "백업 완료" }, - "FILE_NOT_UPLOADED_LIST": "", - "SUBSCRIPTION_EXPIRED": "", - "SUBSCRIPTION_EXPIRED_MESSAGE": "", - "STORAGE_QUOTA_EXCEEDED": "", - "INITIAL_LOAD_DELAY_WARNING": "", - "USER_DOES_NOT_EXIST": "", - "NO_ACCOUNT": "", - "ACCOUNT_EXISTS": "", - "CREATE": "", - "DOWNLOAD": "", - "DOWNLOAD_OPTION": "", - "DOWNLOAD_FAVORITES": "", + "FILE_NOT_UPLOADED_LIST": "아래 파일들은 업로드 되지 않았습니다", + "SUBSCRIPTION_EXPIRED": "구독 만료", + "SUBSCRIPTION_EXPIRED_MESSAGE": "당신 구독이 만료되었으니, 구독을 갱신해주세요", + "STORAGE_QUOTA_EXCEEDED": "스토리지 제한이 초과되었습니다", + "INITIAL_LOAD_DELAY_WARNING": "처음 로딩시 다소 시간이 걸릴 수 있습니다", + "USER_DOES_NOT_EXIST": "죄송합니다. 해당 이메일을 사용하는 사용자를 찾을 수 없습니다", + "NO_ACCOUNT": "계정이 없습니다", + "ACCOUNT_EXISTS": "이미 계정이 있습니다", + "CREATE": "만들기", + "DOWNLOAD": "다운로드", + "DOWNLOAD_OPTION": "다운로드 (D)", + "DOWNLOAD_FAVORITES": "즐겨찾기 다운로드", "DOWNLOAD_UNCATEGORIZED": "", "DOWNLOAD_HIDDEN_ITEMS": "", "COPY_OPTION": "", @@ -590,7 +588,6 @@ "DOWNLOADING_COLLECTION": "", "DOWNLOAD_FAILED": "", "DOWNLOAD_PROGRESS": "", - "CRASH_REPORTING": "", "CHRISTMAS": "", "CHRISTMAS_EVE": "", "NEW_YEAR": "", diff --git a/web/apps/photos/public/locales/nl-NL/translation.json b/web/apps/photos/public/locales/nl-NL/translation.json index df869b0dd..2b2aa1111 100644 --- a/web/apps/photos/public/locales/nl-NL/translation.json +++ b/web/apps/photos/public/locales/nl-NL/translation.json @@ -41,8 +41,6 @@ "REFERRAL_CODE_HINT": "Hoe hoorde je over Ente? (optioneel)", "REFERRAL_INFO": "Wij gebruiken geen tracking. Het zou helpen als je ons vertelt waar je ons gevonden hebt!", "PASSPHRASE_MATCH_ERROR": "Wachtwoorden komen niet overeen", - "CONSOLE_WARNING_STOP": "STOP!", - "CONSOLE_WARNING_DESC": "Dit is een browserfunctie bedoeld voor ontwikkelaars. Gelieve hier geen niet-geverifieerde code te kopiëren/plakken.", "CREATE_COLLECTION": "Nieuw album", "ENTER_ALBUM_NAME": "Album naam", "CLOSE_OPTION": "Sluiten (Esc)", @@ -590,7 +588,6 @@ "DOWNLOADING_COLLECTION": "{{name}} downloaden", "DOWNLOAD_FAILED": "Download mislukt", "DOWNLOAD_PROGRESS": "{{progress.current}} / {{progress.total}} bestanden", - "CRASH_REPORTING": "Foutenrapportering", "CHRISTMAS": "Kerst", "CHRISTMAS_EVE": "Kerstavond", "NEW_YEAR": "Nieuwjaar", diff --git a/web/apps/photos/public/locales/pt-BR/translation.json b/web/apps/photos/public/locales/pt-BR/translation.json index 5145a24aa..4eaa398c4 100644 --- a/web/apps/photos/public/locales/pt-BR/translation.json +++ b/web/apps/photos/public/locales/pt-BR/translation.json @@ -41,8 +41,6 @@ "REFERRAL_CODE_HINT": "Como você ouviu sobre o Ente? (opcional)", "REFERRAL_INFO": "Não rastreamos instalações do aplicativo. Seria útil se você nos contasse onde nos encontrou!", "PASSPHRASE_MATCH_ERROR": "As senhas não coincidem", - "CONSOLE_WARNING_STOP": "PARAR!", - "CONSOLE_WARNING_DESC": "Este é um recurso de navegador destinado a desenvolvedores. Por favor, não copie e cole o código não confirmado aqui.", "CREATE_COLLECTION": "Novo álbum", "ENTER_ALBUM_NAME": "Nome do álbum", "CLOSE_OPTION": "Fechar (Esc)", @@ -590,7 +588,6 @@ "DOWNLOADING_COLLECTION": "Transferindo {{name}}", "DOWNLOAD_FAILED": "Falha ao baixar", "DOWNLOAD_PROGRESS": "{{progress.current}} / {{progress.total}} arquivos", - "CRASH_REPORTING": "Relatório de falhas", "CHRISTMAS": "Natal", "CHRISTMAS_EVE": "Véspera de Natal", "NEW_YEAR": "Ano Novo", diff --git a/web/apps/photos/public/locales/pt-PT/translation.json b/web/apps/photos/public/locales/pt-PT/translation.json index fb33bb972..ac7d17968 100644 --- a/web/apps/photos/public/locales/pt-PT/translation.json +++ b/web/apps/photos/public/locales/pt-PT/translation.json @@ -41,8 +41,6 @@ "REFERRAL_CODE_HINT": "", "REFERRAL_INFO": "", "PASSPHRASE_MATCH_ERROR": "", - "CONSOLE_WARNING_STOP": "PARAR!", - "CONSOLE_WARNING_DESC": "", "CREATE_COLLECTION": "Novo álbum", "ENTER_ALBUM_NAME": "Nome do álbum", "CLOSE_OPTION": "Fechar (Esc)", @@ -590,7 +588,6 @@ "DOWNLOADING_COLLECTION": "", "DOWNLOAD_FAILED": "", "DOWNLOAD_PROGRESS": "", - "CRASH_REPORTING": "", "CHRISTMAS": "", "CHRISTMAS_EVE": "", "NEW_YEAR": "", diff --git a/web/apps/photos/public/locales/ru-RU/translation.json b/web/apps/photos/public/locales/ru-RU/translation.json index d551758ad..0e9715563 100644 --- a/web/apps/photos/public/locales/ru-RU/translation.json +++ b/web/apps/photos/public/locales/ru-RU/translation.json @@ -41,8 +41,6 @@ "REFERRAL_CODE_HINT": "Как вы узнали о Ente? (необязательно)", "REFERRAL_INFO": "Будет полезно, если вы укажете, где нашли нас, так как мы не отслеживаем установки приложения!", "PASSPHRASE_MATCH_ERROR": "Пароли не совпадают", - "CONSOLE_WARNING_STOP": "Остановись!", - "CONSOLE_WARNING_DESC": "Это функция браузера, предназначенная для разработчиков. Пожалуйста, не копируйте и не вставляйте сюда непроверенный код.", "CREATE_COLLECTION": "Новый альбом", "ENTER_ALBUM_NAME": "Название альбома", "CLOSE_OPTION": "Закрыть (Esc)", @@ -590,7 +588,6 @@ "DOWNLOADING_COLLECTION": "Загрузка {{name}}", "DOWNLOAD_FAILED": "Загрузка не удалась", "DOWNLOAD_PROGRESS": "{{progress.current}} / {{progress.total}} файлов", - "CRASH_REPORTING": "Отчеты об ошибках", "CHRISTMAS": "Рождество", "CHRISTMAS_EVE": "Канун Рождества", "NEW_YEAR": "Новый год", diff --git a/web/apps/photos/public/locales/sv-SE/translation.json b/web/apps/photos/public/locales/sv-SE/translation.json index bc335bc77..550780b44 100644 --- a/web/apps/photos/public/locales/sv-SE/translation.json +++ b/web/apps/photos/public/locales/sv-SE/translation.json @@ -9,9 +9,9 @@ "SIGN_UP": "", "NEW_USER": "", "EXISTING_USER": "", - "ENTER_NAME": "", + "ENTER_NAME": "Ange namn", "PUBLIC_UPLOADER_NAME_MESSAGE": "", - "ENTER_EMAIL": "", + "ENTER_EMAIL": "Ange e-postadress", "EMAIL_ERROR": "", "REQUIRED": "", "EMAIL_SENT": "", @@ -24,31 +24,29 @@ "EXPIRED_CODE": "", "SENDING": "", "SENT": "", - "PASSWORD": "", + "PASSWORD": "Lösenord", "LINK_PASSWORD": "", - "RETURN_PASSPHRASE_HINT": "", + "RETURN_PASSPHRASE_HINT": "Lösenord", "SET_PASSPHRASE": "", - "VERIFY_PASSPHRASE": "", + "VERIFY_PASSPHRASE": "Logga in", "INCORRECT_PASSPHRASE": "", "ENTER_ENC_PASSPHRASE": "", "PASSPHRASE_DISCLAIMER": "", - "WELCOME_TO_ENTE_HEADING": "", + "WELCOME_TO_ENTE_HEADING": "Välkommen till ", "WELCOME_TO_ENTE_SUBHEADING": "", "WHERE_YOUR_BEST_PHOTOS_LIVE": "", "KEY_GENERATION_IN_PROGRESS_MESSAGE": "", - "PASSPHRASE_HINT": "", - "CONFIRM_PASSPHRASE": "", + "PASSPHRASE_HINT": "Lösenord", + "CONFIRM_PASSPHRASE": "Bekräfta lösenord", "REFERRAL_CODE_HINT": "", "REFERRAL_INFO": "", - "PASSPHRASE_MATCH_ERROR": "", - "CONSOLE_WARNING_STOP": "", - "CONSOLE_WARNING_DESC": "", + "PASSPHRASE_MATCH_ERROR": "Lösenorden matchar inte", "CREATE_COLLECTION": "", "ENTER_ALBUM_NAME": "", "CLOSE_OPTION": "", - "ENTER_FILE_NAME": "", - "CLOSE": "", - "NO": "", + "ENTER_FILE_NAME": "Filnamn", + "CLOSE": "Stäng", + "NO": "Nej", "NOTHING_HERE": "", "UPLOAD": "", "IMPORT": "", @@ -96,31 +94,31 @@ "TRASH_FILE_TITLE": "", "DELETE_FILES_TITLE": "", "DELETE_FILES_MESSAGE": "", - "DELETE": "", + "DELETE": "Radera", "DELETE_OPTION": "", "FAVORITE_OPTION": "", "UNFAVORITE_OPTION": "", "MULTI_FOLDER_UPLOAD": "", "UPLOAD_STRATEGY_CHOICE": "", "UPLOAD_STRATEGY_SINGLE_COLLECTION": "", - "OR": "", + "OR": "eller", "UPLOAD_STRATEGY_COLLECTION_PER_FOLDER": "", "SESSION_EXPIRED_MESSAGE": "", "SESSION_EXPIRED": "", "PASSWORD_GENERATION_FAILED": "", - "CHANGE_PASSWORD": "", + "CHANGE_PASSWORD": "Ändra lösenord", "GO_BACK": "", - "RECOVERY_KEY": "", + "RECOVERY_KEY": "Återställningsnyckel", "SAVE_LATER": "", - "SAVE": "", + "SAVE": "Spara nyckel", "RECOVERY_KEY_DESCRIPTION": "", "RECOVER_KEY_GENERATION_FAILED": "", "KEY_NOT_STORED_DISCLAIMER": "", - "FORGOT_PASSWORD": "", + "FORGOT_PASSWORD": "Glömt lösenord", "RECOVER_ACCOUNT": "", "RECOVERY_KEY_HINT": "", "RECOVER": "", - "NO_RECOVERY_KEY": "", + "NO_RECOVERY_KEY": "Ingen återställningsnyckel?", "INCORRECT_RECOVERY_KEY": "", "SORRY": "", "NO_RECOVERY_KEY_MESSAGE": "", @@ -128,30 +126,30 @@ "CONTACT_SUPPORT": "", "REQUEST_FEATURE": "", "SUPPORT": "", - "CONFIRM": "", - "CANCEL": "", - "LOGOUT": "", - "DELETE_ACCOUNT": "", + "CONFIRM": "Bekräfta", + "CANCEL": "Avbryt", + "LOGOUT": "Logga ut", + "DELETE_ACCOUNT": "Radera konto", "DELETE_ACCOUNT_MESSAGE": "", "LOGOUT_MESSAGE": "", "CHANGE_EMAIL": "", "OK": "", "SUCCESS": "", "ERROR": "", - "MESSAGE": "", + "MESSAGE": "Meddelande", "INSTALL_MOBILE_APP": "", "DOWNLOAD_APP_MESSAGE": "", "DOWNLOAD_APP": "", "EXPORT": "", "SUBSCRIPTION": "", - "SUBSCRIBE": "", - "MANAGEMENT_PORTAL": "", + "SUBSCRIBE": "Prenumerera", + "MANAGEMENT_PORTAL": "Hantera betalningsmetod", "MANAGE_FAMILY_PORTAL": "", "LEAVE_FAMILY_PLAN": "", "LEAVE": "", "LEAVE_FAMILY_CONFIRM": "", "CHOOSE_PLAN": "", - "MANAGE_PLAN": "", + "MANAGE_PLAN": "Hantera din prenumeration", "ACTIVE": "", "OFFLINE_MSG": "", "FREE_SUBSCRIPTION_INFO": "", @@ -203,18 +201,18 @@ "DOWNLOAD_COLLECTION": "", "DOWNLOAD_COLLECTION_MESSAGE": "", "CREATE_ALBUM_FAILED": "", - "SEARCH": "", - "SEARCH_RESULTS": "", - "NO_RESULTS": "", + "SEARCH": "Sök", + "SEARCH_RESULTS": "Sökresultat", + "NO_RESULTS": "Inga resultat hittades", "SEARCH_HINT": "", "SEARCH_TYPE": { "COLLECTION": "", "LOCATION": "", "CITY": "", - "DATE": "", + "DATE": "Datum", "FILE_NAME": "", "THING": "", - "FILE_CAPTION": "", + "FILE_CAPTION": "Beskrivning", "FILE_TYPE": "", "CLIP": "" }, @@ -369,11 +367,11 @@ "shared_with_people_zero": "", "shared_with_people_one": "", "shared_with_people_other": "", - "participants_zero": "", - "participants_one": "", + "participants_zero": "Inga deltagare", + "participants_one": "1 deltagare", "participants_other": "", "ADD_VIEWERS": "", - "PARTICIPANTS": "", + "PARTICIPANTS": "Deltagare", "CHANGE_PERMISSIONS_TO_VIEWER": "", "CHANGE_PERMISSIONS_TO_COLLABORATOR": "", "CONVERT_TO_VIEWER": "", @@ -416,14 +414,14 @@ "PASSWORD_LOCK": "", "LOCK": "", "DOWNLOAD_UPLOAD_LOGS": "", - "UPLOAD_FILES": "", - "UPLOAD_DIRS": "", + "UPLOAD_FILES": "Fil", + "UPLOAD_DIRS": "Mapp", "UPLOAD_GOOGLE_TAKEOUT": "", "DEDUPLICATE_FILES": "", "AUTHENTICATOR_SECTION": "", "NO_DUPLICATES_FOUND": "", "CLUB_BY_CAPTURE_TIME": "", - "FILES": "", + "FILES": "Filer", "EACH": "", "DEDUPLICATE_BASED_ON_SIZE": "", "STOP_ALL_UPLOADS_MESSAGE": "", @@ -432,8 +430,8 @@ "STOP_DOWNLOADS_HEADER": "", "YES_STOP_DOWNLOADS": "", "STOP_ALL_DOWNLOADS_MESSAGE": "", - "albums_one": "", - "albums_other": "", + "albums_one": "1 album", + "albums_other": "{{count, number}} album", "ALL_ALBUMS": "", "ALBUMS": "", "ALL_HIDDEN_ALBUMS": "", @@ -459,19 +457,19 @@ "FOLDERS_AUTOMATICALLY_MONITORED": "", "UPLOAD_NEW_FILES_TO_ENTE": "", "REMOVE_DELETED_FILES_FROM_ENTE": "", - "ADD_FOLDER": "", + "ADD_FOLDER": "Lägg till mapp", "STOP_WATCHING": "", "STOP_WATCHING_FOLDER": "", "STOP_WATCHING_DIALOG_MESSAGE": "", "YES_STOP": "", - "MONTH_SHORT": "", - "YEAR": "", + "MONTH_SHORT": "mån", + "YEAR": "år", "FAMILY_PLAN": "", "DOWNLOAD_LOGS": "", "DOWNLOAD_LOGS_MESSAGE": "", "CHANGE_FOLDER": "", "TWO_MONTHS_FREE": "", - "GB": "", + "GB": "GB", "POPULAR": "", "FREE_PLAN_OPTION_LABEL": "", "FREE_PLAN_DESCRIPTION": "", @@ -492,7 +490,7 @@ "IGNORE_THIS_VERSION": "", "TODAY": "", "YESTERDAY": "", - "NAME_PLACEHOLDER": "", + "NAME_PLACEHOLDER": "Namn...", "ROOT_LEVEL_FILE_WITH_FOLDER_NOT_ALLOWED": "", "ROOT_LEVEL_FILE_WITH_FOLDER_NOT_ALLOWED_MESSAGE": "", "CHOSE_THEME": "", @@ -514,7 +512,7 @@ "PASSPHRASE_STRENGTH_MODERATE": "", "PASSPHRASE_STRENGTH_STRONG": "", "PREFERENCES": "", - "LANGUAGE": "", + "LANGUAGE": "Språk", "EXPORT_DIRECTORY_DOES_NOT_EXIST": "", "EXPORT_DIRECTORY_DOES_NOT_EXIST_MESSAGE": "", "SUBSCRIPTION_VERIFICATION_ERROR": "", @@ -532,8 +530,8 @@ "MONTH": "", "YEAR": "" }, - "COPY_LINK": "", - "DONE": "", + "COPY_LINK": "Kopiera länk", + "DONE": "Klar", "LINK_SHARE_TITLE": "", "REMOVE_LINK": "", "CREATE_PUBLIC_SHARING": "", @@ -579,7 +577,7 @@ "HIDE": "", "UNHIDE": "", "UNHIDE_TO_COLLECTION": "", - "SORT_BY": "", + "SORT_BY": "Sortera efter", "NEWEST_FIRST": "", "OLDEST_FIRST": "", "CONVERSION_FAILED_NOTIFICATION_MESSAGE": "", @@ -590,12 +588,11 @@ "DOWNLOADING_COLLECTION": "", "DOWNLOAD_FAILED": "", "DOWNLOAD_PROGRESS": "", - "CRASH_REPORTING": "", "CHRISTMAS": "", "CHRISTMAS_EVE": "", "NEW_YEAR": "", "NEW_YEAR_EVE": "", - "IMAGE": "", + "IMAGE": "Bild", "VIDEO": "", "LIVE_PHOTO": "", "CONVERT": "", @@ -616,10 +613,10 @@ "SAVE_A_COPY_TO_ENTE": "", "RESTORE_ORIGINAL": "", "TRANSFORM": "", - "COLORS": "", + "COLORS": "Färger", "FLIP": "", "ROTATION": "", - "RESET": "", + "RESET": "Återställ", "PHOTO_EDITOR": "", "FASTER_UPLOAD": "", "FASTER_UPLOAD_DESCRIPTION": "", diff --git a/web/apps/photos/public/locales/th-TH/translation.json b/web/apps/photos/public/locales/th-TH/translation.json index bc335bc77..fbc0b937a 100644 --- a/web/apps/photos/public/locales/th-TH/translation.json +++ b/web/apps/photos/public/locales/th-TH/translation.json @@ -41,8 +41,6 @@ "REFERRAL_CODE_HINT": "", "REFERRAL_INFO": "", "PASSPHRASE_MATCH_ERROR": "", - "CONSOLE_WARNING_STOP": "", - "CONSOLE_WARNING_DESC": "", "CREATE_COLLECTION": "", "ENTER_ALBUM_NAME": "", "CLOSE_OPTION": "", @@ -590,7 +588,6 @@ "DOWNLOADING_COLLECTION": "", "DOWNLOAD_FAILED": "", "DOWNLOAD_PROGRESS": "", - "CRASH_REPORTING": "", "CHRISTMAS": "", "CHRISTMAS_EVE": "", "NEW_YEAR": "", diff --git a/web/apps/photos/public/locales/tr-TR/translation.json b/web/apps/photos/public/locales/tr-TR/translation.json index bc335bc77..fbc0b937a 100644 --- a/web/apps/photos/public/locales/tr-TR/translation.json +++ b/web/apps/photos/public/locales/tr-TR/translation.json @@ -41,8 +41,6 @@ "REFERRAL_CODE_HINT": "", "REFERRAL_INFO": "", "PASSPHRASE_MATCH_ERROR": "", - "CONSOLE_WARNING_STOP": "", - "CONSOLE_WARNING_DESC": "", "CREATE_COLLECTION": "", "ENTER_ALBUM_NAME": "", "CLOSE_OPTION": "", @@ -590,7 +588,6 @@ "DOWNLOADING_COLLECTION": "", "DOWNLOAD_FAILED": "", "DOWNLOAD_PROGRESS": "", - "CRASH_REPORTING": "", "CHRISTMAS": "", "CHRISTMAS_EVE": "", "NEW_YEAR": "", diff --git a/web/apps/photos/public/locales/zh-CN/translation.json b/web/apps/photos/public/locales/zh-CN/translation.json index 15ef565dd..6e513f24b 100644 --- a/web/apps/photos/public/locales/zh-CN/translation.json +++ b/web/apps/photos/public/locales/zh-CN/translation.json @@ -9,7 +9,7 @@ "SIGN_UP": "注册", "NEW_USER": "刚来到 ente", "EXISTING_USER": "现有用户", - "ENTER_NAME": "现有用户", + "ENTER_NAME": "输入名字", "PUBLIC_UPLOADER_NAME_MESSAGE": "请添加一个名字,以便您的朋友知晓该感谢谁拍摄了这些精美的照片!", "ENTER_EMAIL": "请输入电子邮件地址", "EMAIL_ERROR": "请输入有效的电子邮件", @@ -41,8 +41,6 @@ "REFERRAL_CODE_HINT": "您是如何知道Ente的? (可选的)", "REFERRAL_INFO": "我们不跟踪应用程序安装情况,如果您告诉我们您是在哪里找到我们的,将会有所帮助!", "PASSPHRASE_MATCH_ERROR": "两次输入的密码不一致", - "CONSOLE_WARNING_STOP": "停止!", - "CONSOLE_WARNING_DESC": "这是专为开发人员设计的浏览器功能。 请不要在此处复制粘贴未经验证的代码。", "CREATE_COLLECTION": "新建相册", "ENTER_ALBUM_NAME": "相册名称", "CLOSE_OPTION": "关闭 (或按Esc键)", @@ -85,9 +83,9 @@ "ZOOM_IN_OUT": "放大/缩小", "PREVIOUS": "上一个 (←)", "NEXT": "下一个 (→)", - "TITLE_PHOTOS": "ente 照片", - "TITLE_ALBUMS": "ente 照片", - "TITLE_AUTH": "ente 验证器", + "TITLE_PHOTOS": "Ente 照片", + "TITLE_ALBUMS": "Ente 照片", + "TITLE_AUTH": "Ente 验证器", "UPLOAD_FIRST_PHOTO": "上传您的第一张照片", "IMPORT_YOUR_FOLDERS": "导入您的文件夹", "UPLOAD_DROPZONE_MESSAGE": "拖放以备份您的文件", @@ -590,7 +588,6 @@ "DOWNLOADING_COLLECTION": "正在下载 {{name}}", "DOWNLOAD_FAILED": "下载失败", "DOWNLOAD_PROGRESS": "{{progress.current}} / {{progress.total}} 个文件", - "CRASH_REPORTING": "崩溃报告", "CHRISTMAS": "圣诞", "CHRISTMAS_EVE": "平安夜", "NEW_YEAR": "新年", diff --git a/web/apps/photos/sentry.client.config.ts b/web/apps/photos/sentry.client.config.ts deleted file mode 100644 index c43273663..000000000 --- a/web/apps/photos/sentry.client.config.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { initSentry } from "@ente/shared/sentry/config/sentry.config.base"; - -initSentry("https://0f7214c7feb9b1dd2fed5db09b42fa1b@sentry.ente.io/5"); diff --git a/web/apps/photos/sentry.edge.config.ts b/web/apps/photos/sentry.edge.config.ts deleted file mode 100644 index e69de29bb..000000000 diff --git a/web/apps/photos/sentry.properties b/web/apps/photos/sentry.properties deleted file mode 100644 index 27c3a286f..000000000 --- a/web/apps/photos/sentry.properties +++ /dev/null @@ -1,6 +0,0 @@ -# This file is used by the SentryWebpackPlugin to upload sourcemaps when the -# SENTRY_AUTH_TOKEN environment variable is defined. - -defaults.url = https://sentry.ente.io/ -defaults.org = ente -defaults.project = web-photos diff --git a/web/apps/photos/sentry.server.config.ts b/web/apps/photos/sentry.server.config.ts deleted file mode 100644 index e69de29bb..000000000 diff --git a/web/apps/photos/src/components/PhotoList/index.tsx b/web/apps/photos/src/components/PhotoList/index.tsx index 6a752ceb6..e6d955cda 100644 --- a/web/apps/photos/src/components/PhotoList/index.tsx +++ b/web/apps/photos/src/components/PhotoList/index.tsx @@ -718,6 +718,9 @@ export function PhotoList({ }; useEffect(() => { + // Nothing to do here if nothing is selected. + if (!galleryContext.selectedFile) return; + const notSelectedFiles = displayFiles?.filter( (item) => !galleryContext.selectedFile[item.id], ); @@ -777,23 +780,28 @@ export function PhotoList({ listItem: TimeStampListItem, isScrolling: boolean, ) => { + // Enhancement: This logic doesn't work on the shared album screen, the + // galleryContext.selectedFile is always null there. + const haveSelection = (galleryContext.selectedFile?.count ?? 0) > 0; switch (listItem.itemType) { case ITEM_TYPE.TIME: return listItem.dates ? ( listItem.dates .map((item) => [ - - onChangeSelectAllCheckBox(item.date) - } - size="small" - sx={{ pl: 0 }} - disableRipple={true} - /> + {haveSelection && ( + + onChangeSelectAllCheckBox(item.date) + } + size="small" + sx={{ pl: 0 }} + disableRipple={true} + /> + )} {item.date} ,
, @@ -801,17 +809,19 @@ export function PhotoList({ .flat() ) : ( - - onChangeSelectAllCheckBox(listItem.date) - } - size="small" - sx={{ pl: 0 }} - disableRipple={true} - /> + {haveSelection && ( + + onChangeSelectAllCheckBox(listItem.date) + } + size="small" + sx={{ pl: 0 }} + disableRipple={true} + /> + )} {listItem.date} ); diff --git a/web/apps/photos/src/components/Sidebar/Preferences/index.tsx b/web/apps/photos/src/components/Sidebar/Preferences/index.tsx index ec9d61a47..04dc79a13 100644 --- a/web/apps/photos/src/components/Sidebar/Preferences/index.tsx +++ b/web/apps/photos/src/components/Sidebar/Preferences/index.tsx @@ -1,17 +1,10 @@ import ChevronRight from "@mui/icons-material/ChevronRight"; import { Box, DialogProps, Stack } from "@mui/material"; import { EnteDrawer } from "components/EnteDrawer"; +import { EnteMenuItem } from "components/Menu/EnteMenuItem"; import Titlebar from "components/Titlebar"; import { t } from "i18next"; -import isElectron from "is-electron"; import { useState } from "react"; - -import ElectronAPIs from "@ente/shared/electron"; -import { useLocalState } from "@ente/shared/hooks/useLocalState"; -import { logError } from "@ente/shared/sentry"; -import InMemoryStore, { MS_KEYS } from "@ente/shared/storage/InMemoryStore"; -import { LS_KEYS } from "@ente/shared/storage/localStorage"; -import { EnteMenuItem } from "components/Menu/EnteMenuItem"; import AdvancedSettings from "../AdvancedSettings"; import MapSettings from "../MapSetting"; import { LanguageSelector } from "./LanguageSelector"; @@ -19,10 +12,6 @@ import { LanguageSelector } from "./LanguageSelector"; export default function Preferences({ open, onClose, onRootClose }) { const [advancedSettingsView, setAdvancedSettingsView] = useState(false); const [mapSettingsView, setMapSettingsView] = useState(false); - const [optOutOfCrashReports, setOptOutOfCrashReports] = useLocalState( - LS_KEYS.OPT_OUT_OF_CRASH_REPORTS, - false, - ); const openAdvancedSettings = () => setAdvancedSettingsView(true); const closeAdvancedSettings = () => setAdvancedSettingsView(false); @@ -43,23 +32,6 @@ export default function Preferences({ open, onClose, onRootClose }) { } }; - const toggleOptOutOfCrashReports = async () => { - try { - if (isElectron()) { - await ElectronAPIs.updateOptOutOfCrashReports( - !optOutOfCrashReports, - ); - } - setOptOutOfCrashReports(!optOutOfCrashReports); - InMemoryStore.set( - MS_KEYS.OPT_OUT_OF_CRASH_REPORTS, - !optOutOfCrashReports, - ); - } catch (e) { - logError(e, "toggleOptOutOfCrashReports failed"); - } - }; - return ( - - } diff --git a/web/apps/photos/src/components/Sidebar/UtilitySection.tsx b/web/apps/photos/src/components/Sidebar/UtilitySection.tsx index 3acd0326a..bf9782e3b 100644 --- a/web/apps/photos/src/components/Sidebar/UtilitySection.tsx +++ b/web/apps/photos/src/components/Sidebar/UtilitySection.tsx @@ -134,11 +134,13 @@ export default function UtilitySection({ closeSidebar }) { label={t("TWO_FACTOR")} /> - + {isInternalUser() && ( + + )} setOffline(true); const resetSharedFiles = () => setSharedFiles(null); - useEffect(() => { - if (isI18nReady) { - console.log( - `%c${t("CONSOLE_WARNING_STOP")}`, - "color: red; font-size: 52px;", - ); - console.log(`%c${t("CONSOLE_WARNING_DESC")}`, "font-size: 20px;"); - } - }, [isI18nReady]); - useEffect(() => { const redirectTo = async (redirect) => { if ( diff --git a/web/apps/photos/src/pages/_error.tsx b/web/apps/photos/src/pages/_error.tsx deleted file mode 100644 index bf1bb89be..000000000 --- a/web/apps/photos/src/pages/_error.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { APPS } from "@ente/shared/apps/constants"; -import ErrorPage from "@ente/shared/next/pages/_error"; -import { useRouter } from "next/router"; -import { AppContext } from "pages/_app"; -import { useContext } from "react"; - -export default function Error() { - const appContext = useContext(AppContext); - const router = useRouter(); - return ( - - ); -} diff --git a/web/apps/photos/src/services/upload/thumbnailService.ts b/web/apps/photos/src/services/upload/thumbnailService.ts index 8b1cf7a61..74ce23f4c 100644 --- a/web/apps/photos/src/services/upload/thumbnailService.ts +++ b/web/apps/photos/src/services/upload/thumbnailService.ts @@ -1,4 +1,4 @@ -import { CustomError, errorWithContext } from "@ente/shared/error"; +import { CustomError } from "@ente/shared/error"; import { addLogLine } from "@ente/shared/logging"; import { getFileNameSize } from "@ente/shared/logging/web"; import { logError } from "@ente/shared/sentry"; @@ -145,10 +145,9 @@ export async function generateImageThumbnailUsingCanvas( clearTimeout(timeout); resolve(null); } catch (e) { - const err = errorWithContext( - e, - `${CustomError.THUMBNAIL_GENERATION_FAILED} err: ${e}`, - ); + const err = new Error(CustomError.THUMBNAIL_GENERATION_FAILED, { + cause: e, + }); reject(err); } }; diff --git a/web/apps/photos/tests/upload.test.ts b/web/apps/photos/tests/upload.test.ts index dcd16db3c..6e58cf0c2 100644 --- a/web/apps/photos/tests/upload.test.ts +++ b/web/apps/photos/tests/upload.test.ts @@ -100,13 +100,13 @@ const FILE_NAME_TO_JSON_NAME = [ ]; export async function testUpload() { - const jsonPath = process.env.NEXT_PUBLIC_ENTE_TEST_EXPECTED_JSON_PATH; - if (!jsonPath) { + const jsonString = process.env.NEXT_PUBLIC_ENTE_TEST_EXPECTED_JSON; + if (!jsonString) { throw Error( - "Please specify the NEXT_PUBLIC_ENTE_TEST_EXPECTED_JSON_PATH to run the upload tests", + "Please specify the NEXT_PUBLIC_ENTE_TEST_EXPECTED_JSON to run the upload tests", ); } - const expectedState = await import(jsonPath); + const expectedState = JSON.parse(jsonString); if (!expectedState) { throw Error("upload test failed expectedState missing"); } diff --git a/web/docs/README.md b/web/docs/README.md index e826bdfcc..365d3bea0 100644 --- a/web/docs/README.md +++ b/web/docs/README.md @@ -1,8 +1,9 @@ -## Developer docs +# Developer docs -If you just want to run ente locally or develop on it, you can do +If you just want to run Ente's web apps locally or develop them, you can do - yarn + yarn install yarn dev -The docs in this directory are for more advanced or infrequently needed details. +The docs in this directory provide more details that some developers might find +useful. diff --git a/web/docs/dependencies.md b/web/docs/dependencies.md index aad328c9a..8f5ace071 100644 --- a/web/docs/dependencies.md +++ b/web/docs/dependencies.md @@ -45,6 +45,8 @@ of React, but those are contingent and can be replaced, or even removed. But the usage of React is deep rooted. React also has a sibling "react-dom" package that renders "React" interfaces to the DOM. +### MUI and Emotion + Currently, we use MUI ("@mui/material"), which is a React component library, to get a base set of components. MUI uses Emotion (a styled-component variant) as its preferred CSS-in-JS library and to keep things simple, that's also what we @@ -66,3 +68,18 @@ Emotion itself comes in many parts, of which we need the following three: > use it explicitly, it's a peer dependency of `@mui/material`. * "@emotion/server" + +### Translations + +For showing the app's UI in multiple languages, we use the i18next library, +specifically its three components + +* "i18next": The core `i18next` library. +* "i18next-http-backend": Adds support for initializing `i18next` with JSON file + containing the translation in a particular language, fetched at runtime. +* "react-i18next": React specific support in `i18next`. + +Note that inspite of the "next" in the name of the library, it has nothing to do +with Next.js. + +For more details, see [translations.md](translations.md). diff --git a/web/docs/deploy.md b/web/docs/deploy.md index 55d132da0..c17de8ffa 100644 --- a/web/docs/deploy.md +++ b/web/docs/deploy.md @@ -1,40 +1,51 @@ -# Deploying the web apps +# Deploying -## tl;dr; +The various web apps and static sites in this repository are deployed on +Cloudflare Pages. -```sh -yarn deploy:photos -``` +* Production deployments are triggered by pushing to the `deploy/*` branches. -## Details +* [help.ente.io](https://help.ente.io) gets deployed whenever a PR that changes + anything inside `docs/` gets merged to `main`. -The various web apps (Ente Photos, Ente Auth) are deployed on Cloudflare Pages. +* Every night, all the web apps get automatically deployed to a nightly preview + URLs (`*.ente.sh`) using the current code in main. -The deployment is done using the GitHub app provided by Cloudflare Pages. The -Cloudflare integration watches for pushes to all branches named "deploy/*". In -all cases, it runs the same script, `scripts/deploy.sh`, using the -`CF_PAGES_BRANCH` environment variable to decide what exactly to build ([CF -docs](https://developers.cloudflare.com/pages/how-to/build-commands-branches/)). +* A preview deployment can be made by triggering the "Preview (web)" workflow. + This allows us to deploy a build of any of the apps from an arbitrary branch + to [preview.ente.sh](https://preview.ente.sh). -For each of these branches, we have configured CNAME aliases (Cloudflare calls -them Custom Domains) to give a stable URL to the deployments. +Use the various `yarn deploy:*` commands to help with production deployments. +For example, `yarn deploy:photos` will open a PR to merge the current `main` +onto `deploy/photos`, which'll trigger the deployment workflow, which'll build +and publish to [web.ente.io](https://web.ente.io). -- `deploy/photos` → _web.ente.io_ -- `deploy/auth` → _auth.ente.io_ -- `deploy/accounts` → _accounts.ente.io_ -- `deploy/cast` → _cast.ente.io_ +> When merging these deployment PRs, remember to use rebase and merge so that +> their HEAD is a fast forward of `main` instead of diverging from it because of +> the merge commit. -Thus to trigger a, say, production deployment of the photos app, we can open and -merge a PR into the `deploy/photos` branch. Cloudflare will then build and -deploy the code to _web.ente.io_. +## Deployments -The command `yarn deploy:photos` just does that - it'll open a new PR to fast -forward the current main onto `deploy/photos`. There are similar `yarn deploy:*` -commands for the other apps. +Here is a list of all the deployments, whether or not they are production +deployments, and the action that triggers them: -## Other subdomains +| URL | Type |Deployment action | +|-----|------|------------------| +| [web.ente.io](https://web.ente.io) | Production | Push to `deploy/photos` | +| [photos.ente.io](https://photos.ente.io) | Production | Alias of [web.ente.io](https://web.ente.io) | +| [auth.ente.io](https://auth.ente.io) | Production | Push to `deploy/auth` | +| [accounts.ente.io](https://accounts.ente.io) | Production | Push to `deploy/accounts` | +| [cast.ente.io](https://cast.ente.io) | Production | Push to `deploy/cast` | +| [help.ente.io](https://help.ente.io) | Production | Push to `main` + changes in `docs/` | +| [accounts.ente.sh](https://accounts.ente.sh) | Preview | Nightly deploy of `main` | +| [auth.ente.sh](https://auth.ente.sh) | Preview | Nightly deploy of `main` | +| [cast.ente.sh](https://cast.ente.sh) | Preview | Nightly deploy of `main` | +| [photos.ente.sh](https://photos.ente.sh) | Preview | Nightly deploy of `main` | +| [preview.ente.sh](https://preview.ente.sh) | Preview | Manually triggered | -Apart from this, there are also some subdomains: +### Other subdomains + +Apart from this, there are also some other deployments: - `albums.ente.io` is a CNAME alias to the production deployment (`web.ente.io`). However, when the code detects that it is being served from @@ -44,22 +55,80 @@ Apart from this, there are also some subdomains: - `payments.ente.io` and `family.ente.io` are currently in a separate repositories (Enhancement: bring them in here). -## NODE_VERSION +### Preview deployments -In Cloudflare Pages setting the `NODE_VERSION` environment variables is defined. +To trigger a preview deployment, manually trigger the "Preview (web)" workflow +from the Actions tab on GitHub. You'll need to select the app to build, and the +branch to use. This'll then build the specified app (e.g. "photos") from that +branch, and deploy it to [preview.ente.sh](https://preview.ente.sh). -This determines which version of Node is used when we do `yarn build:foo`. -Currently this is set to `20.11.1`. The major version here should match that of -`@types/node` in our dev dependencies. +The workflow can also be triggered using GitHub's CLI, gh. e.g. -It is a good idea to also use the same major version of node on your machine. -For example, for macOS you can install the the latest from the v20 series using -`brew install node@20`. +```sh +gh workflow run web-preview -F app=cast --ref my-branch +``` -## Adding a new app +--- -1. Add a mapping in `scripts/deploy.sh`. +## Details + +The rest of the document describes details about how things were setup. You +likely don't need to know them to be able to deploy. + +## First time preparation + +Create a new Pages project in Cloudflare, setting it up to use [Direct +Upload](https://developers.cloudflare.com/pages/get-started/direct-upload/). + +> [!NOTE] +> +> Direct upload doesn't work for existing projects tied to your repository using +> the [Git +> integration](https://developers.cloudflare.com/pages/get-started/git-integration/). +> +> If you want to keep the pages.dev domain from an existing project, you should +> be able to delete your existing project and recreate it (assuming no one +> claims the domain in the middle). I've not seen this documented anywhere, but +> it worked when I tried, and it seems to have worked for [other people +> too](https://community.cloudflare.com/t/linking-git-repo-to-existing-cf-pages-project/530888). + + +There are two ways to create a new project, using Wrangler +[[1](https://github.com/cloudflare/pages-action/issues/51)] or using the +Cloudflare dashboard +[[2](https://github.com/cloudflare/pages-action/issues/115)]. Since this is one +time thing, the second option might be easier. + +The remaining steps are documented in [Cloudflare's guide for using Direct +Upload with +CI](https://developers.cloudflare.com/pages/how-to/use-direct-upload-with-continuous-integration/). +As a checklist, + +- Generate `CLOUDFLARE_API_TOKEN` +- Add `CLOUDFLARE_ACCOUNT_ID` and `CLOUDFLARE_API_TOKEN` to the GitHub secrets +- Add your workflow. e.g. see `docs-deploy.yml`. + +This is the basic setup, and should already work. + +## Deploying multiple sites + +However, we wish to deploy multiple sites from this same repository, so the +standard Cloudflare conception of a single "production" branch doesn't work for +us. + +Instead, we tie each deployment to a branch name. Note that we don't have to +actually create the branch or push to it, this branch name is just used as the +the `branch` parameter that gets passed to `cloudflare/pages-action`. + +Since our root pages project is `ente.pages.dev`, so a branch named `foo` would +be available at `foo.ente.pages.dev`. + +Finally, we create CNAME aliases using a [Custom Domain in +Cloudflare](https://developers.cloudflare.com/pages/how-to/custom-branch-aliases/) +to point to these deployments from our user facing DNS names. + +As a concrete example, the GitHub workflow that deploys `docs/` passes "help" as +the branch name. The resulting deployment is available at "help.ente.pages.dev". +Finally, we add a custom domain to point to it from +[help.ente.io](https://help.ente.io). -2. Add a [Custom Domain in - Cloudflare](https://developers.cloudflare.com/pages/how-to/custom-branch-aliases/) - pointing to this branch's deployment. diff --git a/web/docs/dev.md b/web/docs/dev.md index 4c7fd1cfb..58841e9e0 100644 --- a/web/docs/dev.md +++ b/web/docs/dev.md @@ -1,3 +1,5 @@ +# Notes for Developers + ## Monorepo The monorepo uses Yarn (classic) workspaces. diff --git a/web/docs/new.md b/web/docs/new.md index a393066fe..4500617b5 100644 --- a/web/docs/new.md +++ b/web/docs/new.md @@ -1,4 +1,4 @@ -## Welcome! +# Welcome! If you're new to this sort of stuff or coming back to it after mobile/backend development, here is a recommended workflow: @@ -13,3 +13,14 @@ development, here is a recommended workflow: `yarn` comes with it. That's it. Enjoy coding! + +## Yarn + +Note that we use Yarn classic + +``` +$ yarn --version +1.22.21 +``` + +You should be seeing a 1.xx.xx version, otherwise your `yarn install` will fail. diff --git a/web/docs/translations.md b/web/docs/translations.md new file mode 100644 index 000000000..fcf838e25 --- /dev/null +++ b/web/docs/translations.md @@ -0,0 +1,45 @@ +# Translations + +We use Crowdin for translations, and the `i18next` library to load these at +runtime. + +Within our project we have the _source_ strings - these are the key value pairs +in the `public/locales/en-US/translation.json` file in each app. + +Volunteers can add a new _translation_ in their language corresponding to each +such source key-value to our [Crowdin +project](https://crowdin.com/project/ente-photos-web). + +Everyday, we run a [GitHub workflow](../../.github/workflows/web-crowdin.yml) +that + +* Uploads sources to Crowdin - So any new key value pair we add in the source + `translation.json` becomes available to translators to translate. + +* Downloads translations from Crowdin - So any new translations that translators + have made on the Crowdin dashboard (for existing sources) will be added to the + corresponding `lang/translation.json`. + +The workflow also uploads existing translations and also downloads new sources +from Crowdin, but these two should be no-ops. + +## Adding a new string + +- Add a new entry in `public/locales/en-US/translation.json` (the **source `translation.json`**). +- Use the new key in code with the `t` function (`import { t } from "i18next"`). +- During the next sync, the workflow will upload this source item to Crowdin's + dashboard, allowing translators to translate it. + +## Updating an existing string + +- Update the existing value for the key in the source `translation.json`. +- During the next sync, the workflow will clear out all the existing + translations so that they can be translated afresh. + +## Deleting an existing string + +- Remove the key value pair from the source `translation.json`. +- During the next sync, the workflow will delete that source item from all + existing translations (both in the Crowdin project and also from the he other + `lang/translation.json` files in the repository). + diff --git a/web/package.json b/web/package.json index bdbce0b42..f7e734651 100644 --- a/web/package.json +++ b/web/package.json @@ -26,7 +26,6 @@ "lint-fix": "yarn prettier --write . && yarn workspaces run eslint --fix ." }, "resolutions": { - "@sentry/cli": "1.75.0", "libsodium": "0.7.9" }, "devDependencies": { diff --git a/web/packages/next/next.config.base.js b/web/packages/next/next.config.base.js index 5439c9443..1f96ee3b7 100644 --- a/web/packages/next/next.config.base.js +++ b/web/packages/next/next.config.base.js @@ -10,19 +10,27 @@ * https://nextjs.org/docs/pages/api-reference/next-config-js */ -const { withSentryConfig } = require("@sentry/nextjs"); const cp = require("child_process"); -const gitSHA = cp - .execSync("git rev-parse --short HEAD", { - cwd: __dirname, - encoding: "utf8", - }) - .trimEnd(); +/** + * Return the current commit ID if we're running inside a git repository. + */ +const gitSHA = () => { + // Allow the command to fail. gitSHA will be an empty string in such cases. + // This allows us to run the build even when we're outside of a git context. + const result = cp + .execSync("git rev-parse --short HEAD 2>/dev/null || true", { + cwd: __dirname, + encoding: "utf8", + }) + .trimEnd(); + // Convert empty strings (e.g. when the `|| true` part of the above execSync + // comes into play) to undefined. + return result ? result : undefined; +}; /** - * The base Next.js config. Before exporting this, we wrap this in - * {@link withSentryConfig}. + * Configuration for the Next.js build * * @type {import("next").NextConfig} */ @@ -44,7 +52,7 @@ const nextConfig = { // Add environment variables to the JavaScript bundle. They will be // available as `process.env.VAR_NAME` to our code. env: { - GIT_SHA: gitSHA, + GIT_SHA: gitSHA(), }, // https://dev.to/marcinwosinek/how-to-add-resolve-fallback-to-webpack-5-in-nextjs-10-i6j @@ -54,33 +62,6 @@ const nextConfig = { } return config; }, - - // Build time Sentry configuration - // https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/ - sentry: { - widenClientFileUpload: true, - disableServerWebpackPlugin: true, - }, }; -const sentryWebpackPluginOptions = { - // The same release value needs to be used both: - // 1. here to create a new release on Sentry and upload sourcemaps to it, - // 2. and when initializing Sentry in the browser (`Sentry.init`). - release: gitSHA, -}; - -// withSentryConfig extends the default Next.js usage of webpack to: -// -// 1. Initialize the SDK on client page load (See `sentry.client.config.ts`) -// -// 2. Upload sourcemaps, using the settings defined in `sentry.properties`. -// -// Sourcemaps are only uploaded to Sentry if SENTRY_AUTH_TOKEN is defined. Note -// that sourcemaps are always generated in the static export; the Sentry Webpack -// plugin behavies as if the `productionBrowserSourceMaps` Next.js configuration -// setting is `true`. -// -// Irritatingly, Sentry insists that we create empty sentry.server.config.ts and -// sentry.edge.config.ts files, even though we are not using those parts. -module.exports = withSentryConfig(nextConfig, sentryWebpackPluginOptions); +module.exports = nextConfig; diff --git a/web/packages/shared/electron/service.ts b/web/packages/shared/electron/service.ts index 2f00b42f4..8e035e632 100644 --- a/web/packages/shared/electron/service.ts +++ b/web/packages/shared/electron/service.ts @@ -12,11 +12,7 @@ import { deserializeToResponse, serializeResponse } from "./worker/utils/proxy"; export interface LimitedElectronAPIs extends Pick< ElectronAPIsType, - | "openDiskCache" - | "deleteDiskCache" - | "getSentryUserID" - | "convertToJPEG" - | "logToDisk" + "openDiskCache" | "deleteDiskCache" | "convertToJPEG" | "logToDisk" > {} class WorkerSafeElectronServiceImpl implements LimitedElectronAPIs { @@ -56,10 +52,6 @@ class WorkerSafeElectronServiceImpl implements LimitedElectronAPIs { return await this.proxiedElectron.deleteDiskCache(cacheName); } - async getSentryUserID() { - await this.ready; - return this.proxiedElectron.getSentryUserID(); - } async convertToJPEG( inputFileData: Uint8Array, filename: string, diff --git a/web/packages/shared/electron/types.ts b/web/packages/shared/electron/types.ts index df5829ab0..34b8ee591 100644 --- a/web/packages/shared/electron/types.ts +++ b/web/packages/shared/electron/types.ts @@ -80,7 +80,6 @@ export interface ElectronAPIsType { ) => void; updateAndRestart: () => void; skipAppUpdate: (version: string) => void; - getSentryUserID: () => Promise; getAppVersion: () => Promise; runFFmpegCmd: ( cmd: string[], @@ -101,7 +100,6 @@ export interface ElectronAPIsType { deleteFolder: (path: string) => Promise; deleteFile: (path: string) => void; rename: (oldPath: string, newPath: string) => Promise; - updateOptOutOfCrashReports: (optOut: boolean) => Promise; computeImageEmbedding: ( model: Model, imageData: Uint8Array, diff --git a/web/packages/shared/electron/worker/client.ts b/web/packages/shared/electron/worker/client.ts index 12b34a4e7..92db7d972 100644 --- a/web/packages/shared/electron/worker/client.ts +++ b/web/packages/shared/electron/worker/client.ts @@ -9,7 +9,6 @@ export interface ProxiedLimitedElectronAPIs { cacheLimitInBytes?: number, ) => Promise; deleteDiskCache: (cacheName: string) => Promise; - getSentryUserID: () => Promise; convertToJPEG: ( inputFileData: Uint8Array, filename: string, @@ -42,10 +41,6 @@ export class WorkerSafeElectronClient implements ProxiedLimitedElectronAPIs { return await ElectronAPIs.deleteDiskCache(cacheName); } - async getSentryUserID() { - return await ElectronAPIs.getSentryUserID(); - } - async convertToJPEG( inputFileData: Uint8Array, filename: string, diff --git a/web/packages/shared/error/index.ts b/web/packages/shared/error/index.ts index 0463c9610..be8f10cea 100644 --- a/web/packages/shared/error/index.ts +++ b/web/packages/shared/error/index.ts @@ -109,15 +109,6 @@ export function handleUploadError(error: any): Error { return parsedError; } -export function errorWithContext(originalError: Error, context: string) { - const errorWithContext = new Error(context); - errorWithContext.stack = - errorWithContext.stack?.split("\n").slice(2, 4).join("\n") + - "\n" + - originalError.stack; - return errorWithContext; -} - export function parseUploadErrorCodes(error: any) { let parsedMessage = null; if (error instanceof ApiError) { diff --git a/web/packages/shared/logging/web.ts b/web/packages/shared/logging/web.ts index e0c36dc89..7c9e7e2ed 100644 --- a/web/packages/shared/logging/web.ts +++ b/web/packages/shared/logging/web.ts @@ -7,7 +7,6 @@ import { setData, } from "@ente/shared/storage/localStorage"; import { addLogLine } from "."; -import { getSentryUserID } from "../sentry/utils"; import { formatDateTimeShort } from "../time/format"; import { ElectronFile } from "../upload/types"; import type { User } from "../user/types"; @@ -75,10 +74,10 @@ export const logStartupMessage = async (appId: string) => { // TODO (MR): Remove the need to lowercase it, change the enum itself. const appIdL = appId.toLowerCase(); const userID = (getData(LS_KEYS.USER) as User)?.id; - const sentryID = await getSentryUserID(); - const buildId = isDevBuild ? "dev" : `git ${process.env.GIT_SHA}`; + const sha = process.env.GIT_SHA; + const buildId = isDevBuild ? "dev " : sha ? `git ${sha} ` : ""; - addLogLine(`ente-${appIdL}-web ${buildId} uid ${userID} sid ${sentryID}`); + addLogLine(`ente-${appIdL}-web ${buildId}uid ${userID}`); }; function getLogs(): Log[] { diff --git a/web/packages/shared/next/pages/_document.tsx b/web/packages/shared/next/pages/_document.tsx index 1bae45068..a18df1a2a 100644 --- a/web/packages/shared/next/pages/_document.tsx +++ b/web/packages/shared/next/pages/_document.tsx @@ -23,10 +23,9 @@ export default function EnteDocument({ emotionStyleTags }: EnteDocumentProps) { - {emotionStyleTags} diff --git a/web/packages/shared/next/pages/_error.tsx b/web/packages/shared/next/pages/_error.tsx deleted file mode 100644 index c0fc2e39f..000000000 --- a/web/packages/shared/next/pages/_error.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import * as Sentry from "@sentry/nextjs"; -import NextErrorComponent from "next/error"; - -const CustomErrorComponent = (props) => ( - -); - -CustomErrorComponent.getInitialProps = async (contextData) => { - // In case this is running in a serverless function, await this in order to give Sentry - // time to send the error before the lambda exits - await Sentry.captureUnderscoreErrorException(contextData); - - // This will contain the status code of the response - return NextErrorComponent.getInitialProps(contextData); -}; - -export default CustomErrorComponent; diff --git a/web/packages/shared/package.json b/web/packages/shared/package.json index b4d4e1671..807fdd7e6 100644 --- a/web/packages/shared/package.json +++ b/web/packages/shared/package.json @@ -5,7 +5,6 @@ "dependencies": { "@/next": "*", "@ente/eslint-config": "*", - "@sentry/nextjs": "7.77.0", "axios": "^1.6.7" } } diff --git a/web/packages/shared/sentry/config/sentry.config.base.ts b/web/packages/shared/sentry/config/sentry.config.base.ts deleted file mode 100644 index d37572de6..000000000 --- a/web/packages/shared/sentry/config/sentry.config.base.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { isDevBuild } from "@/utils/env"; -import { runningInBrowser } from "@ente/shared/platform"; -import { getSentryUserID } from "@ente/shared/sentry/utils"; -import { getHasOptedOutOfCrashReports } from "@ente/shared/storage/localStorage/helpers"; -import * as Sentry from "@sentry/nextjs"; - -export const initSentry = async (dsn: string) => { - // Don't initialize Sentry for dev builds - if (isDevBuild) return; - - // Don't initialize Sentry if the user has opted out of crash reporting - if (optedOut()) return; - - Sentry.init({ - dsn, - release: process.env.GIT_SHA, - attachStacktrace: true, - autoSessionTracking: false, - tunnel: "https://sentry-reporter.ente.io", - beforeSend(event) { - event.request = event.request || {}; - const currentURL = new URL(document.location.href); - currentURL.hash = ""; - event.request.url = currentURL.href; - return event; - }, - integrations: function (i) { - return i.filter(function (i) { - return i.name !== "Breadcrumbs"; - }); - }, - }); - - Sentry.setUser({ id: await getSentryUserID() }); -}; - -/** Return true if the user has opted out of crash reporting */ -const optedOut = () => runningInBrowser() && getHasOptedOutOfCrashReports(); diff --git a/web/packages/shared/sentry/index.ts b/web/packages/shared/sentry/index.ts index 9279c1134..2dae035b7 100644 --- a/web/packages/shared/sentry/index.ts +++ b/web/packages/shared/sentry/index.ts @@ -1,59 +1,27 @@ -import { ApiError, errorWithContext } from "@ente/shared/error"; -import { addLocalLog, addLogLine } from "@ente/shared/logging"; -import { - getSentryUserID, - isErrorUnnecessaryForSentry, -} from "@ente/shared/sentry/utils"; -import InMemoryStore, { MS_KEYS } from "@ente/shared/storage/InMemoryStore"; -import { getHasOptedOutOfCrashReports } from "@ente/shared/storage/localStorage/helpers"; -import * as Sentry from "@sentry/nextjs"; +import { ApiError } from "@ente/shared/error"; +import { addLogLine } from "@ente/shared/logging"; +/** Deprecated: Use `logError` from `@/utils/logging` */ export const logError = async ( error: any, msg: string, info?: Record, skipAddLogLine = false, ) => { - const err = errorWithContext(error, msg); - if (!skipAddLogLine) { - if (error instanceof ApiError) { - addLogLine(`error: ${error?.name} ${error?.message} + if (skipAddLogLine) return; + + if (error instanceof ApiError) { + addLogLine(`error: ${error?.name} ${error?.message} msg: ${msg} errorCode: ${JSON.stringify(error?.errCode)} httpStatusCode: ${JSON.stringify(error?.httpStatusCode)} ${ info ? `info: ${JSON.stringify(info)}` : "" } ${error?.stack}`); - } else { - addLogLine( - `error: ${error?.name} ${error?.message} + } else { + addLogLine( + `error: ${error?.name} ${error?.message} msg: ${msg} ${info ? `info: ${JSON.stringify(info)}` : ""} ${error?.stack}`, - ); - } - } - if (!InMemoryStore.has(MS_KEYS.OPT_OUT_OF_CRASH_REPORTS)) { - const optedOutOfCrashReports = getHasOptedOutOfCrashReports(); - InMemoryStore.set( - MS_KEYS.OPT_OUT_OF_CRASH_REPORTS, - optedOutOfCrashReports, ); } - if (InMemoryStore.get(MS_KEYS.OPT_OUT_OF_CRASH_REPORTS)) { - addLocalLog(() => `skipping sentry error: ${error?.name}`); - return; - } - if (isErrorUnnecessaryForSentry(error)) { - return; - } - - Sentry.captureException(err, { - level: "info", - user: { id: await getSentryUserID() }, - contexts: { - ...(info && { - info: info, - }), - rootCause: { message: error?.message, completeError: error }, - }, - }); }; diff --git a/web/packages/shared/sentry/utils.ts b/web/packages/shared/sentry/utils.ts deleted file mode 100644 index 258db570e..000000000 --- a/web/packages/shared/sentry/utils.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { WorkerSafeElectronService } from "@ente/shared/electron/service"; -import { - getLocalSentryUserID, - setLocalSentryUserID, -} from "@ente/shared/storage/localStorage/helpers"; -import { HttpStatusCode } from "axios"; -import isElectron from "is-electron"; -import { ApiError } from "../error"; - -export async function getSentryUserID() { - if (isElectron()) { - return await WorkerSafeElectronService.getSentryUserID(); - } else { - let anonymizeUserID = getLocalSentryUserID(); - if (!anonymizeUserID) { - anonymizeUserID = makeID(6); - setLocalSentryUserID(anonymizeUserID); - } - return anonymizeUserID; - } -} - -function makeID(length) { - let result = ""; - const characters = - "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; - const charactersLength = characters.length; - for (let i = 0; i < length; i++) { - result += characters.charAt( - Math.floor(Math.random() * charactersLength), - ); - } - return result; -} - -export function isErrorUnnecessaryForSentry(error: any) { - if (error?.message?.includes("Network Error")) { - return true; - } else if ( - error instanceof ApiError && - error.httpStatusCode === HttpStatusCode.Unauthorized - ) { - return true; - } - return false; -} diff --git a/web/packages/shared/storage/InMemoryStore.ts b/web/packages/shared/storage/InMemoryStore.ts index ded73faf0..88e77b869 100644 --- a/web/packages/shared/storage/InMemoryStore.ts +++ b/web/packages/shared/storage/InMemoryStore.ts @@ -1,5 +1,4 @@ export enum MS_KEYS { - OPT_OUT_OF_CRASH_REPORTS = "optOutOfCrashReports", SRP_CONFIGURE_IN_PROGRESS = "srpConfigureInProgress", REDIRECT_URL = "redirectUrl", } diff --git a/web/packages/shared/storage/localStorage/helpers.ts b/web/packages/shared/storage/localStorage/helpers.ts index d8092a56a..95ae280e3 100644 --- a/web/packages/shared/storage/localStorage/helpers.ts +++ b/web/packages/shared/storage/localStorage/helpers.ts @@ -37,18 +37,6 @@ export function setLocalMapEnabled(value: boolean) { setData(LS_KEYS.MAP_ENABLED, { value }); } -export function getHasOptedOutOfCrashReports(): boolean { - return getData(LS_KEYS.OPT_OUT_OF_CRASH_REPORTS)?.value ?? false; -} - -export function getLocalSentryUserID() { - return getData(LS_KEYS.AnonymizedUserID)?.id; -} - -export function setLocalSentryUserID(id: string) { - setData(LS_KEYS.AnonymizedUserID, { id }); -} - export function getLocalReferralSource() { return getData(LS_KEYS.REFERRAL_SOURCE)?.source; } diff --git a/web/packages/shared/storage/localStorage/index.ts b/web/packages/shared/storage/localStorage/index.ts index e40b64457..14901fdd4 100644 --- a/web/packages/shared/storage/localStorage/index.ts +++ b/web/packages/shared/storage/localStorage/index.ts @@ -12,7 +12,6 @@ export enum LS_KEYS { JUST_SIGNED_UP = "justSignedUp", SHOW_BACK_BUTTON = "showBackButton", EXPORT = "export", - AnonymizedUserID = "anonymizedUserID", THUMBNAIL_FIX_STATE = "thumbnailFixState", LIVE_PHOTO_INFO_SHOWN_COUNT = "livePhotoInfoShownCount", LOGS = "logs", @@ -26,7 +25,6 @@ export enum LS_KEYS { MAP_ENABLED = "mapEnabled", SRP_SETUP_ATTRIBUTES = "srpSetupAttributes", SRP_ATTRIBUTES = "srpAttributes", - OPT_OUT_OF_CRASH_REPORTS = "optOutOfCrashReports", CF_PROXY_DISABLED = "cfProxyDisabled", REFERRAL_SOURCE = "referralSource", CLIENT_PACKAGE = "clientPackage", diff --git a/web/packages/utils/logging.ts b/web/packages/utils/logging.ts index b3d7d8567..cd9c4b53b 100644 --- a/web/packages/utils/logging.ts +++ b/web/packages/utils/logging.ts @@ -17,7 +17,7 @@ * * TODO (MR): Currently this is a placeholder function to funnel error logs * through. This needs to do what the existing logError in @ente/shared does, - * but it cannot have a direct Electron/Sentry dependency here. For now, we just + * but it cannot have a direct Electron dependency here. For now, we just * log on the console. */ export const logError = (message: string, e?: unknown) => { @@ -35,5 +35,7 @@ export const logError = (message: string, e?: unknown) => { // For the rest rare cases, use the default string serialization of e. es = String(e); } + + // TODO(MR): Use addLogLine console.error(`${message}: ${es}`); }; diff --git a/web/scripts/deploy.sh b/web/scripts/deploy.sh deleted file mode 100755 index cbce135b9..000000000 --- a/web/scripts/deploy.sh +++ /dev/null @@ -1,51 +0,0 @@ -#!/bin/sh - -# This script is run by the Cloudflare Pages integration when deploying the apps -# in this repository. The app to build is decided based on the value of the -# CF_PAGES_BRANCH environment variable. -# -# The Cloudflare Pages build configuration is set to use `out/` as the build -# output directory, so once we're done building we copy the app specific output -# to `out/` (symlinking didn't work). -# -# Ref: https://developers.cloudflare.com/pages/how-to/build-commands-branches/ -# -# To test this script locally, run -# -# CF_PAGES_BRANCH=deploy/foo ./scripts/deploy.sh -# - -set -o errexit -set -o xtrace - -if test "$(basename $(pwd))" != "web" -then - echo "ERROR: This script should be run from the web directory" - exit 1 -fi - -rm -rf out - -case "$CF_PAGES_BRANCH" in - deploy/accounts) - yarn build:accounts - cp -R apps/accounts/out . - ;; - deploy/auth) - yarn build:auth - cp -R apps/auth/out . - ;; - deploy/cast) - yarn build:cast - cp -R apps/cast/out . - ;; - deploy/photos) - yarn build:photos - cp -R apps/photos/out . - ;; - *) - echo "ERROR: We don't know how to build and deploy a branch named $CF_PAGES_BRANCH." - echo " Maybe you forgot to add a new case in web/scripts/deploy.sh" - exit 1 - ;; -esac diff --git a/web/yarn.lock b/web/yarn.lock index be4cf6488..2f5cbb105 100644 --- a/web/yarn.lock +++ b/web/yarn.lock @@ -302,11 +302,6 @@ wrap-ansi "^8.1.0" wrap-ansi-cjs "npm:wrap-ansi@^7.0.0" -"@jridgewell/sourcemap-codec@^1.4.13": - version "1.4.15" - resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" - integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== - "@mui/base@5.0.0-beta.36": version "5.0.0-beta.36" resolved "https://registry.yarnpkg.com/@mui/base/-/base-5.0.0-beta.36.tgz#29ca2de9d387f6d3943b6f18a84415c43e5f206c" @@ -538,161 +533,11 @@ dependencies: dequal "^2.0.3" -"@rollup/plugin-commonjs@24.0.0": - version "24.0.0" - resolved "https://registry.yarnpkg.com/@rollup/plugin-commonjs/-/plugin-commonjs-24.0.0.tgz#fb7cf4a6029f07ec42b25daa535c75b05a43f75c" - integrity sha512-0w0wyykzdyRRPHOb0cQt14mIBLujfAv6GgP6g8nvg/iBxEm112t3YPPq+Buqe2+imvElTka+bjNlJ/gB56TD8g== - dependencies: - "@rollup/pluginutils" "^5.0.1" - commondir "^1.0.1" - estree-walker "^2.0.2" - glob "^8.0.3" - is-reference "1.2.1" - magic-string "^0.27.0" - -"@rollup/pluginutils@^5.0.1": - version "5.1.0" - resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-5.1.0.tgz#7e53eddc8c7f483a4ad0b94afb1f7f5fd3c771e0" - integrity sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g== - dependencies: - "@types/estree" "^1.0.0" - estree-walker "^2.0.2" - picomatch "^2.3.1" - "@rushstack/eslint-patch@^1.3.3": version "1.7.2" resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.7.2.tgz#2d4260033e199b3032a08b41348ac10de21c47e9" integrity sha512-RbhOOTCNoCrbfkRyoXODZp75MlpiHMgbE5MEBZAnnnLyQNgrigEj4p0lzsMDyc1zVsJDLrivB58tgg3emX0eEA== -"@sentry-internal/tracing@7.77.0": - version "7.77.0" - resolved "https://registry.yarnpkg.com/@sentry-internal/tracing/-/tracing-7.77.0.tgz#f3d82486f8934a955b3dd2aa54c8d29586e42a37" - integrity sha512-8HRF1rdqWwtINqGEdx8Iqs9UOP/n8E0vXUu3Nmbqj4p5sQPA7vvCfq+4Y4rTqZFc7sNdFpDsRION5iQEh8zfZw== - dependencies: - "@sentry/core" "7.77.0" - "@sentry/types" "7.77.0" - "@sentry/utils" "7.77.0" - -"@sentry/browser@7.77.0": - version "7.77.0" - resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-7.77.0.tgz#155440f1a0d3a1bbd5d564c28d6b0c9853a51d72" - integrity sha512-nJ2KDZD90H8jcPx9BysQLiQW+w7k7kISCWeRjrEMJzjtge32dmHA8G4stlUTRIQugy5F+73cOayWShceFP7QJQ== - dependencies: - "@sentry-internal/tracing" "7.77.0" - "@sentry/core" "7.77.0" - "@sentry/replay" "7.77.0" - "@sentry/types" "7.77.0" - "@sentry/utils" "7.77.0" - -"@sentry/cli@1.75.0", "@sentry/cli@^1.74.6": - version "1.75.0" - resolved "https://registry.yarnpkg.com/@sentry/cli/-/cli-1.75.0.tgz#4a5e71b5619cd4e9e6238cc77857c66f6b38d86a" - integrity sha512-vT8NurHy00GcN8dNqur4CMIYvFH3PaKdkX3qllVvi4syybKqjwoz+aWRCvprbYv0knweneFkLt1SmBWqazUMfA== - dependencies: - https-proxy-agent "^5.0.0" - mkdirp "^0.5.5" - node-fetch "^2.6.7" - progress "^2.0.3" - proxy-from-env "^1.1.0" - which "^2.0.2" - -"@sentry/core@7.77.0": - version "7.77.0" - resolved "https://registry.yarnpkg.com/@sentry/core/-/core-7.77.0.tgz#21100843132beeeff42296c8370cdcc7aa1d8510" - integrity sha512-Tj8oTYFZ/ZD+xW8IGIsU6gcFXD/gfE+FUxUaeSosd9KHwBQNOLhZSsYo/tTVf/rnQI/dQnsd4onPZLiL+27aTg== - dependencies: - "@sentry/types" "7.77.0" - "@sentry/utils" "7.77.0" - -"@sentry/integrations@7.77.0": - version "7.77.0" - resolved "https://registry.yarnpkg.com/@sentry/integrations/-/integrations-7.77.0.tgz#f2717e05cb7c69363316ccd34096b2ea07ae4c59" - integrity sha512-P055qXgBHeZNKnnVEs5eZYLdy6P49Zr77A1aWJuNih/EenzMy922GOeGy2mF6XYrn1YJSjEwsNMNsQkcvMTK8Q== - dependencies: - "@sentry/core" "7.77.0" - "@sentry/types" "7.77.0" - "@sentry/utils" "7.77.0" - localforage "^1.8.1" - -"@sentry/nextjs@7.77.0": - version "7.77.0" - resolved "https://registry.yarnpkg.com/@sentry/nextjs/-/nextjs-7.77.0.tgz#036b1c45dd106e01d44967c97985464e108922be" - integrity sha512-8tYPBt5luFjrng1sAMJqNjM9sq80q0jbt6yariADU9hEr7Zk8YqFaOI2/Q6yn9dZ6XyytIRtLEo54kk2AO94xw== - dependencies: - "@rollup/plugin-commonjs" "24.0.0" - "@sentry/core" "7.77.0" - "@sentry/integrations" "7.77.0" - "@sentry/node" "7.77.0" - "@sentry/react" "7.77.0" - "@sentry/types" "7.77.0" - "@sentry/utils" "7.77.0" - "@sentry/vercel-edge" "7.77.0" - "@sentry/webpack-plugin" "1.20.0" - chalk "3.0.0" - resolve "1.22.8" - rollup "2.78.0" - stacktrace-parser "^0.1.10" - -"@sentry/node@7.77.0": - version "7.77.0" - resolved "https://registry.yarnpkg.com/@sentry/node/-/node-7.77.0.tgz#a247452779a5bcb55724457707286e3e4a29dbbe" - integrity sha512-Ob5tgaJOj0OYMwnocc6G/CDLWC7hXfVvKX/ofkF98+BbN/tQa5poL+OwgFn9BA8ud8xKzyGPxGU6LdZ8Oh3z/g== - dependencies: - "@sentry-internal/tracing" "7.77.0" - "@sentry/core" "7.77.0" - "@sentry/types" "7.77.0" - "@sentry/utils" "7.77.0" - https-proxy-agent "^5.0.0" - -"@sentry/react@7.77.0": - version "7.77.0" - resolved "https://registry.yarnpkg.com/@sentry/react/-/react-7.77.0.tgz#9da14e4b21eae4b5a6306d39bb7c42ef0827d2c2" - integrity sha512-Q+htKzib5em0MdaQZMmPomaswaU3xhcVqmLi2CxqQypSjbYgBPPd+DuhrXKoWYLDDkkbY2uyfe4Lp3yLRWeXYw== - dependencies: - "@sentry/browser" "7.77.0" - "@sentry/types" "7.77.0" - "@sentry/utils" "7.77.0" - hoist-non-react-statics "^3.3.2" - -"@sentry/replay@7.77.0": - version "7.77.0" - resolved "https://registry.yarnpkg.com/@sentry/replay/-/replay-7.77.0.tgz#21d242c9cd70a7235237216174873fd140b6eb80" - integrity sha512-M9Ik2J5ekl+C1Och3wzLRZVaRGK33BlnBwfwf3qKjgLDwfKW+1YkwDfTHbc2b74RowkJbOVNcp4m8ptlehlSaQ== - dependencies: - "@sentry-internal/tracing" "7.77.0" - "@sentry/core" "7.77.0" - "@sentry/types" "7.77.0" - "@sentry/utils" "7.77.0" - -"@sentry/types@7.77.0": - version "7.77.0" - resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.77.0.tgz#c5d00fe547b89ccde59cdea59143bf145cee3144" - integrity sha512-nfb00XRJVi0QpDHg+JkqrmEBHsqBnxJu191Ded+Cs1OJ5oPXEW6F59LVcBScGvMqe+WEk1a73eH8XezwfgrTsA== - -"@sentry/utils@7.77.0": - version "7.77.0" - resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-7.77.0.tgz#1f88501f0b8777de31b371cf859d13c82ebe1379" - integrity sha512-NmM2kDOqVchrey3N5WSzdQoCsyDkQkiRxExPaNI2oKQ/jMWHs9yt0tSy7otPBcXs0AP59ihl75Bvm1tDRcsp5g== - dependencies: - "@sentry/types" "7.77.0" - -"@sentry/vercel-edge@7.77.0": - version "7.77.0" - resolved "https://registry.yarnpkg.com/@sentry/vercel-edge/-/vercel-edge-7.77.0.tgz#6a90a869878e4e78803c4331c30aea841fcc6a73" - integrity sha512-ffddPCgxVeAccPYuH5sooZeHBqDuJ9OIhIRYKoDi4TvmwAzWo58zzZWhRpkHqHgIQdQvhLVZ5F+FSQVWnYSOkw== - dependencies: - "@sentry/core" "7.77.0" - "@sentry/types" "7.77.0" - "@sentry/utils" "7.77.0" - -"@sentry/webpack-plugin@1.20.0": - version "1.20.0" - resolved "https://registry.yarnpkg.com/@sentry/webpack-plugin/-/webpack-plugin-1.20.0.tgz#e7add76122708fb6b4ee7951294b521019720e58" - integrity sha512-Ssj1mJVFsfU6vMCOM2d+h+KQR7QHSfeIP16t4l20Uq/neqWXZUQ2yvQfe4S3BjdbJXz/X4Rw8Hfy1Sd0ocunYw== - dependencies: - "@sentry/cli" "^1.74.6" - webpack-sources "^2.0.0 || ^3.0.0" - "@stripe/stripe-js@^1.13.2": version "1.54.2" resolved "https://registry.yarnpkg.com/@stripe/stripe-js/-/stripe-js-1.54.2.tgz#0665848e22cbda936cfd05256facdfbba121438d" @@ -764,11 +609,6 @@ "@types/node" "*" base-x "^3.0.6" -"@types/estree@*", "@types/estree@^1.0.0": - version "1.0.5" - resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4" - integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw== - "@types/geojson@*": version "7946.0.14" resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.14.tgz#319b63ad6df705ee2a65a73ef042c8271e696613" @@ -1127,13 +967,6 @@ acorn@^8.0.4, acorn@^8.9.0: resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.3.tgz#71e0b14e13a4ec160724b38fb7b0f233b1b81d7a" integrity sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg== -agent-base@6: - version "6.0.2" - resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" - integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== - dependencies: - debug "4" - ajv@^6.12.4: version "6.12.6" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" @@ -1444,14 +1277,6 @@ caniuse-lite@^1.0.30001579: resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001589.tgz#7ad6dba4c9bf6561aec8291976402339dc157dfb" integrity sha512-vNQWS6kI+q6sBlHbh71IIeC+sRwK2N3EDySc/updIGhIee2x5z00J4c1242/5/d6EpEMdOnk/m+6tuk4/tcsqg== -chalk@3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-3.0.0.tgz#3f73c2bf526591f574cc492c51e2456349f844e4" - integrity sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg== - dependencies: - ansi-styles "^4.1.0" - supports-color "^7.1.0" - chalk@^2.4.2: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" @@ -1552,11 +1377,6 @@ commander@^7.2.0: resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7" integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw== -commondir@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" - integrity sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg== - concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" @@ -1631,13 +1451,6 @@ debounce@^2.0.0: resolved "https://registry.yarnpkg.com/debounce/-/debounce-2.0.0.tgz#b2f914518a1481466f4edaee0b063e4d473ad549" integrity sha512-xRetU6gL1VJbs85Mc4FoEGSjQxzpdxRyFhe3lmWFyy2EzydIcD4xzUvRJMD+NPDfMwKNhxa3PvsIOU32luIWeA== -debug@4, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4: - version "4.3.4" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" - integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== - dependencies: - ms "2.1.2" - debug@^3.2.7: version "3.2.7" resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" @@ -1645,6 +1458,13 @@ debug@^3.2.7: dependencies: ms "^2.1.1" +debug@^4.3.1, debug@^4.3.2, debug@^4.3.4: + version "4.3.4" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" + integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== + dependencies: + ms "2.1.2" + deep-is@^0.1.3: version "0.1.4" resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" @@ -2100,11 +1920,6 @@ estraverse@^5.1.0, estraverse@^5.2.0, estraverse@^5.3.0: resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== -estree-walker@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac" - integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w== - esutils@^2.0.2: version "2.0.3" resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" @@ -2382,17 +2197,6 @@ glob@^7.1.3: once "^1.3.0" path-is-absolute "^1.0.0" -glob@^8.0.3: - version "8.1.0" - resolved "https://registry.yarnpkg.com/glob/-/glob-8.1.0.tgz#d388f656593ef708ee3e34640fdfb99a9fd1c33e" - integrity sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^5.0.1" - once "^1.3.0" - globals@^13.19.0: version "13.24.0" resolved "https://registry.yarnpkg.com/globals/-/globals-13.24.0.tgz#8432a19d78ce0c1e833949c36adb345400bb1171" @@ -2523,7 +2327,7 @@ heic-decode@^2.0.0: dependencies: libheif-js "^1.17.1" -hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.1, hoist-non-react-statics@^3.3.2: +hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.1: version "3.3.2" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== @@ -2553,14 +2357,6 @@ html-tokenize@^2.0.0: readable-stream "~1.0.27-1" through2 "~0.4.1" -https-proxy-agent@^5.0.0: - version "5.0.1" - resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6" - integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA== - dependencies: - agent-base "6" - debug "4" - i18next-http-backend@^2.5: version "2.5.0" resolved "https://registry.yarnpkg.com/i18next-http-backend/-/i18next-http-backend-2.5.0.tgz#8396a7df30bfe722eff7a65f629df32a61720414" @@ -2781,13 +2577,6 @@ is-plain-object@^5.0.0: resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-5.0.0.tgz#4427f50ab3429e9025ea7d52e9043a9ef4159344" integrity sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q== -is-reference@1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/is-reference/-/is-reference-1.2.1.tgz#8b2dac0b371f4bc994fdeaba9eb542d03002d0b7" - integrity sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ== - dependencies: - "@types/estree" "*" - is-regex@^1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958" @@ -3041,7 +2830,7 @@ lines-and-columns@^1.1.6: resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== -localforage@^1.8.1, localforage@^1.9.0: +localforage@^1.9.0: version "1.10.0" resolved "https://registry.yarnpkg.com/localforage/-/localforage-1.10.0.tgz#5c465dc5f62b2807c3a84c0c6a1b1b3212781dd4" integrity sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg== @@ -3099,13 +2888,6 @@ lru-cache@^6.0.0: resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.2.0.tgz#0bd445ca57363465900f4d1f9bd8db343a4d95c3" integrity sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q== -magic-string@^0.27.0: - version "0.27.0" - resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.27.0.tgz#e4a3413b4bab6d98d2becffd48b4a257effdbbf3" - integrity sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA== - dependencies: - "@jridgewell/sourcemap-codec" "^1.4.13" - "memoize-one@>=3.1.1 <6", memoize-one@^5.0.0: version "5.2.1" resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e" @@ -3155,13 +2937,6 @@ 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.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.0, minimist@^1.2.6, minimist@~1.2.5: version "1.2.8" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" @@ -3172,13 +2947,6 @@ minimist@^1.2.0, minimist@^1.2.6, minimist@~1.2.5: resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.0.4.tgz#dbce03740f50a4786ba994c1fb908844d27b038c" integrity sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ== -mkdirp@^0.5.5: - version "0.5.6" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6" - integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw== - dependencies: - minimist "^1.2.6" - ml-array-max@^1.2.4: version "1.2.4" resolved "https://registry.yarnpkg.com/ml-array-max/-/ml-array-max-1.2.4.tgz#2373e2b7e51c8807e456cc0ef364c5863713623b" @@ -3266,7 +3034,7 @@ next@^14.1: "@next/swc-win32-ia32-msvc" "14.1.0" "@next/swc-win32-x64-msvc" "14.1.0" -node-fetch@^2.6.1, node-fetch@^2.6.12, node-fetch@^2.6.7: +node-fetch@^2.6.1, node-fetch@^2.6.12: version "2.7.0" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== @@ -3549,11 +3317,6 @@ process-nextick-args@~2.0.0: resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== -progress@^2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" - integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== - prop-types-extra@^1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/prop-types-extra/-/prop-types-extra-1.1.1.tgz#58c3b74cbfbb95d304625975aa2f0848329a010b" @@ -3852,7 +3615,7 @@ resolve-pkg-maps@^1.0.0: resolved "https://registry.yarnpkg.com/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz#616b3dc2c57056b5588c31cdf4b3d64db133720f" integrity sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw== -resolve@1.22.8, resolve@^1.19.0, resolve@^1.22.4: +resolve@^1.19.0, resolve@^1.22.4: version "1.22.8" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== @@ -3887,13 +3650,6 @@ rimraf@^3.0.2: dependencies: glob "^7.1.3" -rollup@2.78.0: - version "2.78.0" - resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.78.0.tgz#00995deae70c0f712ea79ad904d5f6b033209d9e" - integrity sha512-4+YfbQC9QEVvKTanHhIAFVUFSRsezvQF8vFOJwtGfb9Bb+r014S+qryr9PSmw8x6sMnPkmFBGAvIFVQxvJxjtg== - optionalDependencies: - fsevents "~2.3.2" - run-parallel@^1.1.9: version "1.2.0" resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" @@ -4081,13 +3837,6 @@ source-map@^0.5.7: resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" integrity sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ== -stacktrace-parser@^0.1.10: - version "0.1.10" - resolved "https://registry.yarnpkg.com/stacktrace-parser/-/stacktrace-parser-0.1.10.tgz#29fb0cae4e0d0b85155879402857a1639eb6051a" - integrity sha512-KJP1OCML99+8fhOHxwwzyWrlUuVX5GQ0ZpJTd1DFXhdkrvg1szxfHhawXUZ3g9TkXORQd4/WG68jMlQZ2p8wlg== - dependencies: - type-fest "^0.7.1" - streamsearch@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-1.1.0.tgz#404dd1e2247ca94af554e841a8ef0eaa238da764" @@ -4356,11 +4105,6 @@ type-fest@^0.20.2: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== -type-fest@^0.7.1: - version "0.7.1" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.7.1.tgz#8dda65feaf03ed78f0a3f9678f1869147f7c5c48" - integrity sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg== - type-fest@^2.19.0: version "2.19.0" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.19.0.tgz#88068015bb33036a598b952e55e9311a60fd3a9b" @@ -4503,11 +4247,6 @@ webpack-bundle-analyzer@4.10.1: sirv "^2.0.3" ws "^7.3.1" -"webpack-sources@^2.0.0 || ^3.0.0": - version "3.2.3" - resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde" - integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w== - whatwg-url@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" @@ -4566,7 +4305,7 @@ which-typed-array@^1.1.14, which-typed-array@^1.1.9: gopd "^1.0.1" has-tostringtag "^1.0.1" -which@^2.0.1, which@^2.0.2: +which@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==