Переглянути джерело

Merge branch 'main' of https://github.com/ente-io/auth into mobile_face

Neeraj Gupta 1 рік тому
батько
коміт
f9dd509d61
100 змінених файлів з 1899 додано та 724 видалено
  1. 3 0
      .gitattributes
  2. 5 4
      .github/workflows/auth-crowdin.yml
  3. 2 4
      .github/workflows/auth-lint.yml
  4. 2 5
      .github/workflows/auth-release.yml
  5. 1 1
      .github/workflows/cli-release.yml
  6. 47 0
      .github/workflows/docs-deploy.yml
  7. 37 0
      .github/workflows/docs-verify-build.yml
  8. 5 4
      .github/workflows/mobile-crowdin.yml
  9. 2 4
      .github/workflows/mobile-lint.yml
  10. 4 2
      .github/workflows/mobile-release.yml
  11. 2 4
      .github/workflows/server-lint.yml
  12. 5 4
      .github/workflows/web-crowdin.yml
  13. 43 0
      .github/workflows/web-deploy-accounts.yml
  14. 43 0
      .github/workflows/web-deploy-auth.yml
  15. 43 0
      .github/workflows/web-deploy-cast.yml
  16. 43 0
      .github/workflows/web-deploy-photos.yml
  17. 2 13
      .github/workflows/web-lint.yml
  18. 94 0
      .github/workflows/web-nightly.yml
  19. 52 0
      .github/workflows/web-preview.yml
  20. 0 4
      .gitmodules
  21. 2 2
      CONTRIBUTING.md
  22. 1 1
      README.md
  23. 1 1
      SUPPORT.md
  24. 1 1
      auth/README.md
  25. 8 16
      auth/assets/custom-icons/_data/custom-icons.json
  26. 1 7
      auth/ios/Podfile.lock
  27. 2 2
      auth/lib/core/logging/super_logging.dart
  28. 3 2
      auth/lib/l10n/arb/app_en.arb
  29. 61 1
      auth/lib/l10n/arb/app_sv.arb
  30. 5 1
      auth/lib/l10n/arb/app_zh.arb
  31. 13 0
      auth/lib/models/account/two_factor.dart
  32. 22 0
      auth/lib/services/passkey_service.dart
  33. 14 1
      auth/lib/services/user_service.dart
  34. 44 22
      auth/lib/ui/passkey_page.dart
  35. 0 120
      auth/lib/ui/settings/app_update_dialog.dart
  36. 28 1
      auth/lib/ui/settings/security_section_widget.dart
  37. 2 1
      auth/lib/ui/tools/lock_screen.dart
  38. 6 1
      auth/lib/ui/two_factor_authentication_page.dart
  39. 4 0
      auth/lib/ui/two_factor_recovery_page.dart
  40. 17 25
      auth/pubspec.lock
  41. 1 3
      auth/pubspec.yaml
  42. 8 1
      cli/README.md
  43. 40 3
      cli/cmd/account.go
  44. 90 0
      cli/cmd/admin.go
  45. 8 2
      cli/cmd/root.go
  46. 1 1
      cli/cmd/version.go
  47. 10 0
      cli/config.yaml.example
  48. 28 0
      cli/docs/generated/ente.md
  49. 19 0
      cli/docs/generated/ente_account.md
  50. 19 0
      cli/docs/generated/ente_account_add.md
  51. 21 0
      cli/docs/generated/ente_account_get-token.md
  52. 19 0
      cli/docs/generated/ente_account_list.md
  53. 22 0
      cli/docs/generated/ente_account_update.md
  54. 22 0
      cli/docs/generated/ente_admin.md
  55. 21 0
      cli/docs/generated/ente_admin_disable-2fa.md
  56. 21 0
      cli/docs/generated/ente_admin_get-user-id.md
  57. 22 0
      cli/docs/generated/ente_admin_update-subscription.md
  58. 16 0
      cli/docs/generated/ente_auth.md
  59. 19 0
      cli/docs/generated/ente_auth_decrypt.md
  60. 19 0
      cli/docs/generated/ente_export.md
  61. 19 0
      cli/docs/generated/ente_version.md
  62. 27 0
      cli/docs/selfhost.md
  63. 2 0
      cli/go.mod
  64. 2 0
      cli/go.sum
  65. 57 0
      cli/internal/api/admin.go
  66. 12 1
      cli/internal/api/files.go
  67. 10 0
      cli/internal/api/log.go
  68. 16 0
      cli/internal/api/models/user_details.go
  69. 76 2
      cli/internal/promt.go
  70. 32 4
      cli/main.go
  71. 23 1
      cli/pkg/account.go
  72. 114 0
      cli/pkg/admin_actions.go
  73. 6 1
      cli/pkg/model/account.go
  74. 6 0
      cli/pkg/model/admin.go
  75. 12 1
      cli/release.sh
  76. 1 0
      cli/utils/constants/constants.go
  77. 31 0
      cli/utils/convert.go
  78. 0 14
      cli/utils/time.go
  79. 0 3
      desktop/.github/workflows/build.yml
  80. 19 10
      desktop/.gitignore
  81. 0 11
      desktop/.husky/pre-commit
  82. 5 4
      desktop/.prettierrc.json
  83. 0 1
      desktop/.yarnrc
  84. 70 70
      desktop/CHANGELOG.md
  85. 13 18
      desktop/README.md
  86. 26 16
      desktop/build/error.html
  87. 46 26
      desktop/build/splash.html
  88. 22 22
      desktop/build/version.html
  89. 0 25
      desktop/deployment.md
  90. 11 0
      desktop/docs/README.md
  91. 14 0
      desktop/docs/dependencies.md
  92. 4 0
      desktop/docs/dev.md
  93. 21 0
      desktop/docs/electron.md
  94. 66 85
      desktop/package.json
  95. 0 94
      desktop/sentry-symbols.js
  96. 0 3
      desktop/sentry.properties
  97. 10 10
      desktop/src/api/cache.ts
  98. 12 13
      desktop/src/api/clip.ts
  99. 12 17
      desktop/src/api/common.ts
  100. 6 8
      desktop/src/api/electronStore.ts

+ 3 - 0
.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

+ 5 - 4
.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 every 24 hours]
+        - cron: "50 1 * * *"
+    # Also allow manually running the workflow
+    workflow_dispatch:
 
 jobs:
     synchronize-with-crowdin:

+ 2 - 4
.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"

+ 2 - 5
.github/workflows/auth-release.yml

@@ -29,7 +29,7 @@ on:
             - "auth-v*"
 
 env:
-    FLUTTER_VERSION: "3.16.9"
+    FLUTTER_VERSION: "3.13.4"
 
 jobs:
     build-ubuntu:
@@ -118,14 +118,11 @@ jobs:
                   updateOnlyUnreleased: true
 
             - name: Upload AAB to PlayStore
-              # Temporarily disable GP upload, enable this once desktop build
-              # testing is complete.
-              if: false
               uses: r0adkll/upload-google-play@v1
               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:

+ 1 - 1
.github/workflows/cli-release.yml

@@ -49,6 +49,6 @@ jobs:
                   project_path: "./cli"
                   pre_command: export CGO_ENABLED=0
                   build_flags: "-trimpath"
-                  ldflags: "-s -w"
+                  ldflags: "-X main.AppVersion=${{ github.ref_name }} -s -w"
                   md5sum: false
                   sha256sum: true

+ 47 - 0
.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"

+ 37 - 0
.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

+ 5 - 4
.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 every 24 hours]
+        - cron: "40 1 * * *"
+    # Also allow manually running the workflow
+    workflow_dispatch:
 
 jobs:
     synchronize-with-crowdin:

+ 2 - 4
.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]
         paths:
             - "mobile/**"
             - ".github/workflows/mobile-lint.yml"

+ 4 - 2
.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 }}
@@ -52,5 +54,5 @@ jobs:
             - 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

+ 2 - 4
.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"

+ 5 - 4
.github/workflows/web-crowdin.yml

@@ -3,15 +3,16 @@ 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
+        # See: [Note: Run every 24 hours]
+        - cron: "20 1 * * *"
+    # Also allow manually running the workflow
+    workflow_dispatch:
 
 jobs:
     synchronize-with-crowdin:

+ 43 - 0
.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"

+ 43 - 0
.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"

+ 43 - 0
.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"

+ 43 - 0
.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"

+ 2 - 13
.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"

+ 94 - 0
.github/workflows/web-nightly.yml

@@ -0,0 +1,94 @@
+name: "Nightly (web)"
+
+on:
+    schedule:
+        # [Note: Run 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"

+ 52 - 0
.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"

+ 0 - 4
.gitmodules

@@ -9,10 +9,6 @@
 [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

+ 2 - 2
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.

+ 1 - 1
README.md

@@ -70,7 +70,7 @@ existing users will be grandfathered in.
 [<img height="42" src=".github/assets/app-store-badge.svg">](https://apps.apple.com/app/id6444121398)
 [<img height="42" src=".github/assets/play-store-badge.png">](https://play.google.com/store/apps/details?id=io.ente.auth)
 [<img height="42" src=".github/assets/f-droid-badge.png">](https://f-droid.org/packages/io.ente.auth/)
-[<img height="42" src=".github/assets/github-badge.png">](https://github.com/ente-io/ente/releases?q=tag%3Av2.0.34&expanded=true)
+[<img height="42" src=".github/assets/github-badge.png">](https://github.com/ente-io/ente/releases?q=tag%3Aauth-v2)
 [<img height="42" src=".github/assets/web-badge.svg">](https://auth.ente.io)
 
 </div>

+ 1 - 1
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:
 

+ 1 - 1
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.
 

+ 8 - 16
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"
     }
   ]
 }

+ 1 - 7
auth/ios/Podfile.lock

@@ -71,8 +71,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
@@ -126,7 +124,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`)
@@ -183,8 +180,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:
@@ -214,7 +209,7 @@ SPEC CHECKSUMS:
   file_picker: ce3938a0df3cc1ef404671531facef740d03f920
   file_saver: 503e386464dbe118f630e17b4c2e1190fa0cf808
   fk_user_agent: 1f47ec39291e8372b1d692b50084b0d54103c545
-  Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
+  Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
   flutter_email_sender: 02d7443217d8c41483223627972bfdc09f74276b
   flutter_inappwebview: acd4fc0f012cefd09015000c241137d82f01ba62
   flutter_local_notifications: 0c0b1ae97e741e1521e4c1629a459d04b9aec743
@@ -226,7 +221,6 @@ SPEC CHECKSUMS:
   local_auth_ios: c6cf091ded637a88f24f86a8875d8b0f526e2605
   move_to_background: 39a5b79b26d577b0372cbe8a8c55e7aa9fcd3a2d
   MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb
-  open_filex: 6e26e659846ec990262224a12ef1c528bb4edbe4
   OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c
   package_info_plus: fd030dabf36271f146f1f3beacd48f564b0f17f7
   path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943

+ 2 - 2
auth/lib/core/logging/super_logging.dart

@@ -167,7 +167,7 @@ class SuperLogging {
       await setupLogDir();
     }
     if (sentryIsEnabled) {
-      await setupSentry();
+      setupSentry().ignore();
     }
 
     Logger.root.level = Level.ALL;
@@ -250,7 +250,7 @@ class SuperLogging {
 
     // add error to sentry queue
     if (sentryIsEnabled && rec.error != null) {
-      await _sendErrorToSentry(rec.error!, null);
+      _sendErrorToSentry(rec.error!, null).ignore();
     }
   }
 

+ 3 - 2
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",
@@ -407,7 +408,7 @@
   "hearUsWhereTitle": "How did you hear about Ente? (optional)",
   "hearUsExplanation": "We don't track app installs. It'd help if you told us where you found us!",
   "waitingForBrowserRequest": "Waiting for browser request...",
-  "launchPasskeyUrlAgain": "Launch passkey URL again",
+  "waitingForVerification": "Waiting for verification...",
   "passkey": "Passkey",
   "developerSettingsWarning":"Are you sure that you want to modify Developer settings?",
   "developerSettings": "Developer settings",

+ 61 - 1
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,45 @@
   "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",
+  "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."
+  }
 }

+ 5 - 1
auth/lib/l10n/arb/app_zh.arb

@@ -144,6 +144,7 @@
   "enterCodeHint": "从你的身份验证器应用中\n输入6位数字代码",
   "lostDeviceTitle": "丢失了设备吗?",
   "twoFactorAuthTitle": "双因素认证",
+  "passkeyAuthTitle": "通行密钥认证",
   "recoverAccount": "恢复账户",
   "enterRecoveryKeyHint": "输入您的恢复密钥",
   "recover": "恢复",
@@ -404,5 +405,8 @@
   "signOutOtherDevices": "登出其他设备",
   "doNotSignOut": "不要退登",
   "hearUsWhereTitle": "您是如何知道Ente的? (可选的)",
-  "hearUsExplanation": "我们不跟踪应用程序安装情况。如果您告诉我们您是在哪里找到我们的,将会有所帮助!"
+  "hearUsExplanation": "我们不跟踪应用程序安装情况。如果您告诉我们您是在哪里找到我们的,将会有所帮助!",
+  "waitingForBrowserRequest": "正在等待浏览器请求...",
+  "launchPasskeyUrlAgain": "再次启动 通行密钥 URL",
+  "passkey": "通行密钥"
 }

+ 13 - 0
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;
+  }
+}

+ 22 - 0
auth/lib/services/passkey_service.dart

@@ -17,6 +17,28 @@ class PasskeyService {
     return response.data!["accountsToken"] as String;
   }
 
+  Future<bool> isPasskeyRecoveryEnabled() async {
+    final response = await _enteDio.get(
+      "/users/two-factor/recovery-status",
+    );
+    return response.data!["isPasskeyRecoveryEnabled"] as bool;
+  }
+
+  Future<void> 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<void> openPasskeyPage(BuildContext context) async {
     try {
       final jwtToken = await getJwtToken();

+ 14 - 1
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';
@@ -762,7 +763,11 @@ class UserService {
     }
   }
 
-  Future<void> recoverTwoFactor(BuildContext context, String sessionID) async {
+  Future<void> recoverTwoFactor(
+    BuildContext context,
+    String sessionID,
+    TwoFactorType type,
+  ) async {
     final dialog = createProgressDialog(context, context.l10n.pleaseWait);
     await dialog.show();
     try {
@@ -770,6 +775,7 @@ class UserService {
         _config.getHttpEndpoint() + "/users/two-factor/recover",
         queryParameters: {
           "sessionID": sessionID,
+          "twoFactorType": twoFactorTypeToString(type),
         },
       );
       if (response.statusCode == 200) {
@@ -778,6 +784,7 @@ class UserService {
           MaterialPageRoute(
             builder: (BuildContext context) {
               return TwoFactorRecoveryPage(
+                type,
                 sessionID,
                 response.data["encryptedSecret"],
                 response.data["secretDecryptionNonce"],
@@ -788,6 +795,7 @@ class UserService {
         );
       }
     } on DioError catch (e) {
+      await dialog.hide();
       _logger.severe(e);
       if (e.response != null && e.response!.statusCode == 404) {
         showToast(context, context.l10n.sessionExpired);
@@ -809,6 +817,7 @@ class UserService {
         );
       }
     } catch (e) {
+      await dialog.hide();
       _logger.severe(e);
       // ignore: unawaited_futures
       showErrorDialog(
@@ -823,6 +832,7 @@ class UserService {
 
   Future<void> removeTwoFactor(
     BuildContext context,
+    TwoFactorType type,
     String sessionID,
     String recoveryKey,
     String encryptedSecret,
@@ -862,6 +872,7 @@ class UserService {
         data: {
           "sessionID": sessionID,
           "secret": secret,
+          "twoFactorType": twoFactorTypeToString(type),
         },
       );
       if (response.statusCode == 200) {
@@ -881,6 +892,7 @@ class UserService {
         );
       }
     } on DioError catch (e) {
+      await dialog.hide();
       _logger.severe(e);
       if (e.response != null && e.response!.statusCode == 404) {
         showToast(context, "Session expired");
@@ -902,6 +914,7 @@ class UserService {
         );
       }
     } catch (e) {
+      await dialog.hide();
       _logger.severe(e);
       // ignore: unawaited_futures
       showErrorDialog(

+ 44 - 22
auth/lib/ui/passkey_page.dart

@@ -1,9 +1,11 @@
 import 'dart:convert';
 
 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';
@@ -99,30 +101,50 @@ class _PasskeyPageState extends State<PasskeyPage> {
   }
 
   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,
+                    ),
+                  ),
+                ),
+              ),
+            ),
+          ],
+        ),
       ),
     );
   }

+ 0 - 120
auth/lib/ui/settings/app_update_dialog.dart

@@ -1,14 +1,7 @@
-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: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 {
@@ -114,116 +107,3 @@ class _AppUpdateDialogState extends State<AppUpdateDialog> {
     );
   }
 }
-
-class ApkDownloaderDialog extends StatefulWidget {
-  final LatestVersionInfo? versionInfo;
-
-  const ApkDownloaderDialog(this.versionInfo, {Key? key}) : super(key: key);
-
-  @override
-  State<ApkDownloaderDialog> createState() => _ApkDownloaderDialogState();
-}
-
-class _ApkDownloaderDialogState extends State<ApkDownloaderDialog> {
-  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: const Text(
-          "Downloading...",
-          style: TextStyle(
-            fontSize: 16,
-          ),
-          textAlign: TextAlign.center,
-        ),
-        content: LinearProgressIndicator(
-          value: _downloadProgress,
-          valueColor: AlwaysStoppedAnimation<Color>(
-            Theme.of(context).colorScheme.alternativeColor,
-          ),
-        ),
-      ),
-    );
-  }
-
-  Future<void> _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');
-      // ignore: unawaited_futures
-      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,
-              );
-            },
-          ),
-        ],
-      );
-      // ignore: unawaited_futures
-      showDialog(
-        context: context,
-        builder: (BuildContext context) {
-          return alert;
-        },
-        barrierColor: Colors.black87,
-      );
-      return;
-    }
-  }
-}

+ 28 - 1
auth/lib/ui/settings/security_section_widget.dart

@@ -21,6 +21,7 @@ import 'package:ente_auth/utils/dialog_util.dart';
 import 'package:ente_auth/utils/navigation_util.dart';
 import 'package:ente_auth/utils/toast_util.dart';
 import 'package:flutter/material.dart';
+import 'package:logging/logging.dart';
 
 class SecuritySectionWidget extends StatefulWidget {
   const SecuritySectionWidget({Key? key}) : super(key: key);
@@ -32,6 +33,7 @@ class SecuritySectionWidget extends StatefulWidget {
 class _SecuritySectionWidgetState extends State<SecuritySectionWidget> {
   final _config = Configuration.instance;
   late bool _hasLoggedIn;
+  final Logger _logger = Logger('SecuritySectionWidget');
 
   @override
   void initState() {
@@ -75,7 +77,7 @@ class _SecuritySectionWidgetState extends State<SecuritySectionWidget> {
             pressedColor: getEnteColorScheme(context).fillFaint,
             trailingIcon: Icons.chevron_right_outlined,
             trailingIconIsMuted: true,
-            onTap: () => PasskeyService.instance.openPasskeyPage(context),
+            onTap: () async => await onPasskeyClick(context),
           ),
         sectionOptionSpacing,
         MenuItemWidget(
@@ -159,6 +161,31 @@ class _SecuritySectionWidgetState extends State<SecuritySectionWidget> {
     );
   }
 
+  Future<void> 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<void> updateEmailMFA(bool enableEmailMFA) async {
     try {
       final UserDetails details =

+ 2 - 1
auth/lib/ui/tools/lock_screen.dart

@@ -56,7 +56,8 @@ class _LockScreenState extends State<LockScreen> with WidgetsBindingObserver {
                     text: context.l10n.unlock,
                     iconData: Icons.lock_open_outlined,
                     onTap: () async {
-                      await _showLockScreen(source: "tapUnlock");
+                      // ignore: unawaited_futures
+                      _showLockScreen(source: "tapUnlock");
                     },
                   ),
                 ),

+ 6 - 1
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';
@@ -129,7 +130,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),

+ 4 - 0
auth/lib/ui/two_factor_recovery_page.dart

@@ -1,6 +1,7 @@
 import 'dart:ui';
 
 import 'package:ente_auth/l10n/l10n.dart';
+import 'package:ente_auth/models/account/two_factor.dart';
 import 'package:ente_auth/services/user_service.dart';
 import 'package:ente_auth/utils/dialog_util.dart';
 import 'package:flutter/material.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, {
@@ -72,6 +75,7 @@ class _TwoFactorRecoveryPageState extends State<TwoFactorRecoveryPage> {
                   ? () async {
                       await UserService.instance.removeTwoFactor(
                         context,
+                        widget.type,
                         widget.sessionID,
                         _recoveryKey.text,
                         widget.encryptedSecret,

+ 17 - 25
auth/pubspec.lock

@@ -197,10 +197,10 @@ packages:
     dependency: "direct main"
     description:
       name: collection
-      sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
+      sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687
       url: "https://pub.dev"
     source: hosted
-    version: "1.18.0"
+    version: "1.17.2"
   computer:
     dependency: "direct main"
     description:
@@ -827,10 +827,10 @@ packages:
     dependency: transitive
     description:
       name: meta
-      sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e
+      sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3"
       url: "https://pub.dev"
     source: hosted
-    version: "1.10.0"
+    version: "1.9.1"
   mime:
     dependency: transitive
     description:
@@ -879,14 +879,6 @@ packages:
       url: "https://pub.dev"
     source: hosted
     version: "2.0.2"
-  open_filex:
-    dependency: "direct main"
-    description:
-      name: open_filex
-      sha256: "854aefd72dfd74219dc8c8d1767c34ec1eae64b8399a5be317bddb1ec2108915"
-      url: "https://pub.dev"
-    source: hosted
-    version: "4.3.2"
   otp:
     dependency: "direct main"
     description:
@@ -1304,10 +1296,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:
@@ -1320,10 +1312,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:
@@ -1368,26 +1360,26 @@ packages:
     dependency: transitive
     description:
       name: test
-      sha256: a1f7595805820fcc05e5c52e3a231aedd0b72972cb333e8c738a8b1239448b6f
+      sha256: "13b41f318e2a5751c3169137103b60c584297353d4b1761b66029bae6411fe46"
       url: "https://pub.dev"
     source: hosted
-    version: "1.24.9"
+    version: "1.24.3"
   test_api:
     dependency: transitive
     description:
       name: test_api
-      sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b"
+      sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8"
       url: "https://pub.dev"
     source: hosted
-    version: "0.6.1"
+    version: "0.6.0"
   test_core:
     dependency: transitive
     description:
       name: test_core
-      sha256: a757b14fc47507060a162cc2530d9a4a2f92f5100a952c7443b5cad5ef5b106a
+      sha256: "99806e9e6d95c7b059b7a0fc08f07fc53fabe54a829497f0d9676299f1e8637e"
       url: "https://pub.dev"
     source: hosted
-    version: "0.5.9"
+    version: "0.5.3"
   timezone:
     dependency: transitive
     description:
@@ -1576,10 +1568,10 @@ packages:
     dependency: transitive
     description:
       name: web
-      sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152
+      sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10
       url: "https://pub.dev"
     source: hosted
-    version: "0.3.0"
+    version: "0.1.4-beta"
   web_socket_channel:
     dependency: transitive
     description:
@@ -1637,5 +1629,5 @@ packages:
     source: hosted
     version: "3.1.2"
 sdks:
-  dart: ">=3.2.0-194.0.dev <4.0.0"
+  dart: ">=3.1.0-185.0.dev <4.0.0"
   flutter: ">=3.10.0"

+ 1 - 3
auth/pubspec.yaml

@@ -1,6 +1,6 @@
 name: ente_auth
 description: ente two-factor authenticator
-version: 2.0.35+235
+version: 2.0.42+242
 publish_to: none
 
 environment:
@@ -54,13 +54,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

+ 8 - 1
cli/README.md

@@ -64,7 +64,14 @@ ente account update --email email@domain.com --dir ~/photos
 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
 

+ 40 - 3
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)
 }

+ 90 - 0
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)
+}

+ 8 - 2
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)

+ 1 - 1
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)
 	},
 }
 

+ 10 - 0
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

+ 28 - 0
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

+ 19 - 0
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

+ 19 - 0
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

+ 21 - 0
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

+ 19 - 0
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

+ 22 - 0
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

+ 22 - 0
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

+ 21 - 0
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

+ 21 - 0
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

+ 22 - 0
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

+ 16 - 0
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

+ 19 - 0
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

+ 19 - 0
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

+ 19 - 0
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

+ 27 - 0
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
+
+# ....
+```

+ 2 - 0
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 (

+ 2 - 0
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=

+ 57 - 0
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
+
+}

+ 12 - 1
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(),

+ 10 - 0
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) {

+ 16 - 0
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"`
+}

+ 76 - 2
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")

+ 32 - 4
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.

+ 23 - 1
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
 }

+ 114 - 0
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)
+}

+ 6 - 1
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)
+}

+ 6 - 0
cli/pkg/model/admin.go

@@ -0,0 +1,6 @@
+package model
+
+type AdminActionForUser struct {
+	UserEmail  string
+	AdminEmail string
+}

+ 12 - 1
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
 
@@ -29,7 +40,7 @@ do
         fi
 
         # Build the binary and place it in the "bin" directory
-        go build -ldflags="-s -w" -trimpath -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"

+ 1 - 0
cli/utils/constants/constants.go

@@ -1,3 +1,4 @@
 package constants
 
 const CliDataPath = "/cli-data/"
+const EnteApiUrl = "https://api.ente.io"

+ 31 - 0
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])
+}

+ 0 - 14
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])
-}

+ 0 - 3
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

+ 19 - 10
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/

+ 0 - 11
desktop/.husky/pre-commit

@@ -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

+ 5 - 4
desktop/.prettierrc.json

@@ -1,6 +1,7 @@
 {
     "tabWidth": 4,
-    "trailingComma": "es5",
-    "singleQuote": true,
-    "bracketSameLine": true
-}
+    "plugins": [
+        "prettier-plugin-organize-imports",
+        "prettier-plugin-packagejson"
+    ]
+}

+ 0 - 1
desktop/.yarnrc

@@ -1 +0,0 @@
-network-timeout 500000

+ 70 - 70
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
 

+ 13 - 18
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`.
+
+Or create a binary for your platform
 
-```bash
-yarn watch
+```sh
+yarn build
 ```
+
+That's the gist of it. For more development related documentation, see
+[docs](docs/README.md).

+ 26 - 16
desktop/build/error.html

@@ -1,20 +1,30 @@
-<!DOCTYPE html>
+<!doctype html>
 <html lang="en">
+    <head>
+        <meta charset="UTF-8" />
+        <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+        <title>ente Photos</title>
+    </head>
 
-<head>
-    <meta charset="UTF-8">
-    <meta http-equiv="X-UA-Compatible" content="IE=edge">
-    <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>ente Photos</title>
-</head>
-
-<body style="background-color: black;">
-    <div style=" height: 95vh;width: 96vw; display: grid; place-items: center; color: white;">
-        <div>
-            <div style="margin-bottom: 10px;">Site unreachable, please try again later</div>
-            <button onClick="window[`ElectronAPIs`].reloadWindow()">Reload</button>
+    <body style="background-color: black">
+        <div
+            style="
+                height: 95vh;
+                width: 96vw;
+                display: grid;
+                place-items: center;
+                color: white;
+            "
+        >
+            <div>
+                <div style="margin-bottom: 10px">
+                    Site unreachable, please try again later
+                </div>
+                <button onClick="window[`ElectronAPIs`].reloadWindow()">
+                    Reload
+                </button>
+            </div>
         </div>
-    </div>
-</body>
-
+    </body>
 </html>

+ 46 - 26
desktop/build/splash.html

@@ -1,30 +1,50 @@
-<!DOCTYPE html>
+<!doctype html>
 <html lang="en">
+    <head>
+        <meta charset="UTF-8" />
+        <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+        <title>ente Photos</title>
+    </head>
 
-<head>
-    <meta charset="UTF-8">
-    <meta http-equiv="X-UA-Compatible" content="IE=edge">
-    <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>ente Photos</title>
-</head>
-
-<body style="background-color: black;">
-    <div style="display: flex;
-    flex-direction: column;
-    align-items: center;
-    justify-content: center;
-    height: 90vh;">
-        <div style="width:64px;"><svg version="1.1" id="L9" xmlns="http://www.w3.org/2000/svg"
-                xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 100 100"
-                enable-background="new 0 0 0 0" xml:space="preserve">
-                <path fill="#2dc262"
-                    d="M73,50c0-12.7-10.3-23-23-23S27,37.3,27,50 M30.9,50c0-10.5,8.5-19.1,19.1-19.1S69.1,39.5,69.1,50">
-                    <animateTransform attributeName="transform" attributeType="XML" type="rotate" dur="1s"
-                        from="0 50 50" to="360 50 50" repeatCount="indefinite" />
-                </path>
-            </svg>
+    <body style="background-color: black">
+        <div
+            style="
+                display: flex;
+                flex-direction: column;
+                align-items: center;
+                justify-content: center;
+                height: 90vh;
+            "
+        >
+            <div style="width: 64px">
+                <svg
+                    version="1.1"
+                    id="L9"
+                    xmlns="http://www.w3.org/2000/svg"
+                    xmlns:xlink="http://www.w3.org/1999/xlink"
+                    x="0px"
+                    y="0px"
+                    viewBox="0 0 100 100"
+                    enable-background="new 0 0 0 0"
+                    xml:space="preserve"
+                >
+                    <path
+                        fill="#2dc262"
+                        d="M73,50c0-12.7-10.3-23-23-23S27,37.3,27,50 M30.9,50c0-10.5,8.5-19.1,19.1-19.1S69.1,39.5,69.1,50"
+                    >
+                        <animateTransform
+                            attributeName="transform"
+                            attributeType="XML"
+                            type="rotate"
+                            dur="1s"
+                            from="0 50 50"
+                            to="360 50 50"
+                            repeatCount="indefinite"
+                        />
+                    </path>
+                </svg>
+            </div>
         </div>
-    </div>
-</body>
-
+    </body>
 </html>

+ 22 - 22
desktop/build/version.html

@@ -1,24 +1,24 @@
-<!DOCTYPE html>
+<!doctype html>
 <html>
-<head>
-  <title>Electron Updater Example</title>
-</head>
-<body>
-  Current version: <span id="version">vX.Y.Z</span>
-  <div id="messages"></div>
-<script>
-// Display the current version
-let version = window.location.hash.substring(1);
-document.getElementById('version').innerText = version;
+    <head>
+        <title>Electron Updater Example</title>
+    </head>
+    <body>
+        Current version: <span id="version">vX.Y.Z</span>
+        <div id="messages"></div>
+        <script>
+            // Display the current version
+            let version = window.location.hash.substring(1);
+            document.getElementById("version").innerText = version;
 
-// 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);
-})
-</script>
-</body>
-</html>
+            // 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);
+            });
+        </script>
+    </body>
+</html>

+ 0 - 25
desktop/deployment.md

@@ -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
-```

+ 11 - 0
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.

+ 14 - 0
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.

+ 4 - 0
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.

+ 21 - 0
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.

+ 66 - 85
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 <code@ente.io>",
     "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"
         ],
-        "files": [
-            "app/**/*",
+        "extraFiles": [
             {
-                "from": "ui/apps/photos",
-                "to": "ui",
-                "filter": [
-                    "!**/*",
-                    "out/**/*"
-                ]
+                "from": "build",
+                "to": "resources"
             }
+        ],
+        "files": [
+            "app/**/*",
+            "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 <code@ente.io>",
-    "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"
-        ]
     }
 }

+ 0 - 94
desktop/sentry-symbols.js

@@ -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));

+ 0 - 3
desktop/sentry.properties

@@ -1,3 +0,0 @@
-defaults.url=https://sentry.ente.io/
-defaults.org=ente
-defaults.project=desktop-photos

+ 10 - 10
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<void> {
-    await ipcRenderer.invoke('set-custom-cache-directory', directory);
+    await ipcRenderer.invoke("set-custom-cache-directory", directory);
 }
 
 async function getCustomCacheDirectory(): Promise<string> {
-    return await ipcRenderer.invoke('get-custom-cache-directory');
+    return await ipcRenderer.invoke("get-custom-cache-directory");
 }

+ 12 - 13
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<Float32Array> {
     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<Float32Array> {
     try {
         const embedding = await ipcRenderer.invoke(
-            'compute-text-embedding',
+            "compute-text-embedding",
             model,
-            text
+            text,
         );
         return embedding;
     } catch (err) {

+ 12 - 17
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<string> => {
     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<string> => {
     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<void> => {
     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";

+ 6 - 8
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;
     }
 };

Деякі файли не було показано, через те що забагато файлів було змінено